diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 73ec6b72..00000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -omit = pendulum/locales/*, - pendulum/_compat.py, - pendulum/__version__.py, - pendulum/_extensions/* - pendulum/parsing/iso8601.py - pendulum/utils/_compat.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index facde560..00000000 --- a/.flake8 +++ /dev/null @@ -1,20 +0,0 @@ -[flake8] -max-line-length = 88 -ignore = E501, E203, W503 -per-file-ignores = - __init__.py:F401 - pendulum/tz/timezone.py:F811 -exclude = - .git - __pycache__ - setup.py - build - dist - releases - .idea - .venv - .tox - .mypy_cache - .pytest_cache - .vscode - .github diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 00000000..3c18e72b --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,46 @@ +name: codspeed + +on: + push: + branches: + - "master" + pull_request: + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +jobs: + benchmarks: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: "3.10" + + - name: Get full Python version + id: full-python-version + run: | + echo version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") >> $GITHUB_OUTPUT + + - name: Install poetry + run: | + pipx install poetry>=2 + + - name: Configure poetry + run: poetry config virtualenvs.in-project true + + - name: Install dependencies + run: poetry install --only test --only benchmark --only build -vvv --no-root + + - name: Install pendulum and check extensions + run: | + poetry run pip install -e . -vvv + poetry run python -c 'import pendulum._pendulum' + + - name: Run benchmarks + uses: CodSpeedHQ/action@dbda7111f8ac363564b0c51b992d4ce76bb89f2f # v4.5.2 + with: + mode: simulation + token: ${{ secrets.CODSPEED_TOKEN }} + run: poetry run pytest tests/ --codspeed diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3dc1bf57..a52fdb60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,129 +4,155 @@ on: push: tags: - '*.*.*' + workflow_dispatch: jobs: - - Linux: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Get tag - id: tag - run: | - echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - - name: Building release - run: | - make linux_release - - name: Upload distributions artifacts - uses: actions/upload-artifact@v1 - with: - name: pendulum-dist - path: dist/wheelhouse - - MacOS: - runs-on: macos-latest + build: + name: Build on ${{ matrix.platform || matrix.os }} (${{ matrix.target }} - ${{ matrix.manylinux || 'auto' }}) + environment: release strategy: + fail-fast: false matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + os: [ubuntu, macos, windows] + target: [x86_64, aarch64] + manylinux: [auto] + include: + - os: ubuntu + platform: linux + - os: windows + ls: dir + interpreter: 3.10 3.11 3.12 3.13 3.14 pypy3.11 + - os: windows + ls: dir + target: aarch64 + interpreter: 3.11 3.12 3.13 3.14 + - os: macos + target: aarch64 + interpreter: 3.10 3.11 3.12 3.13 3.14 pypy3.11 + - os: ubuntu + platform: linux + target: aarch64 + # musllinux + - os: ubuntu + platform: linux + target: x86_64 + manylinux: musllinux_1_1 + - os: ubuntu + platform: linux + target: aarch64 + manylinux: musllinux_1_1 + - os: ubuntu + platform: linux + target: ppc64le + interpreter: 3.10 3.11 3.12 3.13 3.14 + - os: ubuntu + platform: linux + target: s390x + interpreter: 3.10 3.11 3.12 3.13 3.14 + runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v2 - - name: Get tag - id: tag - run: | - echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install and set up Poetry - run: | - curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py - python get-poetry.py --preview -y - - name: Build distributions - run: | - source $HOME/.poetry/env - poetry build -vvv - - name: Upload distribution artifacts - uses: actions/upload-artifact@v1 - with: - name: pendulum-dist - path: dist - - Windows: - runs-on: windows-latest - strategy: - matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - steps: - - uses: actions/checkout@v2 - - name: Get tag - id: tag - shell: bash - run: | - echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install and setup Poetry - run: | - Invoke-WebRequest https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py -O get-poetry.py - python get-poetry.py --preview -y - - name: Build distributions - run: | - $env:Path += ";$env:Userprofile\.poetry\bin" - poetry build -vvv - - name: Upload distribution artifact - uses: actions/upload-artifact@v1 - with: - name: pendulum-dist - path: dist + - name: set up python + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: '3.11' + architecture: ${{ matrix.python-architecture || 'x64' }} - Release: - needs: [Linux, MacOS, Windows] + - name: build wheels + uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 + with: + target: ${{ matrix.target }} + manylinux: ${{ matrix.manylinux || 'auto' }} + container: ${{ matrix.container }} + args: --release --out dist --interpreter ${{ matrix.interpreter || '3.10 3.11 3.12 3.13 3.14 pypy3.11' }} ${{ matrix.extra-build-args }} + rust-toolchain: stable + docker-options: -e CI + + - run: ${{ matrix.ls || 'ls -lh' }} dist/ + + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: dist-${{ matrix.os }}-${{ matrix.target }}-${{ matrix.manylinux }} + path: dist + + build_sdist: runs-on: ubuntu-latest + environment: release + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Build sdist + uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: dist-sdist + path: dist + build_no_ext: + runs-on: ubuntu-latest + environment: release steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Get tag - id: tag + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Install and configure Poetry + run: pipx install poetry + - name: Hotswap build backend for Poetry + # Maturin doesn't support building no-extension wheels, so we swap to Poetry for that run: | - echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - - name: Download distribution artifact - uses: actions/download-artifact@master + sed -i -e '/^\[build-system\]/,/^\[/{s/^requires = .*/requires = ["poetry-core>=2.0.0,<3.0.0"]/; s/^build-backend = .*/build-backend = "poetry.core.masonry.api"/}' pyproject.toml + - name: Install dependencies + run: poetry install --only main --only test --only typing --only build + - name: Run poetry build + run: poetry build -f wheel + - name: Upload no-ext wheel + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: - name: pendulum-dist + name: dist-any path: dist - - name: Install and set up Poetry - run: | - curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py - python get-poetry.py --preview -y - - name: Set up cache - uses: actions/cache@v1 + + + Release: + needs: [ build, build_sdist, build_no_ext ] + if: success() + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + environment: + name: pypi + url: https://pypi.org/project/pendulum/ + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download artifacts + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: - path: .venv - key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + pattern: dist* + path: dist + merge-multiple: true + - name: Check distributions run: | ls -la dist - - name: Publish to PyPI - env: - POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} + + - name: Check Version + id: check-version run: | - source $HOME/.poetry/env - poetry publish + [[ "${GITHUB_REF#refs/tags/}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ + || echo prerelease=true >> $GITHUB_OUTPUT + - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 with: - tag_name: ${{ steps.tag.outputs.tag }} - release_name: ${{ steps.tag.outputs.tag }} + artifacts: "dist/*" draft: false - prerelease: false + prerelease: steps.check-version.outputs.prerelease == 'true' + body: "See CHANGELOG.md for details" + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2e8b58e7..ccf6a585 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,154 +1,93 @@ name: Tests -on: [push, pull_request] +on: + push: + paths-ignore: + - 'docs/**' + branches: + - master + pull_request: + paths-ignore: + - 'docs/**' + branches: + - '**' jobs: Linting: + name: Linting runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: - python-version: 3.8 - - name: Linting - run: | - pip install pre-commit - pre-commit run --all-files - Linux: - needs: Linting - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8, pypy3] + python-version: "3.11" + - name: "Install pre-commit" + run: pip install pre-commit + - name: "Install Rust toolchain" + run: rustup component add rustfmt clippy + - run: pre-commit run --all-files - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Get full python version - id: full-python-version - run: | - echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") - - name: Install and set up Poetry - run: | - curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py - python get-poetry.py --preview -y - source $HOME/.poetry/env - poetry config virtualenvs.in-project true - - name: Set up cache - uses: actions/cache@v1 - with: - path: .venv - key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} - - name: Upgrade pip - run: | - source $HOME/.poetry/env - poetry run python -m pip install pip -U - - name: Install dependencies - run: | - source $HOME/.poetry/env - poetry install -vvv - - name: Test Pure Python - run: | - source $HOME/.poetry/env - PENDULUM_EXTENSIONS=0 poetry run pytest -q tests - - name: Test - run: | - source $HOME/.poetry/env - poetry run pytest -q tests - poetry install - - MacOS: - needs: Linting - runs-on: macos-latest + Tests: + name: ${{ matrix.os }} / ${{ matrix.python-version }} + runs-on: ${{ matrix.os }}-latest strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8, pypy3] + os: [Ubuntu, MacOS, Windows] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + defaults: + run: + shell: bash steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Get full python version - id: full-python-version - run: | - echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") - - name: Install and set up Poetry - run: | - curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py - python get-poetry.py --preview -y - source $HOME/.poetry/env - poetry config virtualenvs.in-project true - - name: Set up cache - uses: actions/cache@v1 - with: - path: .venv - key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-fix-${{ hashFiles('**/poetry.lock') }} - - name: Upgrade pip - run: | - source $HOME/.poetry/env - poetry run python -m pip install pip -U - - name: Install dependencies - run: | - source $HOME/.poetry/env - poetry install -vvv - - name: Test Pure Python - run: | - source $HOME/.poetry/env - PENDULUM_EXTENSIONS=0 poetry run pytest -q tests - - name: Test - run: | - source $HOME/.poetry/env - poetry run pytest -q tests - Windows: - needs: Linting - runs-on: windows-latest - strategy: - matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Get full python version - id: full-python-version - shell: bash - run: | - echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") - - name: Install and setup Poetry - run: | - Invoke-WebRequest https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py -O get-poetry.py - python get-poetry.py --preview -y - $env:Path += ";$env:Userprofile\.poetry\bin" - poetry config virtualenvs.in-project true - - name: Set up cache - uses: actions/cache@v1 - with: - path: .venv - key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} - - name: Upgrade pip - run: | - $env:Path += ";$env:Userprofile\.poetry\bin" - poetry run python -m pip install pip -U - - name: Install dependencies - run: | - $env:Path += ";$env:Userprofile\.poetry\bin" - poetry install -vvv - - name: Test Pure Python - run: | - $env:Path += ";$env:Userprofile\.poetry\bin" - $env:PENDULUM_EXTENSIONS = "0" - poetry run pytest -q tests - - name: Test - run: | - $env:Path += ";$env:Userprofile\.poetry\bin" - poetry run pytest -q tests + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Get full Python version + id: full-python-version + run: | + echo version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") >> $GITHUB_OUTPUT + + - name: Install poetry + run: | + pipx install poetry>=2 + + - name: Configure poetry + run: poetry config virtualenvs.in-project true + + - name: Set up cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + id: cache + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Ensure cache is healthy + # MacOS does not come with `timeout` command out of the box + if: steps.cache.outputs.cache-hit == 'true' && matrix.os != 'MacOS' + run: timeout 10s poetry run pip --version || rm -rf .venv + + - name: Install runtime, testing, and typing dependencies + run: poetry install --only main --only test --only typing --only build --no-root -vvv + + - name: Install project + run: poetry run maturin develop + + - name: Run type checking + run: poetry run mypy + + - name: Uninstall typing dependencies + # This ensures pendulum runs without typing_extensions installed + run: poetry sync --only main --only test --only build --no-root -vvv + + - name: Test Pure Python + run: | + PENDULUM_EXTENSIONS=0 poetry run pytest -q tests + + - name: Test + run: | + poetry run pytest -q tests diff --git a/.gitignore b/.gitignore index bb25f8b3..dd3696f6 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ setup.py # editor .vscode +/target +/rust/target diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 42669213..f611b1f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,26 +1,27 @@ -repos: - - repo: https://github.com/psf/black - rev: stable - hooks: - - id: black - - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.8 - hooks: - - id: flake8 - - - repo: https://github.com/timothycrosley/isort - rev: 4.3.21 - hooks: - - id: isort - additional_dependencies: [toml] - exclude: ^.*/?setup\.py$ +ci: + autofix_prs: false +repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + rev: v4.4.0 hooks: - id: trailing-whitespace exclude: ^tests/.*/fixtures/.* - id: end-of-file-fixer exclude: ^tests/.*/fixtures/.* - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.11 + hooks: + - id: ruff + - id: ruff-format + + - repo: local + hooks: + - id: lint-rust + name: Lint Rust + entry: make lint-rust + types: [rust] + language: rust + pass_filenames: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb58a3e..58bb1312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,45 +1,149 @@ # Change Log +## [3.2.0] - 2026-01-30 + +### Added +- Added support for Python 3.14 [#923](https://github.com/python-pendulum/pendulum/pull/923) +- Added upper limit to `time-machine` dependency [#931](https://github.com/python-pendulum/pendulum/pull/931) + +### Changed +- **Dropped support for PyPy below PyPy3.11** [#937](https://github.com/python-pendulum/pendulum/pull/937) +- Optimize usage of `re.` methods [#741](https://github.com/python-pendulum/pendulum/pull/741) +- Fixed `pendulum.parse` not being marked as exported [#693](https://github.com/python-pendulum/pendulum/pull/693) +- Fixed `pendulum.parse('now', tz='...')` ignoring the timezone [#701](https://github.com/python-pendulum/pendulum/pull/701) +- Use `pathlib` to read Unix TZ data [#742](https://github.com/python-pendulum/pendulum/pull/742) +- Fixed `Interval` deepcopying [#850](https://github.com/python-pendulum/pendulum/pull/850) +- Fixed typo in `end_of('century')` docs [#910](https://github.com/python-pendulum/pendulum/pull/910) +- Bumped PyO3 to 0.27 [#922](https://github.com/python-pendulum/pendulum/pull/922) +- Fixed incorrect date offset calculation in Rust extensions [#918](https://github.com/python-pendulum/pendulum/pull/918) +- Changed locales and `pytest` to be lazy loaded [#926](https://github.com/python-pendulum/pendulum/pull/926) +- Fixed error of `Duration` deepcopy not including weeks [#933](https://github.com/python-pendulum/pendulum/pull/933) +- Fixed empty `Duration`s not being an error in Python ISO8601 parser implementation [#903](https://github.com/python-pendulum/pendulum/pull/903) +- Fixed parsing invalid interval string [#860](https://github.com/python-pendulum/pendulum/pull/860) +- Fixed pluralization bug in `Duration.in_words()` [#826](https://github.com/python-pendulum/pendulum/pull/826) + +### Locales +- Added HI (Hindi) locale [#902](https://github.com/python-pendulum/pendulum/pull/902) + +### Removed +- Removed dependency on `pytz` [#911](https://github.com/python-pendulum/pendulum/pull/911) + +## [3.1.0] - 2025-04-19 + +### Added +- Added support for Python 3.13 [#871](https://github.com/python-pendulum/pendulum/pull/871) + +### Changed +- Removed support for Python 3.8 [#863](https://github.com/python-pendulum/pendulum/pull/863) +- Fixed pure Python wheels support [#889](https://github.com/python-pendulum/pendulum/pull/889) +- Fixed `pendulum.tz.timezones()` to use system tzdata [#801](https://github.com/python-pendulum/pendulum/pull/801) +- Fixed spelling of Kyiv [#885](https://github.com/python-pendulum/pendulum/pull/885) +- Fixed `DeprecationWarning` from `utcfromtimestamp` [#887](https://github.com/python-pendulum/pendulum/pull/887) +- Fixed parsing of invalid intervals [#843](https://github.com/python-pendulum/pendulum/pull/843) + +### Locales +- Added UA (Ukraine) locale [#793](https://github.com/python-pendulum/pendulum/pull/793) +- Added BG (Bulgarian) locale [#812](https://github.com/python-pendulum/pendulum/pull/812) +- Fixed KO (Korean) translations for `before` and `after` [#858](https://github.com/python-pendulum/pendulum/pull/858) + + +## [3.0.0] - 2023-12-16 + +### Changed + +- Relaxed dependency constraints. [#760](https://github.com/python-pendulum/pendulum/pull/760) +- The testing helpers are now optional and must be opted-in via the `test` extra. [#778](https://github.com/python-pendulum/pendulum/pull/778) + +### Fixed + +- Removed remaining mentions of periods instead of intervals. [#757](https://github.com/python-pendulum/pendulum/pull/757) +- Fixed the behavior of the `week_of_month` property for edge cases in January and December. [#774](https://github.com/python-pendulum/pendulum/pull/774) +- Fixed the handling of the `fold` attribute when deep-copying a `DateTime` instance. [#776](https://github.com/python-pendulum/pendulum/pull/776) +- Fixed errors where hours and days were not handled properly when adding durations. [#775](https://github.com/python-pendulum/pendulum/pull/775) +- Fixed errors where hours and days were not handled properly when adding durations. [#775](https://github.com/python-pendulum/pendulum/pull/775) + + +## [3.0.0b1] - 2023-10-01 + +### Added + +- Made `instance()` support all native types (date, time, datetime). [#732](https://github.com/python-pendulum/pendulum/pull/732) + +### Changed + +- Dropped support for Python 3.7. [#734](https://github.com/python-pendulum/pendulum/pull/734) +- Rewrote extensions in Rust. [#721](https://github.com/python-pendulum/pendulum/pull/721) +- Made day of week convention more consistent across the codebase. [#731](https://github.com/python-pendulum/pendulum/pull/731) + +### Fixed + +- Fixed datetime string representation to match the native library. [#733](https://github.com/python-pendulum/pendulum/pull/733) +- Fixed issues on some system when retrieving the local timezone. [#733](https://github.com/python-pendulum/pendulum/pull/733) +- Fixed DST handling in `start_of()/end_of()` methods. [#713](https://github.com/python-pendulum/pendulum/pull/713) + + +## [3.0.0a1] - 2022-11-23 + +### Added + +- Added new testing helpers to time travel. [#626](https://github.com/python-pendulum/pendulum/pull/626) + +### Changed + +- Dropped support for Python 2.7, 3.5 and 3.6. [#569](https://github.com/python-pendulum/pendulum/pull/569) +- The `Timezone` class now relies on the native `zoneinfo.ZoneInfo` class. [#569](https://github.com/python-pendulum/pendulum/pull/569) +- Renamed the `Period` class to `Interval`. [#676](https://github.com/python-pendulum/pendulum/pull/676) +- Renamed the `period` helper to `interval`. [#676](https://github.com/python-pendulum/pendulum/pull/676) +- Removed existing testing helpers: `test()` and `set_test_now()`. [#626](https://github.com/python-pendulum/pendulum/pull/626) + +### Locales + +- Added the `sk` locale. [#575](https://github.com/python-pendulum/pendulum/pull/575) +- Added the `ja` locale. [#610](https://github.com/python-pendulum/pendulum/pull/610) +- Added the `he` locale. [#585](https://github.com/python-pendulum/pendulum/pull/585) +- Added the `sv` locale. [#562](https://github.com/python-pendulum/pendulum/pull/562) + + ## [2.1.1] - 2020-07-13 ### Fixed -- Fixed errors where invalid timezones were matched in `from_format()` ([#374](https://github.com/sdispater/pendulum/pull/374)). -- Fixed errors when subtracting negative timedeltas ([#419](https://github.com/sdispater/pendulum/pull/419)). -- Fixed errors in total units computation for durations with years and months ([#482](https://github.com/sdispater/pendulum/pull/482)). -- Fixed an error where the `fold` attribute was overridden when using `replace()` ([#414](https://github.com/sdispater/pendulum/pull/414)). -- Fixed an error where `now()` was not returning the correct result on DST transitions ([#483](https://github.com/sdispater/pendulum/pull/483)). -- Fixed inconsistent typing annotation for the `parse()` function ([#452](https://github.com/sdispater/pendulum/pull/452)). +- Fixed errors where invalid timezones were matched in `from_format()` ([#374](https://github.com/python-pendulum/pendulum/pull/374)). +- Fixed errors when subtracting negative timedeltas ([#419](https://github.com/python-pendulum/pendulum/pull/419)). +- Fixed errors in total units computation for durations with years and months ([#482](https://github.com/python-pendulum/pendulum/pull/482)). +- Fixed an error where the `fold` attribute was overridden when using `replace()` ([#414](https://github.com/python-pendulum/pendulum/pull/414)). +- Fixed an error where `now()` was not returning the correct result on DST transitions ([#483](https://github.com/python-pendulum/pendulum/pull/483)). +- Fixed inconsistent typing annotation for the `parse()` function ([#452](https://github.com/python-pendulum/pendulum/pull/452)). ### Locales -- Added the `pl` locale ([#459](https://github.com/sdispater/pendulum/pull/459)). +- Added the `pl` locale ([#459](https://github.com/python-pendulum/pendulum/pull/459)). ## [2.1.0] - 2020-03-07 ### Added -- Added better typing and PEP-561 compliance ([#320](https://github.com/sdispater/pendulum/pull/320)). -- Added the `is_anniversary()` method as an alias of `is_birthday()` ([#298](https://github.com/sdispater/pendulum/pull/298)). +- Added better typing and PEP-561 compliance ([#320](https://github.com/python-pendulum/pendulum/pull/320)). +- Added the `is_anniversary()` method as an alias of `is_birthday()` ([#298](https://github.com/python-pendulum/pendulum/pull/298)). ### Changed - Dropped support for Python 3.4. -- `is_utc()` will now return `True` for any datetime with an offset of 0, similar to the behavior in the `1.*` versions ([#295](https://github.com/sdispater/pendulum/pull/295)) +- `is_utc()` will now return `True` for any datetime with an offset of 0, similar to the behavior in the `1.*` versions ([#295](https://github.com/python-pendulum/pendulum/pull/295)) - `Duration.in_words()` will now return `0 milliseconds` for empty durations. ### Fixed -- Fixed various issues with timezone transitions for some edge cases ([#321](https://github.com/sdispater/pendulum/pull/321), ([#350](https://github.com/sdispater/pendulum/pull/350))). -- Fixed out of bound detection for `nth_of("month")` ([#357](https://github.com/sdispater/pendulum/pull/357)). -- Fixed an error where extra text was accepted in `from_format()` ([#372](https://github.com/sdispater/pendulum/pull/372)). -- Fixed a recursion error when adding time to a `DateTime` with a fixed timezone ([#431](https://github.com/sdispater/pendulum/pull/431)). -- Fixed errors where `Period` instances were not properly compared to other classes, especially `timedelta` instances ([#427](https://github.com/sdispater/pendulum/pull/427)). -- Fixed deprecation warnings due to internal regexps ([#427](https://github.com/sdispater/pendulum/pull/427)). -- Fixed an error where the `test()` helper would not unset the test instance when an exception was raised ([#445](https://github.com/sdispater/pendulum/pull/445)). -- Fixed an error where the `week_of_month` attribute was not returning the correct value ([#446](https://github.com/sdispater/pendulum/pull/446)). -- Fixed an error in the way the `Z` ISO-8601 UTC designator was not parsed as UTC ([#448](https://github.com/sdispater/pendulum/pull/448)). +- Fixed various issues with timezone transitions for some edge cases ([#321](https://github.com/python-pendulum/pendulum/pull/321), ([#350](https://github.com/python-pendulum/pendulum/pull/350))). +- Fixed out of bound detection for `nth_of("month")` ([#357](https://github.com/python-pendulum/pendulum/pull/357)). +- Fixed an error where extra text was accepted in `from_format()` ([#372](https://github.com/python-pendulum/pendulum/pull/372)). +- Fixed a recursion error when adding time to a `DateTime` with a fixed timezone ([#431](https://github.com/python-pendulum/pendulum/pull/431)). +- Fixed errors where `Period` instances were not properly compared to other classes, especially `timedelta` instances ([#427](https://github.com/python-pendulum/pendulum/pull/427)). +- Fixed deprecation warnings due to internal regexps ([#427](https://github.com/python-pendulum/pendulum/pull/427)). +- Fixed an error where the `test()` helper would not unset the test instance when an exception was raised ([#445](https://github.com/python-pendulum/pendulum/pull/445)). +- Fixed an error where the `week_of_month` attribute was not returning the correct value ([#446](https://github.com/python-pendulum/pendulum/pull/446)). +- Fixed an error in the way the `Z` ISO-8601 UTC designator was not parsed as UTC ([#448](https://github.com/python-pendulum/pendulum/pull/448)). ### Locales @@ -143,12 +247,18 @@ -[Unreleased]: https://github.com/sdispater/pendulum/compare/2.1.1...master -[2.1.1]: https://github.com/sdispater/pendulum/releases/tag/2.1.1 -[2.1.0]: https://github.com/sdispater/pendulum/releases/tag/2.1.0 -[2.0.5]: https://github.com/sdispater/pendulum/releases/tag/2.0.5 -[2.0.4]: https://github.com/sdispater/pendulum/releases/tag/2.0.4 -[2.0.3]: https://github.com/sdispater/pendulum/releases/tag/2.0.3 -[2.0.2]: https://github.com/sdispater/pendulum/releases/tag/2.0.2 -[2.0.1]: https://github.com/sdispater/pendulum/releases/tag/2.0.1 -[2.0.0]: https://github.com/sdispater/pendulum/releases/tag/2.0.0 +[Unreleased]: https://github.com/python-pendulum/pendulum/compare/3.1.0...master +[3.2.0]: https://github.com/python-pendulum/pendulum/releases/tag/3.2.0 +[3.1.0]: https://github.com/python-pendulum/pendulum/releases/tag/3.1.0 +[3.1.0]: https://github.com/python-pendulum/pendulum/releases/tag/3.1.0 +[3.0.0]: https://github.com/python-pendulum/pendulum/releases/tag/3.0.0 +[3.0.0b1]: https://github.com/python-pendulum/pendulum/releases/tag/3.0.0b1 +[3.0.0a1]: https://github.com/python-pendulum/pendulum/releases/tag/3.0.0a1 +[2.1.1]: https://github.com/python-pendulum/pendulum/releases/tag/2.1.1 +[2.1.0]: https://github.com/python-pendulum/pendulum/releases/tag/2.1.0 +[2.0.5]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.5 +[2.0.4]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.4 +[2.0.3]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.3 +[2.0.2]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.2 +[2.0.1]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.1 +[2.0.0]: https://github.com/python-pendulum/pendulum/releases/tag/2.0.0 diff --git a/Makefile b/Makefile index e68b94c1..575ab920 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,3 @@ -# This file is part of orator -# https://github.com/sdispater/orator - -# Licensed under the MIT license: -# http://www.opensource.org/licenses/MIT-license -# Copyright (c) 2015 Sébastien Eustace - -PENDULUM_RELEASE := $$(sed -n -E "s/VERSION = \"(.+)\"/\1/p" pendulum/version.py) # lists all available targets list: @@ -16,38 +8,26 @@ list: # required for list no_targets__: -# install all dependencies -setup: setup-python - -# test your application (tests in the tests/ directory) -test: - @py.test --cov=pendulum --cov-config .coveragerc tests/ -sq - -linux_release: wheels_x64 wheels_i686 +lint-rust: + cd rust && cargo fmt --all -- --check + cd rust && cargo clippy --tests -- -D warnings -release: wheels_x64 wheels_i686 wheel -publish: - @poetry publish --no-build +format-rust: + cd rust && cargo fmt --all + cd rust && cargo clippy --tests --fix --allow-dirty -- -D warnings -tar: - python setup.py sdist --formats=gztar +dev: + poetry install --only main --only test --only typing --only build --only lint + poetry run maturin develop -wheel: - @poetry build -v +lint: + poetry run mypy + poetry run pre-commit run --all-files -wheels_x64: build_wheels_x64 - -wheels_i686: build_wheels_i686 - -build_wheels_x64: - docker pull quay.io/pypa/manylinux1_x86_64 - docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/build-wheels.sh - -build_wheels_i686: - docker pull quay.io/pypa/manylinux1_i686 - docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_i686 /io/build-wheels.sh +test: + PENDULUM_EXTENSIONS=0 poetry run pytest -q tests + poetry run pytest -q tests -# run tests against all supported python versions -tox: - @tox +clean: + rm src/pendulum/*.so diff --git a/README.rst b/README.rst index 313d4c92..e2ae92f2 100644 --- a/README.rst +++ b/README.rst @@ -7,16 +7,13 @@ Pendulum .. image:: https://img.shields.io/pypi/l/pendulum.svg :target: https://pypi.python.org/pypi/pendulum -.. image:: https://img.shields.io/codecov/c/github/sdispater/pendulum/master.svg - :target: https://codecov.io/gh/sdispater/pendulum/branch/master - -.. image:: https://travis-ci.org/sdispater/pendulum.svg +.. image:: https://github.com/sdispater/pendulum/actions/workflows/tests.yml/badge.svg :alt: Pendulum Build status - :target: https://travis-ci.org/sdispater/pendulum + :target: https://github.com/sdispater/pendulum/actions Python datetimes made easy. -Supports Python **2.7** and **3.4+**. +Supports Python **3.10 and newer**. .. code-block:: python @@ -36,7 +33,7 @@ Supports Python **2.7** and **3.4+**. >>> past = pendulum.now().subtract(minutes=2) >>> past.diff_for_humans() - >>> '2 minutes ago' + '2 minutes ago' >>> delta = past - last_week >>> delta.hours @@ -55,6 +52,13 @@ Supports Python **2.7** and **3.4+**. '2013-03-31T03:00:00+02:00' +Resources +========= + +* `Official Website `_ +* `Documentation `_ +* `Issue Tracker `_ + Why Pendulum? ============= @@ -65,7 +69,7 @@ So it's still ``datetime`` but better. Unlike other datetime libraries for Python, Pendulum is a drop-in replacement for the standard ``datetime`` class (it inherits from it), so, basically, you can replace all your ``datetime`` -instances by ``DateTime`` instances in you code (exceptions exist for libraries that check +instances by ``DateTime`` instances in your code (exceptions exist for libraries that check the type of the objects by using the ``type`` function like ``sqlite3`` or ``PyMySQL`` for instance). It also removes the notion of naive datetimes: each ``Pendulum`` instance is timezone-aware @@ -73,55 +77,6 @@ and by default in ``UTC`` for ease of use. Pendulum also improves the standard ``timedelta`` class by providing more intuitive methods and properties. - -Why not Arrow? -============== - -Arrow is the most popular datetime library for Python right now, however its behavior -and API can be erratic and unpredictable. The ``get()`` method can receive pretty much anything -and it will try its best to return something while silently failing to handle some cases: - -.. code-block:: python - - arrow.get('2016-1-17') - # - - pendulum.parse('2016-1-17') - # - - arrow.get('20160413') - # - - pendulum.parse('20160413') - # - - arrow.get('2016-W07-5') - # - - pendulum.parse('2016-W07-5') - # - - # Working with DST - just_before = arrow.Arrow(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris') - just_after = just_before.replace(microseconds=1) - '2013-03-31T02:00:00+02:00' - # Should be 2013-03-31T03:00:00+02:00 - - (just_after.to('utc') - just_before.to('utc')).total_seconds() - -3599.999999 - # Should be 1e-06 - - just_before = pendulum.datetime(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris') - just_after = just_before.add(microseconds=1) - '2013-03-31T03:00:00+02:00' - - (just_after.in_timezone('utc') - just_before.in_timezone('utc')).total_seconds() - 1e-06 - -Those are a few examples showing that Arrow cannot always be trusted to have a consistent -behavior with the data you are passing to it. - - Limitations =========== @@ -169,14 +124,6 @@ a possible solution, if any: return '' if val is None else val.isoformat() -Resources -========= - -* `Official Website `_ -* `Documentation `_ -* `Issue Tracker `_ - - Contributing ============ @@ -186,7 +133,7 @@ Getting started --------------- To work on the Pendulum codebase, you'll want to clone the project locally -and install the required depedendencies via `poetry `_. +and install the required dependencies via `poetry `_. .. code-block:: bash @@ -218,7 +165,7 @@ The ``locale.py`` file must not be modified. It contains the translations provid the CLDR database. The ``custom.py`` file is the one you want to modify. It contains the data needed -by Pendulum that are not provided by the CLDR database. You can take the `en `_ +by Pendulum that are not provided by the CLDR database. You can take the `en `_ data as a reference to see which data is needed. You should also add tests for the created or modified locale. diff --git a/build-wheels.sh b/build-wheels.sh deleted file mode 100755 index af63d1b1..00000000 --- a/build-wheels.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -e -x - -cd $(dirname $0) - -export PATH=/opt/python/cp38-cp38/bin/:$PATH - -curl -fsS -o get-poetry.py https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py -/opt/python/cp38-cp38/bin/python get-poetry.py --preview -y -rm get-poetry.py - -for PYBIN in /opt/python/cp3*/bin; do - if [ "$PYBIN" == "/opt/python/cp34-cp34m/bin" ]; then - continue - fi - rm -rf build - "${PYBIN}/python" $HOME/.poetry/bin/poetry build -vvv -done - -cd dist -for whl in *.whl; do - auditwheel repair "$whl" - rm "$whl" -done diff --git a/build.py b/build.py deleted file mode 100644 index 885b1c3b..00000000 --- a/build.py +++ /dev/null @@ -1,87 +0,0 @@ -import os -import shutil -import sys - -from distutils.command.build_ext import build_ext -from distutils.core import Distribution -from distutils.core import Extension -from distutils.errors import CCompilerError -from distutils.errors import DistutilsExecError -from distutils.errors import DistutilsPlatformError - - -# C Extensions -with_extensions = os.getenv("PENDULUM_EXTENSIONS", None) - -if with_extensions == "1" or with_extensions is None: - with_extensions = True - -if with_extensions == "0" or hasattr(sys, "pypy_version_info"): - with_extensions = False - -extensions = [] -if with_extensions: - extensions = [ - Extension("pendulum._extensions._helpers", ["pendulum/_extensions/_helpers.c"]), - Extension("pendulum.parsing._iso8601", ["pendulum/parsing/_iso8601.c"]), - ] - - -class BuildFailed(Exception): - - pass - - -class ExtBuilder(build_ext): - # This class allows C extension building to fail. - - built_extensions = [] - - def run(self): - try: - build_ext.run(self) - except (DistutilsPlatformError, FileNotFoundError): - print( - " Unable to build the C extensions, " - "Pendulum will use the pure python code instead." - ) - - def build_extension(self, ext): - try: - build_ext.build_extension(self, ext) - except (CCompilerError, DistutilsExecError, DistutilsPlatformError, ValueError): - print( - ' Unable to build the "{}" C extension, ' - "Pendulum will use the pure python version of the extension.".format( - ext.name - ) - ) - - -def build(setup_kwargs): - """ - This function is mandatory in order to build the extensions. - """ - distribution = Distribution({"name": "pendulum", "ext_modules": extensions}) - distribution.package_dir = "pendulum" - - cmd = ExtBuilder(distribution) - cmd.ensure_finalized() - cmd.run() - - # Copy built extensions back to the project - for output in cmd.get_outputs(): - relative_extension = os.path.relpath(output, cmd.build_lib) - if not os.path.exists(output): - continue - - shutil.copyfile(output, relative_extension) - mode = os.stat(relative_extension).st_mode - mode |= (mode & 0o444) >> 2 - os.chmod(relative_extension, mode) - - return setup_kwargs - - -if __name__ == "__main__": - build({}) diff --git a/clock b/clock index 96e226d2..5e026907 100755 --- a/clock +++ b/clock @@ -1,5 +1,6 @@ #!/usr/bin/env python -from __future__ import unicode_literals + +from __future__ import annotations import glob import json @@ -16,9 +17,9 @@ from babel.plural import _binary_compiler from babel.plural import _GettextCompiler from babel.plural import _unary_compiler from babel.plural import compile_zero -from cleo import Application -from cleo import Command -from cleo import argument +from cleo.application import Application +from cleo.commands.command import Command +from cleo.helpers import argument from pendulum import __version__ @@ -41,21 +42,17 @@ class _LambdaCompiler(_GettextCompiler): code = code.replace("||", "or") if method == "in": expr = self.compile(expr) - code = "(%s == %s and %s)" % (expr, expr, code) + code = f"({expr} == {expr} and {code})" return code class LocaleCreate(Command): - - name = "create" + name = "locale create" description = "Creates locale translations." arguments = [argument("locales", "Locales to dump.", optional=False, multiple=True)] - TEMPLATE = """# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from .custom import translations as custom_translations + TEMPLATE = """from .custom import translations as custom_translations \"\"\" @@ -73,11 +70,7 @@ locale = {{ }} """ - CUSTOM_TEMPLATE = """# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - -\"\"\" + CUSTOM_TEMPLATE = """\"\"\" {locale} custom locale file. \"\"\" @@ -99,10 +92,10 @@ translations = {{}} normalized = normalize_locale(locale.replace("-", "_")) if not normalized: - self.line("Locale [{}] does not exist.".format(locale)) + self.line(f"Locale [{locale}] does not exist.") continue - self.line("Generating {} locale.".format(locale)) + self.line(f"Generating {locale} locale.") content = LocaleDataDict(load(normalized)) @@ -119,8 +112,8 @@ translations = {{}} data["days"] = {} for fmt, names in days.items(): data["days"][fmt] = {} - for value, name in names.items(): - data["days"][fmt][(value + 1) % 7] = name + for value, name in sorted(names.items()): + data["days"][fmt][value] = name # Getting months names months = content["months"]["format"] @@ -140,7 +133,7 @@ translations = {{}} ] data["units"] = {} for unit in units: - pattern = patterns["duration-{}".format(unit)]["long"] + pattern = patterns[f"duration-{unit}"]["long"] if "per" in pattern: del pattern["per"] @@ -165,6 +158,9 @@ translations = {{}} # Day periods data["day_periods"] = content["day_periods"]["format"]["wide"] + # Week data + data["week_data"] = content["week_data"] + result = self.TEMPLATE.format( locale=locale, plural=plural, @@ -194,13 +190,14 @@ translations = {{}} def format_dict(self, d, tab=1): s = ["{\n"] for k, v in d.items(): - if isinstance(v, (dict, LocaleDataDict)): - v = self.format_dict(v, tab + 1) - else: - v = repr(v) + v = ( + self.format_dict(v, tab + 1) + if isinstance(v, (dict, LocaleDataDict)) + else repr(v) + ) - s.append("%s%r: %s,\n" % (" " * tab, k, v)) - s.append("%s}" % (" " * (tab - 1))) + s.append(f"{' ' * tab}{k!r}: {v},\n") + s.append(f"{' ' * (tab - 1)}}}") return "".join(s) @@ -208,7 +205,7 @@ translations = {{}} to_py = _LambdaCompiler().compile result = ["lambda n: "] for tag, ast in PluralRule.parse(rule).abstract: - result.append("'%s' if %s else " % (tag, to_py(ast))) + result.append(f"'{tag}' if {to_py(ast)} else ") result.append("'other'") return "".join(result) @@ -223,20 +220,19 @@ translations = {{}} limit = PATTERN_CHARS[fieldchar] if limit and fieldnum not in limit: raise ValueError( - "Invalid length for field: %r" % (fieldchar * fieldnum) + f"Invalid length for field: {(fieldchar * fieldnum)!r}" ) result.append( self.TOKENS_MAP.get(fieldchar * fieldnum, fieldchar * fieldnum) ) else: - raise NotImplementedError("Unknown token type: %s" % tok_type) + raise NotImplementedError(f"Unknown token type: {tok_type}") return "".join(result) class LocaleRecreate(Command): - - name = "recreate" + name = "locale recreate" description = "Recreate existing locales." def handle(self): @@ -244,58 +240,35 @@ class LocaleRecreate(Command): locales_dir = os.path.join("pendulum", "locales") locales = glob.glob(os.path.join(locales_dir, "*", "locale.py")) - locales = [os.path.basename(os.path.dirname(l)) for l in locales] - - self.call("locale:create", [("locales", locales)]) - + locales = [os.path.basename(os.path.dirname(locale)) for locale in locales] -class LocaleCommand(Command): - - name = "locale" - description = "Locale related commands." - - commands = [LocaleCreate()] - - def handle(self): - self.call("help", self._config.name) + self.call("locale create", "locales " + " ".join(locales)) class WindowsTzDump(Command): - - name = "dump-timezones" + name = "windows dump-timezones" description = "Dumps the mapping of Windows timezones to IANA timezones." MAPPING_DIR = os.path.join("pendulum", "tz", "data") def handle(self): raw_tznames = get_global("windows_zone_mapping") - sorted_names = sorted(list(raw_tznames.keys())) + sorted_names = sorted(raw_tznames.keys()) tznames = {} for name in sorted_names: tznames[name] = raw_tznames[name] - mapping = json.dumps(tznames, indent=4) - mapping = "windows_timezones = " + mapping.replace('"', "'") + "\n" + mapping = json.dumps(tznames, indent=4).replace('"', "'") with open(os.path.join(self.MAPPING_DIR, "windows.py"), "w") as f: - f.write(mapping) - - -class WindowsCommand(Command): - - name = "windows" - description = "Windows related commands." - - commands = [WindowsTzDump()] - - def handle(self): - self.call("help", self._config.name) + f.write(f"windows_timezones = {mapping}\n") app = Application("clock", __version__) -app.add(LocaleCommand()) -app.add(WindowsCommand()) +app.add(LocaleCreate()) +app.add(LocaleRecreate()) +app.add(WindowsTzDump()) if __name__ == "__main__": diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 3cbba287..00000000 --- a/codecov.yml +++ /dev/null @@ -1,7 +0,0 @@ -comment: false - -coverage: - status: - patch: - default: - enabled: false diff --git a/docs/docs/addition_subtraction.md b/docs/docs/addition_subtraction.md index 686f67f0..32cb875d 100644 --- a/docs/docs/addition_subtraction.md +++ b/docs/docs/addition_subtraction.md @@ -64,7 +64,7 @@ Each method returns a new `DateTime` instance. >>> dt = dt.subtract(minutes=1) '2012-01-28 01:01:00' >>> dt = dt.subtract(minutes=24) -'2012-01-28 00:00:00' +'2012-01-28 00:37:00' >>> dt = dt.add(seconds=61) '2012-01-28 00:01:01' diff --git a/docs/docs/comparison.md b/docs/docs/comparison.md index bc3fb50e..6be8d0da 100644 --- a/docs/docs/comparison.md +++ b/docs/docs/comparison.md @@ -64,7 +64,7 @@ the `now()` is created in the same timezone as the instance. >>> born = pendulum.datetime(1987, 4, 23) >>> not_birthday = pendulum.datetime(2014, 9, 26) ->>> birthday = pendulum.datetime(2014, 2, 23) +>>> birthday = pendulum.datetime(2014, 4, 23) >>> past_birthday = pendulum.now().subtract(years=50) >>> born.is_birthday(not_birthday) diff --git a/docs/docs/difference.md b/docs/docs/difference.md index 3a7f0634..2653f01f 100644 --- a/docs/docs/difference.md +++ b/docs/docs/difference.md @@ -1,6 +1,6 @@ # Difference -The `diff()` method returns a [Period](#period) instance that represents the total duration +The `diff()` method returns an [Interval](#interval) instance that represents the total duration between two `DateTime` instances. This interval can be then expressed in various units. These interval methods always return *the total difference expressed* in the specified time requested. All values are truncated and not rounded. diff --git a/docs/docs/duration.md b/docs/docs/duration.md index 0801d9ea..a657d9ae 100644 --- a/docs/docs/duration.md +++ b/docs/docs/duration.md @@ -11,10 +11,10 @@ It has many improvements over the base class. ```python >>> import pendulum - >>> from datetime import datetime + >>> import datetime - >>> d1 = datetime(2012, 1, 1, 1, 2, 3, tzinfo=pytz.UTC) - >>> d2 = datetime(2011, 12, 31, 22, 2, 3, tzinfo=pytz.UTC) + >>> d1 = datetime.datetime(2012, 1, 1, 1, 2, 3, tzinfo=datetime.UTC) + >>> d2 = datetime.datetime(2011, 12, 31, 22, 2, 3, tzinfo=datetime.UTC) >>> delta = d2 - d1 >>> delta.days -1 diff --git a/docs/docs/index.md b/docs/docs/index.md index daca205e..107e043c 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -12,6 +12,6 @@ {!docs/modifiers.md!} {!docs/timezones.md!} {!docs/duration.md!} -{!docs/period.md!} +{!docs/interval.md!} {!docs/testing.md!} {!docs/limitations.md!} diff --git a/docs/docs/installation.md b/docs/docs/installation.md index bfa9641f..9f80e874 100644 --- a/docs/docs/installation.md +++ b/docs/docs/installation.md @@ -11,3 +11,23 @@ or, if you are using [poetry](https://python-poetry.org): ```bash $ poetry add pendulum ``` + +## Optional features + +Pendulum provides optional features that you must explicitly require in order to use them. + +These optional features are: + +- `test`: Provides a set of helpers to make testing easier by allowing you to control the flow of time. + +You can install them by specifying them when installing Pendulum + +```bash +$ pip install pendulum[test] +``` + +or, if you are using [poetry](https://python-poetry.org): + +```bash +$ poetry add pendulum[test] +``` diff --git a/docs/docs/period.md b/docs/docs/interval.md similarity index 60% rename from docs/docs/period.md rename to docs/docs/interval.md index 052966e3..fc70fb3c 100644 --- a/docs/docs/period.md +++ b/docs/docs/interval.md @@ -1,6 +1,6 @@ -# Period +# Interval -When you subtract a `DateTime` instance from another, or use the `diff()` method, it will return a `Period` instance. +When you subtract a `DateTime` instance from another, or use the `diff()` method, it will return an `Interval` instance. It inherits from the [Duration](#duration) class with the added benefit that it is aware of the instances that generated it, so that it can give access to more methods and properties: @@ -10,29 +10,29 @@ instances that generated it, so that it can give access to more methods and prop >>> start = pendulum.datetime(2000, 11, 20) >>> end = pendulum.datetime(2016, 11, 5) ->>> period = end - start +>>> interval = end - start ->>> period.years +>>> interval.years 15 ->>> period.months +>>> interval.months 11 ->>> period.in_years() +>>> interval.in_years() 15 ->>> period.in_months() +>>> interval.in_months() 191 # Note that the weeks property # will change compared to the Duration class ->>> period.weeks +>>> interval.weeks 2 # 832 for the duration # However the days property will still remain the same -# to keep the compatiblity with the timedelta class ->>> period.days +# to keep the compatibility with the timedelta class +>>> interval.days 5829 ``` -Be aware that a period, just like an interval, is compatible with the `timedelta` class regarding +Be aware that an interval, just like an duration, is compatible with the `timedelta` class regarding its attributes. However, its custom attributes (like `remaining_days`) will be aware of any DST transitions that might have occurred and adjust accordingly. Let's take an example: @@ -42,42 +42,42 @@ transitions that might have occurred and adjust accordingly. Let's take an examp >>> start = pendulum.datetime(2017, 3, 7, tz='America/Toronto') >>> end = start.add(days=6) ->>> period = end - start +>>> interval = end - start # timedelta properties ->>> period.days +>>> interval.days 5 ->>> period.seconds +>>> interval.seconds 82800 -# period properties ->>> period.remaining_days +# interval properties +>>> interval.remaining_days 6 ->>> period.hours +>>> interval.hours 0 ->>> period.remaining_seconds +>>> interval.remaining_seconds 0 ``` !!!warning Due to their nature (fixed duration between two datetimes), most arithmetic operations will - return a `Duration` instead of a `Period`. + return a `Duration` instead of an `Interval`. ```python >>> import pendulum >>> dt1 = pendulum.datetime(2016, 8, 7, 12, 34, 56) >>> dt2 = dt1.add(days=6, seconds=34) - >>> period = pendulum.period(dt1, dt2) - >>> period * 2 + >>> interval = pendulum.interval(dt1, dt2) + >>> interval * 2 Duration(weeks=1, days=5, minutes=1, seconds=8) ``` ## Instantiation -You can create an instance by using the `period()` helper: +You can create an instance by using the `interval()` helper: ```python @@ -86,29 +86,29 @@ You can create an instance by using the `period()` helper: >>> start = pendulum.datetime(2000, 1, 1) >>> end = pendulum.datetime(2000, 1, 31) ->>> period = pendulum.period(start, end) +>>> interval = pendulum.interval(start, end) ``` -You can also make an inverted period: +You can also make an inverted interval: ```python ->>> period = pendulum.period(end, start) ->>> period.remaining_days +>>> interval = pendulum.interval(end, start) +>>> interval.remaining_days -2 ``` -If you have inverted dates but want to make sure that the period is positive, +If you have inverted dates but want to make sure that the interval is positive, you should set the `absolute` keyword argument to `True`: ```python ->>> period = pendulum.period(end, start, absolute=True) ->>> period.remaining_days +>>> interval = pendulum.interval(end, start, absolute=True) +>>> interval.remaining_days 2 ``` ## Range -If you want to iterate over a period, you can use the `range()` method: +If you want to iterate over a interval, you can use the `range()` method: ```python >>> import pendulum @@ -116,9 +116,9 @@ If you want to iterate over a period, you can use the `range()` method: >>> start = pendulum.datetime(2000, 1, 1) >>> end = pendulum.datetime(2000, 1, 10) ->>> period = pendulum.period(start, end) +>>> interval = pendulum.interval(start, end) ->>> for dt in period.range('days'): +>>> for dt in interval.range('days'): >>> print(dt) '2000-01-01T00:00:00+00:00' @@ -136,12 +136,12 @@ If you want to iterate over a period, you can use the `range()` method: !!!note Supported units for `range()` are: `years`, `months`, `weeks`, - `days`, `hours`, `minutes` and `seconds` + `days`, `hours`, `minutes`, `seconds` and `microseconds` You can pass an amount for the passed unit to control the length of the gap: ```python ->>> for dt in period.range('days', 2): +>>> for dt in interval.range('days', 2): >>> print(dt) '2000-01-01T00:00:00+00:00' @@ -151,18 +151,18 @@ You can pass an amount for the passed unit to control the length of the gap: '2000-01-09T00:00:00+00:00' ``` -You can also directly iterate over the `Period` instance, +You can also directly iterate over the `Interval` instance, the unit will be `days` in this case: ```python ->>> for dt in period: +>>> for dt in interval: >>> print(dt) ``` -You can check if a `DateTime` instance is inside a period using the `in` keyword: +You can check if a `DateTime` instance is inside a interval using the `in` keyword: ```python >>> dt = pendulum.datetime(2000, 1, 4) ->>> dt in period +>>> dt in interval True ``` diff --git a/docs/docs/introduction.md b/docs/docs/introduction.md index fbbab976..0078b488 100644 --- a/docs/docs/introduction.md +++ b/docs/docs/introduction.md @@ -18,4 +18,4 @@ For example, all comparisons are done in `UTC` or in the timezone of the datetim 3 ``` -The default timezone, except when using the `now()`, method will always be `UTC`. +The default timezone, except when using the `now()` method, will always be `UTC`. diff --git a/docs/docs/limitations.md b/docs/docs/limitations.md index 7deff230..913aca1c 100644 --- a/docs/docs/limitations.md +++ b/docs/docs/limitations.md @@ -4,7 +4,7 @@ Even though the `DateTime` class is a subclass of `datetime`, there are some rare cases where it can't replace the native class directly. Here is a list (non-exhaustive) of the reported cases with a possible solution, if any: -* `sqlite3` will use the the `type()` function to determine the type of the object by default. To work around it you can register a new adapter: +* `sqlite3` will use the `type()` function to determine the type of the object by default. To work around it you can register a new adapter: ```python import pendulum @@ -13,7 +13,7 @@ Here is a list (non-exhaustive) of the reported cases with a possible solution, register_adapter(pendulum.DateTime, lambda val: val.isoformat(' ')) ``` -* `mysqlclient` (former `MySQLdb`) and `PyMySQL` will use the the `type()` function to determine the type of the object by default. To work around it you can register a new adapter: +* `mysqlclient` (former `MySQLdb`) and `PyMySQL` will use the `type()` function to determine the type of the object by default. To work around it you can register a new adapter: ```python import pendulum diff --git a/docs/docs/modifiers.md b/docs/docs/modifiers.md index b440587a..64a3a994 100644 --- a/docs/docs/modifiers.md +++ b/docs/docs/modifiers.md @@ -37,10 +37,10 @@ It returns the middle date between itself and the provided `DateTime` argument. '2019-12-31 23:59:59' >>> dt.start_of('century') -'2000-01-01 00:00:00' +'2001-01-01 00:00:00' >>> dt.end_of('century') -'2099-12-31 23:59:59' +'2100-12-31 23:59:59' >>> dt.start_of('week') '2012-01-30 00:00:00' @@ -81,6 +81,6 @@ True '2014-01-15 12:00:00' # others that are defined that are similar -# and tha accept month, quarter and year units +# and that accept month, quarter and year units # first_of(), last_of(), nth_of() ``` diff --git a/docs/docs/string_formatting.md b/docs/docs/string_formatting.md index ec6e0213..91b95fc4 100644 --- a/docs/docs/string_formatting.md +++ b/docs/docs/string_formatting.md @@ -126,8 +126,8 @@ The following tokens are currently supported: | | dd | Mo, Tu, We ... | | | d | 0, 1, 2 ... 6 | | **Days of ISO Week** | E | 1, 2, 3 ... 7 | -| **Hour** | HH | 00, 01, 02 ... 23, 24 | -| | H | 0, 1, 2 ... 23, 24 | +| **Hour** | HH | 00, 01, 02 ... 23 | +| | H | 0, 1, 2 ... 23 | | | hh | 01, 02, 03 ... 11, 12 | | | h | 1, 2, 3 ... 11, 12 | | **Minute** | mm | 00, 01, 02 ... 58, 59 | diff --git a/docs/docs/testing.md b/docs/docs/testing.md index dfca0547..25aad8d6 100644 --- a/docs/docs/testing.md +++ b/docs/docs/testing.md @@ -1,59 +1,87 @@ # Testing -The testing methods allow you to set a `DateTime` instance (real or mock) to be returned -when a "now" instance is created. -The provided instance will be returned specifically under the following conditions: +Pendulum provides a few helpers to help you control the flow of time in your tests. Note that +these helpers are only available if you opted in the `test` extra during [installation](#installation). -* A call to the `now()` method, ex. `pendulum.now()`. -* When the string "now" is passed to the `parse()` method, ex. `pendulum.parse('now')` +!!!warning + If you are migrating from Pendulum 2, note that the `set_test_now()` and `test()` + helpers have been removed. + + +## Relative time travel + +You can travel in time relatively to the current time ```python >>> import pendulum -# Create testing datetime ->>> known = pendulum.datetime(2001, 5, 21, 12) +>>> now = pendulum.now() +>>> pendulum.travel(minutes=5) +>>> pendulum.now().diff_for_humans(now) +"5 minutes after" +``` -# Set the mock ->>> pendulum.set_test_now(known) +Note that once you've travelled in time the clock **keeps ticking**. If you prefer to stop the time completely +you can use the `freeze` parameter: ->>> print(pendulum.now()) -'2001-05-21T12:00:00+00:00' +```python +>>> import pendulum ->>> print(pendulum.parse('now')) -'2001-05-21T12:00:00+00:00' +>>> now = pendulum.now() +>>> pendulum.travel(minutes=5, freeze=True) +>>> pendulum.now().diff_for_humans(now) +"5 minutes after" # This will stay like this indefinitely +``` -# Clear the mock ->>> pendulum.set_test_now() ->>> print(pendulum.now()) -'2016-07-10T22:10:33.954851-05:00' -``` +## Absolute time travel -Related methods will also return values mocked according to the **now** instance. +Sometimes, you may want to place yourself at a specific point in time. +This is possible by using the `travel_to()` helper. This helper accepts a `DateTime` instance +that represents the point in time where you want to travel to. ```python ->>> print(pendulum.today()) -'2001-05-21T00:00:00+00:00' +>>> import pendulum ->>> print(pendulum.tomorrow()) -'2001-05-22T00:00:00+00:00' +>>> pendulum.travel_to(pendulum.yesterday()) +``` ->>> print(pendulum.yesterday()) -'2001-05-20T00:00:00+00:00' +Similarly to `travel`, it's important to remember that, by default, the time keeps ticking so, if you prefer +stopping the time, use the `freeze` parameter: + +```python +>>> import pendulum + +>>> pendulum.travel_to(pendulum.yesterday(), freeze=True) ``` -If you don't want to manually clear the mock (or you are afraid of forgetting), -you can use the provided `test()` contextmanager. +## Travelling back to the present + +Using any of the travel helpers will keep you in the past, or future, until you decide +to travel back to the present time. To do so, you may use the `travel_back()` helper. ```python >>> import pendulum ->>> known = pendulum.datetime(2001, 5, 21, 12) +>>> now = pendulum.now() +>>> pendulum.travel(minutes=5, freeze=True) +>>> pendulum.now().diff_for_humans(now) +"5 minutes after" +>>> pendulum.travel_back() +>>> pendulum.now().diff_for_humans(now) +"a few seconds after" +``` + +However, it might be cumbersome to remember to travel back so, instead, you can use any of the helpers as a context +manager: ->>> with pendulum.test(known): ->>> print(pendulum.now()) -'2001-05-21T12:00:00+00:00' +```python +>>> import pendulum ->>> print(pendulum.now()) -'2016-07-10T22:10:33.954851-05:00' +>>> now = pendulum.now() +>>> with pendulum.travel(minutes=5, freeze=True): +>>> pendulum.now().diff_for_humans(now) +"5 minutes after" +>>> pendulum.now().diff_for_humans(now) +"a few seconds after" ``` diff --git a/docs/docs/timezones.md b/docs/docs/timezones.md index 85ff1477..e70034e7 100644 --- a/docs/docs/timezones.md +++ b/docs/docs/timezones.md @@ -67,7 +67,7 @@ adopt the proper behavior and apply the transition accordingly. >>> dt = dt.add(microseconds=1) '2013-03-31T03:00:00+02:00' >>> dt.subtract(microseconds=1) -'2013-03-31T01:59:59.999998+01:00' +'2013-03-31T01:59:59.999999+01:00' >>> dt = pendulum.datetime(2013, 10, 27, 2, 59, 59, 999999, tz='Europe/Paris', diff --git a/pendulum/__init__.py b/pendulum/__init__.py deleted file mode 100644 index bb1e0ca7..00000000 --- a/pendulum/__init__.py +++ /dev/null @@ -1,315 +0,0 @@ -from __future__ import absolute_import - -import datetime as _datetime - -from typing import Optional -from typing import Union - -from .__version__ import __version__ -from .constants import DAYS_PER_WEEK -from .constants import FRIDAY -from .constants import HOURS_PER_DAY -from .constants import MINUTES_PER_HOUR -from .constants import MONDAY -from .constants import MONTHS_PER_YEAR -from .constants import SATURDAY -from .constants import SECONDS_PER_DAY -from .constants import SECONDS_PER_HOUR -from .constants import SECONDS_PER_MINUTE -from .constants import SUNDAY -from .constants import THURSDAY -from .constants import TUESDAY -from .constants import WEDNESDAY -from .constants import WEEKS_PER_YEAR -from .constants import YEARS_PER_CENTURY -from .constants import YEARS_PER_DECADE -from .date import Date -from .datetime import DateTime -from .duration import Duration -from .formatting import Formatter -from .helpers import format_diff -from .helpers import get_locale -from .helpers import get_test_now -from .helpers import has_test_now -from .helpers import locale -from .helpers import set_locale -from .helpers import set_test_now -from .helpers import test -from .helpers import week_ends_at -from .helpers import week_starts_at -from .parser import parse -from .period import Period -from .time import Time -from .tz import POST_TRANSITION -from .tz import PRE_TRANSITION -from .tz import TRANSITION_ERROR -from .tz import UTC -from .tz import local_timezone -from .tz import set_local_timezone -from .tz import test_local_timezone -from .tz import timezone -from .tz import timezones -from .tz.timezone import Timezone as _Timezone -from .utils._compat import _HAS_FOLD - - -_TEST_NOW = None # type: Optional[DateTime] -_LOCALE = "en" -_WEEK_STARTS_AT = MONDAY -_WEEK_ENDS_AT = SUNDAY - -_formatter = Formatter() - - -def _safe_timezone(obj): - # type: (Optional[Union[str, float, _datetime.tzinfo, _Timezone]]) -> _Timezone - """ - Creates a timezone instance - from a string, Timezone, TimezoneInfo or integer offset. - """ - if isinstance(obj, _Timezone): - return obj - - if obj is None or obj == "local": - return local_timezone() - - if isinstance(obj, (int, float)): - obj = int(obj * 60 * 60) - elif isinstance(obj, _datetime.tzinfo): - # pytz - if hasattr(obj, "localize"): - obj = obj.zone - elif obj.tzname(None) == "UTC": - return UTC - else: - offset = obj.utcoffset(None) - - if offset is None: - offset = _datetime.timedelta(0) - - obj = int(offset.total_seconds()) - - return timezone(obj) - - -# Public API -def datetime( - year, # type: int - month, # type: int - day, # type: int - hour=0, # type: int - minute=0, # type: int - second=0, # type: int - microsecond=0, # type: int - tz=UTC, # type: Optional[Union[str, float, _Timezone]] - dst_rule=POST_TRANSITION, # type: str -): # type: (...) -> DateTime - """ - Creates a new DateTime instance from a specific date and time. - """ - if tz is not None: - tz = _safe_timezone(tz) - - if not _HAS_FOLD: - dt = naive(year, month, day, hour, minute, second, microsecond) - else: - dt = _datetime.datetime(year, month, day, hour, minute, second, microsecond) - if tz is not None: - dt = tz.convert(dt, dst_rule=dst_rule) - - return DateTime( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - tzinfo=dt.tzinfo, - fold=dt.fold, - ) - - -def local( - year, month, day, hour=0, minute=0, second=0, microsecond=0 -): # type: (int, int, int, int, int, int, int) -> DateTime - """ - Return a DateTime in the local timezone. - """ - return datetime( - year, month, day, hour, minute, second, microsecond, tz=local_timezone() - ) - - -def naive( - year, month, day, hour=0, minute=0, second=0, microsecond=0 -): # type: (int, int, int, int, int, int, int) -> DateTime - """ - Return a naive DateTime. - """ - return DateTime(year, month, day, hour, minute, second, microsecond) - - -def date(year, month, day): # type: (int, int, int) -> Date - """ - Create a new Date instance. - """ - return Date(year, month, day) - - -def time(hour, minute=0, second=0, microsecond=0): # type: (int, int, int, int) -> Time - """ - Create a new Time instance. - """ - return Time(hour, minute, second, microsecond) - - -def instance( - dt, tz=UTC -): # type: (_datetime.datetime, Optional[Union[str, _Timezone]]) -> DateTime - """ - Create a DateTime instance from a datetime one. - """ - if not isinstance(dt, _datetime.datetime): - raise ValueError("instance() only accepts datetime objects.") - - if isinstance(dt, DateTime): - return dt - - tz = dt.tzinfo or tz - - # Checking for pytz/tzinfo - if isinstance(tz, _datetime.tzinfo) and not isinstance(tz, _Timezone): - # pytz - if hasattr(tz, "localize") and tz.zone: - tz = tz.zone - else: - # We have no sure way to figure out - # the timezone name, we fallback - # on a fixed offset - tz = tz.utcoffset(dt).total_seconds() / 3600 - - return datetime( - dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, tz=tz - ) - - -def now(tz=None): # type: (Optional[Union[str, _Timezone]]) -> DateTime - """ - Get a DateTime instance for the current date and time. - """ - if has_test_now(): - test_instance = get_test_now() - _tz = _safe_timezone(tz) - - if tz is not None and _tz != test_instance.timezone: - test_instance = test_instance.in_tz(_tz) - - return test_instance - - if tz is None or tz == "local": - dt = _datetime.datetime.now(local_timezone()) - elif tz is UTC or tz == "UTC": - dt = _datetime.datetime.now(UTC) - else: - dt = _datetime.datetime.now(UTC) - tz = _safe_timezone(tz) - dt = tz.convert(dt) - - return DateTime( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - tzinfo=dt.tzinfo, - fold=dt.fold if _HAS_FOLD else 0, - ) - - -def today(tz="local"): # type: (Union[str, _Timezone]) -> DateTime - """ - Create a DateTime instance for today. - """ - return now(tz).start_of("day") - - -def tomorrow(tz="local"): # type: (Union[str, _Timezone]) -> DateTime - """ - Create a DateTime instance for today. - """ - return today(tz).add(days=1) - - -def yesterday(tz="local"): # type: (Union[str, _Timezone]) -> DateTime - """ - Create a DateTime instance for today. - """ - return today(tz).subtract(days=1) - - -def from_format( - string, fmt, tz=UTC, locale=None, # noqa -): # type: (str, str, Union[str, _Timezone], Optional[str]) -> DateTime - """ - Creates a DateTime instance from a specific format. - """ - parts = _formatter.parse(string, fmt, now(), locale=locale) - if parts["tz"] is None: - parts["tz"] = tz - - return datetime(**parts) - - -def from_timestamp( - timestamp, tz=UTC -): # type: (Union[int, float], Union[str, _Timezone]) -> DateTime - """ - Create a DateTime instance from a timestamp. - """ - dt = _datetime.datetime.utcfromtimestamp(timestamp) - - dt = datetime( - dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond - ) - - if tz is not UTC or tz != "UTC": - dt = dt.in_timezone(tz) - - return dt - - -def duration( - days=0, # type: float - seconds=0, # type: float - microseconds=0, # type: float - milliseconds=0, # type: float - minutes=0, # type: float - hours=0, # type: float - weeks=0, # type: float - years=0, # type: float - months=0, # type: float -): # type: (...) -> Duration - """ - Create a Duration instance. - """ - return Duration( - days=days, - seconds=seconds, - microseconds=microseconds, - milliseconds=milliseconds, - minutes=minutes, - hours=hours, - weeks=weeks, - years=years, - months=months, - ) - - -def period(start, end, absolute=False): # type: (DateTime, DateTime, bool) -> Period - """ - Create a Period instance. - """ - return Period(start, end, absolute=absolute) diff --git a/pendulum/__version__.py b/pendulum/__version__.py deleted file mode 100644 index 58039f50..00000000 --- a/pendulum/__version__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "2.1.1" diff --git a/pendulum/_extensions/_helpers.c b/pendulum/_extensions/_helpers.c deleted file mode 100644 index b9310f25..00000000 --- a/pendulum/_extensions/_helpers.c +++ /dev/null @@ -1,930 +0,0 @@ -/* ------------------------------------------------------------------------- */ - -#include -#include -#include -#include -#include -#include -#include -#include - -/* ------------------------------------------------------------------------- */ - -#define EPOCH_YEAR 1970 - -#define DAYS_PER_N_YEAR 365 -#define DAYS_PER_L_YEAR 366 - -#define USECS_PER_SEC 1000000 - -#define SECS_PER_MIN 60 -#define SECS_PER_HOUR (60 * SECS_PER_MIN) -#define SECS_PER_DAY (SECS_PER_HOUR * 24) - -// 400-year chunks always have 146097 days (20871 weeks). -#define DAYS_PER_400_YEARS 146097L -#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY) - -// The number of seconds in an aligned 100-year chunk, for those that -// do not begin with a leap year and those that do respectively. -const int64_t SECS_PER_100_YEARS[2] = { - (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY}; - -// The number of seconds in an aligned 4-year chunk, for those that -// do not begin with a leap year and those that do respectively. -const int32_t SECS_PER_4_YEARS[2] = { - (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY}; - -// The number of seconds in non-leap and leap years respectively. -const int32_t SECS_PER_YEAR[2] = { - DAYS_PER_N_YEAR * SECS_PER_DAY, - DAYS_PER_L_YEAR *SECS_PER_DAY}; - -#define MONTHS_PER_YEAR 12 - -// The month lengths in non-leap and leap years respectively. -const int32_t DAYS_PER_MONTHS[2][13] = { - {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, - {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; - -// The day offsets of the beginning of each (1-based) month in non-leap -// and leap years respectively. -// For example, in a leap year there are 335 days before December. -const int32_t MONTHS_OFFSETS[2][14] = { - {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}, - {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}}; - -const int DAY_OF_WEEK_TABLE[12] = { - 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; - -#define TM_SUNDAY 0 -#define TM_MONDAY 1 -#define TM_TUESDAY 2 -#define TM_WEDNESDAY 3 -#define TM_THURSDAY 4 -#define TM_FRIDAY 5 -#define TM_SATURDAY 6 - -#define TM_JANUARY 0 -#define TM_FEBRUARY 1 -#define TM_MARCH 2 -#define TM_APRIL 3 -#define TM_MAY 4 -#define TM_JUNE 5 -#define TM_JULY 6 -#define TM_AUGUST 7 -#define TM_SEPTEMBER 8 -#define TM_OCTOBER 9 -#define TM_NOVEMBER 10 -#define TM_DECEMBER 11 - -/* ------------------------------------------------------------------------- */ - -int _p(int y) -{ - return y + y / 4 - y / 100 + y / 400; -} - -int _is_leap(int year) -{ - return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); -} - -int _is_long_year(int year) -{ - return (_p(year) % 7 == 4) || (_p(year - 1) % 7 == 3); -} - -int _week_day(int year, int month, int day) -{ - int y; - int w; - - y = year - (month < 3); - - w = (_p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7; - - if (!w) - { - w = 7; - } - - return w; -} - -int _days_in_year(int year) -{ - if (_is_leap(year)) - { - return DAYS_PER_L_YEAR; - } - - return DAYS_PER_N_YEAR; -} - -int _day_number(int year, int month, int day) -{ - month = (month + 9) % 12; - year = year - month / 10; - - return ( - 365 * year + year / 4 - year / 100 + year / 400 + (month * 306 + 5) / 10 + (day - 1)); -} - -int _get_offset(PyObject *dt) -{ - PyObject *tzinfo; - PyObject *offset; - - tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo; - - if (tzinfo != Py_None) - { - offset = PyObject_CallMethod(tzinfo, "utcoffset", "O", dt); - - return PyDateTime_DELTA_GET_DAYS(offset) * SECS_PER_DAY + PyDateTime_DELTA_GET_SECONDS(offset); - } - - return 0; -} - -int _has_tzinfo(PyObject *dt) -{ - return ((_PyDateTime_BaseTZInfo *)(dt))->hastzinfo; -} - -char *_get_tz_name(PyObject *dt) -{ - PyObject *tzinfo; - char *tz = ""; - - tzinfo = ((PyDateTime_DateTime *)(dt))->tzinfo; - - if (tzinfo != Py_None) - { - if (PyObject_HasAttrString(tzinfo, "name")) - { - // Pendulum timezone - tz = (char *)PyUnicode_AsUTF8( - PyObject_GetAttrString(tzinfo, "name")); - } - else if (PyObject_HasAttrString(tzinfo, "zone")) - { - // pytz timezone - tz = (char *)PyUnicode_AsUTF8( - PyObject_GetAttrString(tzinfo, "zone")); - } - } - - return tz; -} - -/* ------------------------ Custom Types ------------------------------- */ - -/* - * class Diff(): - */ -typedef struct -{ - PyObject_HEAD int years; - int months; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - int total_days; -} Diff; - -/* - * def __init__(self, years, months, days, hours, minutes, seconds, microseconds, total_days): - * self.years = years - * # ... -*/ -static int Diff_init(Diff *self, PyObject *args, PyObject *kwargs) -{ - int years; - int months; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - int total_days; - - if (!PyArg_ParseTuple(args, "iiiiiii", &years, &months, &days, &hours, &minutes, &seconds, µseconds, &total_days)) - return -1; - - self->years = years; - self->months = months; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - self->total_days = total_days; - - return 0; -} - -/* - * def __repr__(self): - * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format( - * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds - * ) - */ -static PyObject *Diff_repr(Diff *self) -{ - char repr[82] = {0}; - - sprintf( - repr, - "%d years %d months %d days %d hours %d minutes %d seconds %d microseconds", - self->years, - self->months, - self->days, - self->hours, - self->minutes, - self->seconds, - self->microseconds); - - return PyUnicode_FromString(repr); -} - -/* - * Instantiate new Diff_type object - * Skip overhead of calling PyObject_New and PyObject_Init. - * Directly allocate object. - */ -static PyObject *new_diff_ex(int years, int months, int days, int hours, int minutes, int seconds, int microseconds, int total_days, PyTypeObject *type) -{ - Diff *self = (Diff *)(type->tp_alloc(type, 0)); - - if (self != NULL) - { - self->years = years; - self->months = months; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - self->total_days = total_days; - } - - return (PyObject *)self; -} - -/* - * Class member / class attributes - */ -static PyMemberDef Diff_members[] = { - {"years", T_INT, offsetof(Diff, years), 0, "years in diff"}, - {"months", T_INT, offsetof(Diff, months), 0, "months in diff"}, - {"days", T_INT, offsetof(Diff, days), 0, "days in diff"}, - {"hours", T_INT, offsetof(Diff, hours), 0, "hours in diff"}, - {"minutes", T_INT, offsetof(Diff, minutes), 0, "minutes in diff"}, - {"seconds", T_INT, offsetof(Diff, seconds), 0, "seconds in diff"}, - {"microseconds", T_INT, offsetof(Diff, microseconds), 0, "microseconds in diff"}, - {"total_days", T_INT, offsetof(Diff, total_days), 0, "total days in diff"}, - {NULL}}; - -static PyTypeObject Diff_type = { - PyVarObject_HEAD_INIT(NULL, 0) "PreciseDiff", /* tp_name */ - sizeof(Diff), /* tp_basicsize */ - 0, /* tp_itemsize */ - 0, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_as_async */ - (reprfunc)Diff_repr, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - (reprfunc)Diff_repr, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ - "Precise difference between two datetime objects", /* tp_doc */ -}; - -#define new_diff(years, months, days, hours, minutes, seconds, microseconds, total_days) new_diff_ex(years, months, days, hours, minutes, seconds, microseconds, total_days, &Diff_type) - -/* -------------------------- Functions --------------------------*/ - -PyObject *is_leap(PyObject *self, PyObject *args) -{ - PyObject *leap; - int year; - - if (!PyArg_ParseTuple(args, "i", &year)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - leap = PyBool_FromLong(_is_leap(year)); - - return leap; -} - -PyObject *is_long_year(PyObject *self, PyObject *args) -{ - PyObject *is_long; - int year; - - if (!PyArg_ParseTuple(args, "i", &year)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - is_long = PyBool_FromLong(_is_long_year(year)); - - return is_long; -} - -PyObject *week_day(PyObject *self, PyObject *args) -{ - PyObject *wd; - int year; - int month; - int day; - - if (!PyArg_ParseTuple(args, "iii", &year, &month, &day)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - wd = PyLong_FromLong(_week_day(year, month, day)); - - return wd; -} - -PyObject *days_in_year(PyObject *self, PyObject *args) -{ - PyObject *ndays; - int year; - - if (!PyArg_ParseTuple(args, "i", &year)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - ndays = PyLong_FromLong(_days_in_year(year)); - - return ndays; -} - -PyObject *timestamp(PyObject *self, PyObject *args) -{ - int64_t result; - PyObject *dt; - - if (!PyArg_ParseTuple(args, "O", &dt)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - int year = (double)PyDateTime_GET_YEAR(dt); - int month = PyDateTime_GET_MONTH(dt); - int day = PyDateTime_GET_DAY(dt); - int hour = PyDateTime_DATE_GET_HOUR(dt); - int minute = PyDateTime_DATE_GET_MINUTE(dt); - int second = PyDateTime_DATE_GET_SECOND(dt); - - result = (year - 1970) * 365 + MONTHS_OFFSETS[0][month]; - result += (int)floor((double)(year - 1968) / 4); - result -= (year - 1900) / 100; - result += (year - 1600) / 400; - - if (_is_leap(year) && month < 3) - { - result -= 1; - } - - result += day - 1; - result *= 24; - result += hour; - result *= 60; - result += minute; - result *= 60; - result += second; - - return PyLong_FromSsize_t(result); -} - -PyObject *local_time(PyObject *self, PyObject *args) -{ - double unix_time; - int32_t utc_offset; - int32_t year; - int32_t microsecond; - int64_t seconds; - int32_t leap_year; - int64_t sec_per_100years; - int64_t sec_per_4years; - int32_t sec_per_year; - int32_t month; - int32_t day; - int32_t month_offset; - int32_t hour; - int32_t minute; - int32_t second; - - if (!PyArg_ParseTuple(args, "dii", &unix_time, &utc_offset, µsecond)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - year = EPOCH_YEAR; - seconds = (int64_t)floor(unix_time); - - // Shift to a base year that is 400-year aligned. - if (seconds >= 0) - { - seconds -= 10957L * SECS_PER_DAY; - year += 30; // == 2000; - } - else - { - seconds += (int64_t)(146097L - 10957L) * SECS_PER_DAY; - year -= 370; // == 1600; - } - - seconds += utc_offset; - - // Handle years in chunks of 400/100/4/1 - year += 400 * (seconds / SECS_PER_400_YEARS); - seconds %= SECS_PER_400_YEARS; - if (seconds < 0) - { - seconds += SECS_PER_400_YEARS; - year -= 400; - } - - leap_year = 1; // 4-century aligned - - sec_per_100years = SECS_PER_100_YEARS[leap_year]; - - while (seconds >= sec_per_100years) - { - seconds -= sec_per_100years; - year += 100; - leap_year = 0; // 1-century, non 4-century aligned - sec_per_100years = SECS_PER_100_YEARS[leap_year]; - } - - sec_per_4years = SECS_PER_4_YEARS[leap_year]; - while (seconds >= sec_per_4years) - { - seconds -= sec_per_4years; - year += 4; - leap_year = 1; // 4-year, non century aligned - sec_per_4years = SECS_PER_4_YEARS[leap_year]; - } - - sec_per_year = SECS_PER_YEAR[leap_year]; - while (seconds >= sec_per_year) - { - seconds -= sec_per_year; - year += 1; - leap_year = 0; // non 4-year aligned - sec_per_year = SECS_PER_YEAR[leap_year]; - } - - // Handle months and days - month = TM_DECEMBER + 1; - day = seconds / SECS_PER_DAY + 1; - seconds %= SECS_PER_DAY; - while (month != TM_JANUARY + 1) - { - month_offset = MONTHS_OFFSETS[leap_year][month]; - if (day > month_offset) - { - day -= month_offset; - break; - } - - month -= 1; - } - - // Handle hours, minutes and seconds - hour = seconds / SECS_PER_HOUR; - seconds %= SECS_PER_HOUR; - minute = seconds / SECS_PER_MIN; - second = seconds % SECS_PER_MIN; - - return Py_BuildValue("NNNNNNN", - PyLong_FromLong(year), - PyLong_FromLong(month), - PyLong_FromLong(day), - PyLong_FromLong(hour), - PyLong_FromLong(minute), - PyLong_FromLong(second), - PyLong_FromLong(microsecond)); -} - -// Calculate a precise difference between two datetimes. -PyObject *precise_diff(PyObject *self, PyObject *args) -{ - PyObject *dt1; - PyObject *dt2; - - if (!PyArg_ParseTuple(args, "OO", &dt1, &dt2)) - { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters"); - return NULL; - } - - int year_diff = 0; - int month_diff = 0; - int day_diff = 0; - int hour_diff = 0; - int minute_diff = 0; - int second_diff = 0; - int microsecond_diff = 0; - int sign = 1; - int year; - int month; - int leap; - int days_in_last_month; - int days_in_month; - int dt1_year = PyDateTime_GET_YEAR(dt1); - int dt2_year = PyDateTime_GET_YEAR(dt2); - int dt1_month = PyDateTime_GET_MONTH(dt1); - int dt2_month = PyDateTime_GET_MONTH(dt2); - int dt1_day = PyDateTime_GET_DAY(dt1); - int dt2_day = PyDateTime_GET_DAY(dt2); - int dt1_hour = 0; - int dt2_hour = 0; - int dt1_minute = 0; - int dt2_minute = 0; - int dt1_second = 0; - int dt2_second = 0; - int dt1_microsecond = 0; - int dt2_microsecond = 0; - int dt1_total_seconds = 0; - int dt2_total_seconds = 0; - int dt1_offset = 0; - int dt2_offset = 0; - int dt1_is_datetime = PyDateTime_Check(dt1); - int dt2_is_datetime = PyDateTime_Check(dt2); - char *tz1 = ""; - char *tz2 = ""; - int in_same_tz = 0; - int total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day)); - - // If both dates are datetimes, we check - // If we are in the same timezone - if (dt1_is_datetime && dt2_is_datetime) - { - if (_has_tzinfo(dt1)) - { - tz1 = _get_tz_name(dt1); - dt1_offset = _get_offset(dt1); - } - - if (_has_tzinfo(dt2)) - { - tz2 = _get_tz_name(dt2); - dt2_offset = _get_offset(dt2); - } - - in_same_tz = tz1 == tz2 && strncmp(tz1, "", 1); - } - - // If we have datetimes (and not only dates) - // we get the information we need - if (dt1_is_datetime) - { - dt1_hour = PyDateTime_DATE_GET_HOUR(dt1); - dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1); - dt1_second = PyDateTime_DATE_GET_SECOND(dt1); - dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1); - - if ((!in_same_tz && dt1_offset != 0) || total_days == 0) - { - dt1_hour -= dt1_offset / SECS_PER_HOUR; - dt1_offset %= SECS_PER_HOUR; - dt1_minute -= dt1_offset / SECS_PER_MIN; - dt1_offset %= SECS_PER_MIN; - dt1_second -= dt1_offset; - - if (dt1_second < 0) - { - dt1_second += 60; - dt1_minute -= 1; - } - else if (dt1_second > 60) - { - dt1_second -= 60; - dt1_minute += 1; - } - - if (dt1_minute < 0) - { - dt1_minute += 60; - dt1_hour -= 1; - } - else if (dt1_minute > 60) - { - dt1_minute -= 60; - dt1_hour += 1; - } - - if (dt1_hour < 0) - { - dt1_hour += 24; - dt1_day -= 1; - } - else if (dt1_hour > 24) - { - dt1_hour -= 24; - dt1_day += 1; - } - } - - dt1_total_seconds = (dt1_hour * SECS_PER_HOUR + dt1_minute * SECS_PER_MIN + dt1_second); - } - - if (dt2_is_datetime) - { - dt2_hour = PyDateTime_DATE_GET_HOUR(dt2); - dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2); - dt2_second = PyDateTime_DATE_GET_SECOND(dt2); - dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2); - - if ((!in_same_tz && dt2_offset != 0) || total_days == 0) - { - dt2_hour -= dt2_offset / SECS_PER_HOUR; - dt2_offset %= SECS_PER_HOUR; - dt2_minute -= dt2_offset / SECS_PER_MIN; - dt2_offset %= SECS_PER_MIN; - dt2_second -= dt2_offset; - - if (dt2_second < 0) - { - dt2_second += 60; - dt2_minute -= 1; - } - else if (dt2_second > 60) - { - dt2_second -= 60; - dt2_minute += 1; - } - - if (dt2_minute < 0) - { - dt2_minute += 60; - dt2_hour -= 1; - } - else if (dt2_minute > 60) - { - dt2_minute -= 60; - dt2_hour += 1; - } - - if (dt2_hour < 0) - { - dt2_hour += 24; - dt2_day -= 1; - } - else if (dt2_hour > 24) - { - dt2_hour -= 24; - dt2_day += 1; - } - } - - dt2_total_seconds = (dt2_hour * SECS_PER_HOUR + dt2_minute * SECS_PER_MIN + dt2_second); - } - - // Direct comparison between two datetimes does not work - // so we need to check by properties - int dt1_gt_dt2 = (dt1_year > dt2_year || (dt1_year == dt2_year && dt1_month > dt2_month) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day > dt2_day) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds > dt2_total_seconds) || (dt1_year == dt2_year && dt1_month == dt2_month && dt1_day == dt2_day && dt1_total_seconds == dt2_total_seconds && dt1_microsecond > dt2_microsecond)); - - if (dt1_gt_dt2) - { - PyObject *temp; - temp = dt1; - dt1 = dt2; - dt2 = temp; - sign = -1; - - // Retrieving properties - dt1_year = PyDateTime_GET_YEAR(dt1); - dt2_year = PyDateTime_GET_YEAR(dt2); - dt1_month = PyDateTime_GET_MONTH(dt1); - dt2_month = PyDateTime_GET_MONTH(dt2); - dt1_day = PyDateTime_GET_DAY(dt1); - dt2_day = PyDateTime_GET_DAY(dt2); - - if (dt2_is_datetime) - { - dt1_hour = PyDateTime_DATE_GET_HOUR(dt1); - dt1_minute = PyDateTime_DATE_GET_MINUTE(dt1); - dt1_second = PyDateTime_DATE_GET_SECOND(dt1); - dt1_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt1); - } - - if (dt1_is_datetime) - { - dt2_hour = PyDateTime_DATE_GET_HOUR(dt2); - dt2_minute = PyDateTime_DATE_GET_MINUTE(dt2); - dt2_second = PyDateTime_DATE_GET_SECOND(dt2); - dt2_microsecond = PyDateTime_DATE_GET_MICROSECOND(dt2); - } - - total_days = (_day_number(dt2_year, dt2_month, dt2_day) - _day_number(dt1_year, dt1_month, dt1_day)); - } - - year_diff = dt2_year - dt1_year; - month_diff = dt2_month - dt1_month; - day_diff = dt2_day - dt1_day; - hour_diff = dt2_hour - dt1_hour; - minute_diff = dt2_minute - dt1_minute; - second_diff = dt2_second - dt1_second; - microsecond_diff = dt2_microsecond - dt1_microsecond; - - if (microsecond_diff < 0) - { - microsecond_diff += 1e6; - second_diff -= 1; - } - - if (second_diff < 0) - { - second_diff += 60; - minute_diff -= 1; - } - - if (minute_diff < 0) - { - minute_diff += 60; - hour_diff -= 1; - } - - if (hour_diff < 0) - { - hour_diff += 24; - day_diff -= 1; - } - - if (day_diff < 0) - { - // If we have a difference in days, - // we have to check if they represent months - year = dt2_year; - month = dt2_month; - - if (month == 1) - { - month = 12; - year -= 1; - } - else - { - month -= 1; - } - - leap = _is_leap(year); - - days_in_last_month = DAYS_PER_MONTHS[leap][month]; - days_in_month = DAYS_PER_MONTHS[_is_leap(dt2_year)][dt2_month]; - - if (day_diff < days_in_month - days_in_last_month) - { - // We don't have a full month, we calculate days - if (days_in_last_month < dt1_day) - { - day_diff += dt1_day; - } - else - { - day_diff += days_in_last_month; - } - } - else if (day_diff == days_in_month - days_in_last_month) - { - // We have exactly a full month - // We remove the days difference - // and add one to the months difference - day_diff = 0; - month_diff += 1; - } - else - { - // We have a full month - day_diff += days_in_last_month; - } - - month_diff -= 1; - } - - if (month_diff < 0) - { - month_diff += 12; - year_diff -= 1; - } - - return new_diff( - year_diff * sign, - month_diff * sign, - day_diff * sign, - hour_diff * sign, - minute_diff * sign, - second_diff * sign, - microsecond_diff * sign, - total_days * sign); -} - -/* ------------------------------------------------------------------------- */ - -static PyMethodDef helpers_methods[] = { - {"is_leap", - (PyCFunction)is_leap, - METH_VARARGS, - PyDoc_STR("Checks if a year is a leap year.")}, - {"is_long_year", - (PyCFunction)is_long_year, - METH_VARARGS, - PyDoc_STR("Checks if a year is a long year.")}, - {"week_day", - (PyCFunction)week_day, - METH_VARARGS, - PyDoc_STR("Returns the weekday number.")}, - {"days_in_year", - (PyCFunction)days_in_year, - METH_VARARGS, - PyDoc_STR("Returns the number of days in the given year.")}, - {"timestamp", - (PyCFunction)timestamp, - METH_VARARGS, - PyDoc_STR("Returns the timestamp of the given datetime.")}, - {"local_time", - (PyCFunction)local_time, - METH_VARARGS, - PyDoc_STR("Returns a UNIX time as a broken down time for a particular transition type.")}, - {"precise_diff", - (PyCFunction)precise_diff, - METH_VARARGS, - PyDoc_STR("Calculate a precise difference between two datetimes.")}, - {NULL}}; - -/* ------------------------------------------------------------------------- */ - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_helpers", - NULL, - -1, - helpers_methods, - NULL, - NULL, - NULL, - NULL, -}; - -PyMODINIT_FUNC -PyInit__helpers(void) -{ - PyObject *module; - - PyDateTime_IMPORT; - - module = PyModule_Create(&moduledef); - - if (module == NULL) - return NULL; - - // Diff declaration - Diff_type.tp_new = PyType_GenericNew; - Diff_type.tp_members = Diff_members; - Diff_type.tp_init = (initproc)Diff_init; - - if (PyType_Ready(&Diff_type) < 0) - return NULL; - - PyModule_AddObject(module, "PreciseDiff", (PyObject *)&Diff_type); - - return module; -} diff --git a/pendulum/exceptions.py b/pendulum/exceptions.py deleted file mode 100644 index 4c6448a6..00000000 --- a/pendulum/exceptions.py +++ /dev/null @@ -1,6 +0,0 @@ -from .parsing.exceptions import ParserError # noqa - - -class PendulumException(Exception): - - pass diff --git a/pendulum/formatting/__init__.py b/pendulum/formatting/__init__.py deleted file mode 100644 index 856321af..00000000 --- a/pendulum/formatting/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .formatter import Formatter - - -__all__ = ["Formatter"] diff --git a/pendulum/helpers.py b/pendulum/helpers.py deleted file mode 100644 index 6e51a731..00000000 --- a/pendulum/helpers.py +++ /dev/null @@ -1,224 +0,0 @@ -from __future__ import absolute_import - -import os -import struct - -from contextlib import contextmanager -from datetime import date -from datetime import datetime -from datetime import timedelta -from math import copysign -from typing import TYPE_CHECKING -from typing import Iterator -from typing import Optional -from typing import TypeVar -from typing import overload - -import pendulum - -from .constants import DAYS_PER_MONTHS -from .formatting.difference_formatter import DifferenceFormatter -from .locales.locale import Locale - - -if TYPE_CHECKING: - # Prevent import cycles - from .period import Period - -with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" - -_DT = TypeVar("_DT", bound=datetime) -_D = TypeVar("_D", bound=date) - -try: - if not with_extensions or struct.calcsize("P") == 4: - raise ImportError() - - from ._extensions._helpers import local_time - from ._extensions._helpers import precise_diff - from ._extensions._helpers import is_leap - from ._extensions._helpers import is_long_year - from ._extensions._helpers import week_day - from ._extensions._helpers import days_in_year - from ._extensions._helpers import timestamp -except ImportError: - from ._extensions.helpers import local_time # noqa - from ._extensions.helpers import precise_diff # noqa - from ._extensions.helpers import is_leap # noqa - from ._extensions.helpers import is_long_year # noqa - from ._extensions.helpers import week_day # noqa - from ._extensions.helpers import days_in_year # noqa - from ._extensions.helpers import timestamp # noqa - - -difference_formatter = DifferenceFormatter() - - -@overload -def add_duration( - dt, # type: _DT - years=0, # type: int - months=0, # type: int - weeks=0, # type: int - days=0, # type: int - hours=0, # type: int - minutes=0, # type: int - seconds=0, # type: int - microseconds=0, # type: int -): # type: (...) -> _DT - pass - - -@overload -def add_duration( - dt, # type: _D - years=0, # type: int - months=0, # type: int - weeks=0, # type: int - days=0, # type: int -): # type: (...) -> _D - pass - - -def add_duration( - dt, - years=0, - months=0, - weeks=0, - days=0, - hours=0, - minutes=0, - seconds=0, - microseconds=0, -): - """ - Adds a duration to a date/datetime instance. - """ - days += weeks * 7 - - if ( - isinstance(dt, date) - and not isinstance(dt, datetime) - and any([hours, minutes, seconds, microseconds]) - ): - raise RuntimeError("Time elements cannot be added to a date instance.") - - # Normalizing - if abs(microseconds) > 999999: - s = _sign(microseconds) - div, mod = divmod(microseconds * s, 1000000) - microseconds = mod * s - seconds += div * s - - if abs(seconds) > 59: - s = _sign(seconds) - div, mod = divmod(seconds * s, 60) - seconds = mod * s - minutes += div * s - - if abs(minutes) > 59: - s = _sign(minutes) - div, mod = divmod(minutes * s, 60) - minutes = mod * s - hours += div * s - - if abs(hours) > 23: - s = _sign(hours) - div, mod = divmod(hours * s, 24) - hours = mod * s - days += div * s - - if abs(months) > 11: - s = _sign(months) - div, mod = divmod(months * s, 12) - months = mod * s - years += div * s - - year = dt.year + years - month = dt.month - - if months: - month += months - if month > 12: - year += 1 - month -= 12 - elif month < 1: - year -= 1 - month += 12 - - day = min(DAYS_PER_MONTHS[int(is_leap(year))][month], dt.day) - - dt = dt.replace(year=year, month=month, day=day) - - return dt + timedelta( - days=days, - hours=hours, - minutes=minutes, - seconds=seconds, - microseconds=microseconds, - ) - - -def format_diff( - diff, is_now=True, absolute=False, locale=None -): # type: (Period, bool, bool, Optional[str]) -> str - if locale is None: - locale = get_locale() - - return difference_formatter.format(diff, is_now, absolute, locale) - - -def _sign(x): - return int(copysign(1, x)) - - -# Global helpers - - -@contextmanager -def test(mock): # type: (pendulum.DateTime) -> Iterator[None] - set_test_now(mock) - try: - yield - finally: - set_test_now() - - -def set_test_now(test_now=None): # type: (Optional[pendulum.DateTime]) -> None - pendulum._TEST_NOW = test_now - - -def get_test_now(): # type: () -> Optional[pendulum.DateTime] - return pendulum._TEST_NOW - - -def has_test_now(): # type: () -> bool - return pendulum._TEST_NOW is not None - - -def locale(name): # type: (str) -> Locale - return Locale.load(name) - - -def set_locale(name): # type: (str) -> None - locale(name) - - pendulum._LOCALE = name - - -def get_locale(): # type: () -> str - return pendulum._LOCALE - - -def week_starts_at(wday): # type: (int) -> None - if wday < pendulum.SUNDAY or wday > pendulum.SATURDAY: - raise ValueError("Invalid week day as start of week.") - - pendulum._WEEK_STARTS_AT = wday - - -def week_ends_at(wday): # type: (int) -> None - if wday < pendulum.SUNDAY or wday > pendulum.SATURDAY: - raise ValueError("Invalid week day as start of week.") - - pendulum._WEEK_ENDS_AT = wday diff --git a/pendulum/locales/fr/__init__.py b/pendulum/locales/fr/__init__.py deleted file mode 100644 index 40a96afc..00000000 --- a/pendulum/locales/fr/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/pendulum/locales/locale.py b/pendulum/locales/locale.py deleted file mode 100644 index de4cd82e..00000000 --- a/pendulum/locales/locale.py +++ /dev/null @@ -1,104 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import os -import re - -from importlib import import_module -from typing import Any -from typing import Optional -from typing import Union - -from pendulum.utils._compat import basestring -from pendulum.utils._compat import decode - - -class Locale: - """ - Represent a specific locale. - """ - - _cache = {} - - def __init__(self, locale, data): # type: (str, Any) -> None - self._locale = locale - self._data = data - self._key_cache = {} - - @classmethod - def load(cls, locale): # type: (Union[str, Locale]) -> Locale - if isinstance(locale, Locale): - return locale - - locale = cls.normalize_locale(locale) - if locale in cls._cache: - return cls._cache[locale] - - # Checking locale existence - actual_locale = locale - locale_path = os.path.join(os.path.dirname(__file__), actual_locale) - while not os.path.exists(locale_path): - if actual_locale == locale: - raise ValueError("Locale [{}] does not exist.".format(locale)) - - actual_locale = actual_locale.split("_")[0] - - m = import_module("pendulum.locales.{}.locale".format(actual_locale)) - - cls._cache[locale] = cls(locale, m.locale) - - return cls._cache[locale] - - @classmethod - def normalize_locale(cls, locale): # type: (str) -> str - m = re.match("([a-z]{2})[-_]([a-z]{2})", locale, re.I) - if m: - return "{}_{}".format(m.group(1).lower(), m.group(2).lower()) - else: - return locale.lower() - - def get(self, key, default=None): # type: (str, Optional[Any]) -> Any - if key in self._key_cache: - return self._key_cache[key] - - parts = key.split(".") - try: - result = self._data[parts[0]] - for part in parts[1:]: - result = result[part] - except KeyError: - result = default - - if isinstance(result, basestring): - result = decode(result) - - self._key_cache[key] = result - - return self._key_cache[key] - - def translation(self, key): # type: (str) -> Any - return self.get("translations.{}".format(key)) - - def plural(self, number): # type: (int) -> str - return decode(self._data["plural"](number)) - - def ordinal(self, number): # type: (int) -> str - return decode(self._data["ordinal"](number)) - - def ordinalize(self, number): # type: (int) -> str - ordinal = self.get("custom.ordinal.{}".format(self.ordinal(number))) - - if not ordinal: - return decode("{}".format(number)) - - return decode("{}{}".format(number, ordinal)) - - def match_translation(self, key, value): - translations = self.translation(key) - if value not in translations.values(): - return None - - return {v: k for k, v in translations.items()}[value] - - def __repr__(self): - return "{}('{}')".format(self.__class__.__name__, self._locale) diff --git a/pendulum/mixins/__init__.py b/pendulum/mixins/__init__.py deleted file mode 100644 index 40a96afc..00000000 --- a/pendulum/mixins/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/pendulum/parsing/_iso8601.c b/pendulum/parsing/_iso8601.c deleted file mode 100644 index 41c66fae..00000000 --- a/pendulum/parsing/_iso8601.c +++ /dev/null @@ -1,1371 +0,0 @@ -/* ------------------------------------------------------------------------- */ - -#include -#include -#include -#include -#include -#include -#include - -#ifndef PyVarObject_HEAD_INIT -#define PyVarObject_HEAD_INIT(type, size) PyObject_HEAD_INIT(type) size, -#endif - - -/* ------------------------------------------------------------------------- */ - -#define EPOCH_YEAR 1970 - -#define DAYS_PER_N_YEAR 365 -#define DAYS_PER_L_YEAR 366 - -#define USECS_PER_SEC 1000000 - -#define SECS_PER_MIN 60 -#define SECS_PER_HOUR (60 * SECS_PER_MIN) -#define SECS_PER_DAY (SECS_PER_HOUR * 24) - -// 400-year chunks always have 146097 days (20871 weeks). -#define DAYS_PER_400_YEARS 146097L -#define SECS_PER_400_YEARS ((int64_t)DAYS_PER_400_YEARS * (int64_t)SECS_PER_DAY) - -// The number of seconds in an aligned 100-year chunk, for those that -// do not begin with a leap year and those that do respectively. -const int64_t SECS_PER_100_YEARS[2] = { - (uint64_t)(76L * DAYS_PER_N_YEAR + 24L * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (uint64_t)(75L * DAYS_PER_N_YEAR + 25L * DAYS_PER_L_YEAR) * SECS_PER_DAY -}; - -// The number of seconds in an aligned 4-year chunk, for those that -// do not begin with a leap year and those that do respectively. -const int32_t SECS_PER_4_YEARS[2] = { - (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, - (3 * DAYS_PER_N_YEAR + 1 * DAYS_PER_L_YEAR) * SECS_PER_DAY -}; - -// The number of seconds in non-leap and leap years respectively. -const int32_t SECS_PER_YEAR[2] = { - DAYS_PER_N_YEAR * SECS_PER_DAY, - DAYS_PER_L_YEAR * SECS_PER_DAY -}; - -#define MONTHS_PER_YEAR 12 - -// The month lengths in non-leap and leap years respectively. -const int32_t DAYS_PER_MONTHS[2][13] = { - {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, - {-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} -}; - -// The day offsets of the beginning of each (1-based) month in non-leap -// and leap years respectively. -// For example, in a leap year there are 335 days before December. -const int32_t MONTHS_OFFSETS[2][14] = { - {-1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}, - {-1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366} -}; - -const int DAY_OF_WEEK_TABLE[12] = { - 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 -}; - -#define TM_SUNDAY 0 -#define TM_MONDAY 1 -#define TM_TUESDAY 2 -#define TM_WEDNESDAY 3 -#define TM_THURSDAY 4 -#define TM_FRIDAY 5 -#define TM_SATURDAY 6 - -#define TM_JANUARY 0 -#define TM_FEBRUARY 1 -#define TM_MARCH 2 -#define TM_APRIL 3 -#define TM_MAY 4 -#define TM_JUNE 5 -#define TM_JULY 6 -#define TM_AUGUST 7 -#define TM_SEPTEMBER 8 -#define TM_OCTOBER 9 -#define TM_NOVEMBER 10 -#define TM_DECEMBER 11 - -// Parsing errors -const int PARSER_INVALID_ISO8601 = 0; -const int PARSER_INVALID_DATE = 1; -const int PARSER_INVALID_TIME = 2; -const int PARSER_INVALID_WEEK_DATE = 3; -const int PARSER_INVALID_WEEK_NUMBER = 4; -const int PARSER_INVALID_WEEKDAY_NUMBER = 5; -const int PARSER_INVALID_ORDINAL_DAY_FOR_YEAR = 6; -const int PARSER_INVALID_MONTH_OR_DAY = 7; -const int PARSER_INVALID_MONTH = 8; -const int PARSER_INVALID_DAY_FOR_MONTH = 9; -const int PARSER_INVALID_HOUR = 10; -const int PARSER_INVALID_MINUTE = 11; -const int PARSER_INVALID_SECOND = 12; -const int PARSER_INVALID_SUBSECOND = 13; -const int PARSER_INVALID_TZ_OFFSET = 14; -const int PARSER_INVALID_DURATION = 15; -const int PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED = 16; - -const char PARSER_ERRORS[17][80] = { - "Invalid ISO 8601 string", - "Invalid date", - "Invalid time", - "Invalid week date", - "Invalid week number", - "Invalid weekday number", - "Invalid ordinal day for year", - "Invalid month and/or day", - "Invalid month", - "Invalid day for month", - "Invalid hour", - "Invalid minute", - "Invalid second", - "Invalid subsecond", - "Invalid timezone offset", - "Invalid duration", - "Float years and months are not supported" -}; - -/* ------------------------------------------------------------------------- */ - - -int p(int y) { - return y + y/4 - y/100 + y/400; -} - -int is_leap(int year) { - return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); -} - -int week_day(int year, int month, int day) { - int y; - int w; - - y = year - (month < 3); - - w = (p(y) + DAY_OF_WEEK_TABLE[month - 1] + day) % 7; - - if (!w) { - w = 7; - } - - return w; -} - -int days_in_year(int year) { - if (is_leap(year)) { - return DAYS_PER_L_YEAR; - } - - return DAYS_PER_N_YEAR; -} - -int is_long_year(int year) { - return (p(year) % 7 == 4) || (p(year - 1) % 7 == 3); -} - - -/* ------------------------ Custom Types ------------------------------- */ - - -/* - * class FixedOffset(tzinfo): - */ -typedef struct { - PyObject_HEAD - int offset; - char *tzname; -} FixedOffset; - -/* - * def __init__(self, offset): - * self.offset = offset -*/ -static int FixedOffset_init(FixedOffset *self, PyObject *args, PyObject *kwargs) { - int offset; - char *tzname = NULL; - - static char *kwlist[] = {"offset", "tzname", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|s", kwlist, &offset, &tzname)) - return -1; - - self->offset = offset; - self->tzname = tzname; - - return 0; -} - -/* - * def utcoffset(self, dt): - * return timedelta(seconds=self.offset * 60) - */ -static PyObject *FixedOffset_utcoffset(FixedOffset *self, PyObject *args) { - return PyDelta_FromDSU(0, self->offset, 0); -} - -/* - * def dst(self, dt): - * return timedelta(seconds=self.offset * 60) - */ -static PyObject *FixedOffset_dst(FixedOffset *self, PyObject *args) { - return PyDelta_FromDSU(0, self->offset, 0); -} - -/* - * def tzname(self, dt): - * sign = '+' - * if self.offset < 0: - * sign = '-' - * return "%s%d:%d" % (sign, self.offset / 60, self.offset % 60) - */ -static PyObject *FixedOffset_tzname(FixedOffset *self, PyObject *args) { - if (self->tzname != NULL) { - return PyUnicode_FromString(self->tzname); - } - - char tzname_[7] = {0}; - char sign = '+'; - int offset = self->offset; - - if (offset < 0) { - sign = '-'; - offset *= -1; - } - - sprintf( - tzname_, - "%c%02d:%02d", - sign, - offset / SECS_PER_HOUR, - offset / SECS_PER_MIN % SECS_PER_MIN - ); - - return PyUnicode_FromString(tzname_); -} - -/* - * def __repr__(self): - * return self.tzname() - */ -static PyObject *FixedOffset_repr(FixedOffset *self) { - return FixedOffset_tzname(self, NULL); -} - -/* - * Class member / class attributes - */ -static PyMemberDef FixedOffset_members[] = { - {"offset", T_INT, offsetof(FixedOffset, offset), 0, "UTC offset"}, - {NULL} -}; - -/* - * Class methods - */ -static PyMethodDef FixedOffset_methods[] = { - {"utcoffset", (PyCFunction)FixedOffset_utcoffset, METH_VARARGS, ""}, - {"dst", (PyCFunction)FixedOffset_dst, METH_VARARGS, ""}, - {"tzname", (PyCFunction)FixedOffset_tzname, METH_VARARGS, ""}, - {NULL} -}; - -static PyTypeObject FixedOffset_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "FixedOffset_type", /* tp_name */ - sizeof(FixedOffset), /* tp_basicsize */ - 0, /* tp_itemsize */ - 0, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_as_async */ - (reprfunc)FixedOffset_repr, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - (reprfunc)FixedOffset_repr, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */ - "TZInfo with fixed offset", /* tp_doc */ -}; - -/* - * Instantiate new FixedOffset_type object - * Skip overhead of calling PyObject_New and PyObject_Init. - * Directly allocate object. - */ -static PyObject *new_fixed_offset_ex(int offset, char *name, PyTypeObject *type) { - FixedOffset *self = (FixedOffset *) (type->tp_alloc(type, 0)); - - if (self != NULL) - self->offset = offset; - self->tzname = name; - - return (PyObject *) self; -} - -#define new_fixed_offset(offset, name) new_fixed_offset_ex(offset, name, &FixedOffset_type) - - -/* - * class Duration(): - */ -typedef struct { - PyObject_HEAD - int years; - int months; - int weeks; - int days; - int hours; - int minutes; - int seconds; - int microseconds; -} Duration; - -/* - * def __init__(self, years, months, days, hours, minutes, seconds, microseconds): - * self.years = years - * # ... -*/ -static int Duration_init(Duration *self, PyObject *args, PyObject *kwargs) { - int years; - int months; - int weeks; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - - if (!PyArg_ParseTuple(args, "iiiiiiii", &years, &months, &weeks, &days, &hours, &minutes, &seconds, µseconds)) - return -1; - - self->years = years; - self->months = months; - self->weeks = weeks; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - - return 0; -} - -/* - * def __repr__(self): - * return '{} years {} months {} days {} hours {} minutes {} seconds {} microseconds'.format( - * self.years, self.months, self.days, self.minutes, self.hours, self.seconds, self.microseconds - * ) - */ -static PyObject *Duration_repr(Duration *self) { - char repr[82] = {0}; - - sprintf( - repr, - "%d years %d months %d weeks %d days %d hours %d minutes %d seconds %d microseconds", - self->years, - self->months, - self->weeks, - self->days, - self->hours, - self->minutes, - self->seconds, - self->microseconds - ); - - return PyUnicode_FromString(repr); -} - -/* - * Instantiate new Duration_type object - * Skip overhead of calling PyObject_New and PyObject_Init. - * Directly allocate object. - */ -static PyObject *new_duration_ex(int years, int months, int weeks, int days, int hours, int minutes, int seconds, int microseconds, PyTypeObject *type) { - Duration *self = (Duration *) (type->tp_alloc(type, 0)); - - if (self != NULL) { - self->years = years; - self->months = months; - self->weeks = weeks; - self->days = days; - self->hours = hours; - self->minutes = minutes; - self->seconds = seconds; - self->microseconds = microseconds; - } - - return (PyObject *) self; -} - -/* - * Class member / class attributes - */ -static PyMemberDef Duration_members[] = { - {"years", T_INT, offsetof(Duration, years), 0, "years in duration"}, - {"months", T_INT, offsetof(Duration, months), 0, "months in duration"}, - {"weeks", T_INT, offsetof(Duration, weeks), 0, "weeks in duration"}, - {"days", T_INT, offsetof(Duration, days), 0, "days in duration"}, - {"remaining_days", T_INT, offsetof(Duration, days), 0, "days in duration"}, - {"hours", T_INT, offsetof(Duration, hours), 0, "hours in duration"}, - {"minutes", T_INT, offsetof(Duration, minutes), 0, "minutes in duration"}, - {"seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"}, - {"remaining_seconds", T_INT, offsetof(Duration, seconds), 0, "seconds in duration"}, - {"microseconds", T_INT, offsetof(Duration, microseconds), 0, "microseconds in duration"}, - {NULL} -}; - -static PyTypeObject Duration_type = { - PyVarObject_HEAD_INIT(NULL, 0) - "Duration", /* tp_name */ - sizeof(Duration), /* tp_basicsize */ - 0, /* tp_itemsize */ - 0, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_as_async */ - (reprfunc)Duration_repr, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - (reprfunc)Duration_repr, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /* tp_flags */ - "Duration", /* tp_doc */ -}; - -#define new_duration(years, months, weeks, days, hours, minutes, seconds, microseconds) new_duration_ex(years, months, weeks, days, hours, minutes, seconds, microseconds, &Duration_type) - -typedef struct { - int is_date; - int is_time; - int is_datetime; - int is_duration; - int is_period; - int ambiguous; - int year; - int month; - int day; - int hour; - int minute; - int second; - int microsecond; - int offset; - int has_offset; - char *tzname; - int years; - int months; - int weeks; - int days; - int hours; - int minutes; - int seconds; - int microseconds; - int error; -} Parsed; - - -Parsed* new_parsed() { - Parsed *parsed; - - if((parsed = malloc(sizeof *parsed)) != NULL) { - parsed->is_date = 0; - parsed->is_time = 0; - parsed->is_datetime = 0; - parsed->is_duration = 0; - parsed->is_period = 0; - - parsed->ambiguous = 0; - parsed->year = 0; - parsed->month = 1; - parsed->day = 1; - parsed->hour = 0; - parsed->minute = 0; - parsed->second = 0; - parsed->microsecond = 0; - parsed->offset = 0; - parsed->has_offset = 0; - parsed->tzname = NULL; - - parsed->years = 0; - parsed->months = 0; - parsed->weeks = 0; - parsed->days = 0; - parsed->hours = 0; - parsed->minutes = 0; - parsed->seconds = 0; - parsed->microseconds = 0; - - parsed->error = -1; - } - - return parsed; -} - - -/* -------------------------- Functions --------------------------*/ - -Parsed* _parse_iso8601_datetime(char *str, Parsed *parsed) { - char* c; - int monthday = 0; - int week = 0; - int weekday = 1; - int ordinal; - int tz_sign = 0; - int leap = 0; - int separators = 0; - int time = 0; - int has_hour = 0; - int i; - int j; - - // Assuming date only for now - parsed->is_date = 1; - - c = str; - - for (i = 0; i < 4; i++) { - if (*c >= '0' && *c <= '9') { - parsed->year = 10 * parsed->year + *c++ - '0'; - } else { - parsed->error = PARSER_INVALID_ISO8601; - - return NULL; - } - } - - leap = is_leap(parsed->year); - - // Optional separator - if (*c == '-') { - separators++; - c++; - } - - // Checking for week dates - if (*c == 'W') { - c++; - - i = 0; - while (*c != '\0' && *c != ' ' && *c != 'T') { - if (*c == '-') { - separators++; - c++; - continue; - } - - week = 10 * week + *c++ - '0'; - - i++; - } - - switch (i) { - case 2: - // Only week number - break; - case 3: - // Week with weekday - if (!(separators == 0 || separators == 2)) { - // We should have 2 or no separator - parsed->error = PARSER_INVALID_WEEK_DATE; - - return NULL; - } - - weekday = week % 10; - week /= 10; - - break; - default: - // Any other case is wrong - parsed->error = PARSER_INVALID_WEEK_DATE; - - return NULL; - } - - // Checks - if (week > 53 || (week > 52 && !is_long_year(parsed->year))) { - parsed->error = PARSER_INVALID_WEEK_NUMBER; - - return NULL; - } - - if (weekday > 7) { - parsed->error = PARSER_INVALID_WEEKDAY_NUMBER; - - return NULL; - } - - // Calculating ordinal day - ordinal = week * 7 + weekday - (week_day(parsed->year, 1, 4) + 3); - - if (ordinal < 1) { - // Previous year - ordinal += days_in_year(parsed->year - 1); - parsed->year -= 1; - leap = is_leap(parsed->year); - } - - if (ordinal > days_in_year(parsed->year)) { - // Next year - ordinal -= days_in_year(parsed->year); - parsed->year += 1; - leap = is_leap(parsed->year); - } - - for (j = 1; j < 14; j++) { - if (ordinal <= MONTHS_OFFSETS[leap][j]) { - parsed->day = ordinal - MONTHS_OFFSETS[leap][j - 1]; - parsed->month = j - 1; - - break; - } - } - } else { - // At this point we need to check the number - // of characters until the end of the date part - // (or the end of the string). - // - // If two, we have only a month if there is a separator, it may be a time otherwise. - // If three, we have an ordinal date. - // If four, we have a complete date - i = 0; - while (*c != '\0' && *c != ' ' && *c != 'T') { - if (*c == '-') { - separators++; - c++; - continue; - } - - if (!(*c >= '0' && *c <='9')) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - monthday = 10 * monthday + *c++ - '0'; - - i++; - } - - switch (i) { - case 0: - // No month/day specified (only a year) - break; - case 2: - if (!separators) { - // The date looks like 201207 - // which is invalid for a date - // But it might be a time in the form hhmmss - parsed->ambiguous = 1; - } else if (separators > 1) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - parsed->month = monthday; - break; - case 3: - // Ordinal day - if (separators > 1) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - if (monthday < 1 || monthday > MONTHS_OFFSETS[leap][13]) { - parsed->error = PARSER_INVALID_ORDINAL_DAY_FOR_YEAR; - - return NULL; - } - - for (j = 1; j < 14; j++) { - if (monthday <= MONTHS_OFFSETS[leap][j]) { - parsed->day = monthday - MONTHS_OFFSETS[leap][j - 1]; - parsed->month = j - 1; - - break; - } - } - - break; - case 4: - // Month and day - parsed->month = monthday / 100; - parsed->day = monthday % 100; - - break; - default: - parsed->error = PARSER_INVALID_MONTH_OR_DAY; - - return NULL; - } - } - - // Checks - if (separators && !monthday && !week) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - if (parsed->month > 12) { - parsed->error = PARSER_INVALID_MONTH; - - return NULL; - } - - if (parsed->day > DAYS_PER_MONTHS[leap][parsed->month]) { - parsed->error = PARSER_INVALID_DAY_FOR_MONTH; - - return NULL; - } - - separators = 0; - if (*c == 'T' || *c == ' ') { - if (parsed->ambiguous) { - parsed->error = PARSER_INVALID_DATE; - - return NULL; - } - - // We have time so we have a datetime - parsed->is_datetime = 1; - parsed->is_date = 0; - - c++; - - // Grabbing time information - i = 0; - while (*c != '\0' && *c != '.' && *c != ',' && *c != 'Z' && *c != '+' && *c != '-') { - if (*c == ':') { - separators++; - c++; - continue; - } - - if (!(*c >= '0' && *c <='9')) { - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - time = 10 * time + *c++ - '0'; - i++; - } - - switch (i) { - case 2: - // Hours only - if (separators > 0) { - // Extraneous separators - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - parsed->hour = time; - has_hour = 1; - break; - case 4: - // Hours and minutes - if (separators > 1) { - // Extraneous separators - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - parsed->hour = time / 100; - parsed->minute = time % 100; - has_hour = 1; - break; - case 6: - // Hours, minutes and seconds - if (!(separators == 0 || separators == 2)) { - // We should have either two separators or none - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - parsed->hour = time / 10000; - parsed->minute = time / 100 % 100; - parsed->second = time % 100; - has_hour = 1; - break; - default: - // Any other case is wrong - parsed->error = PARSER_INVALID_TIME; - - return NULL; - } - - // Checks - if (parsed->hour > 23) { - parsed->error = PARSER_INVALID_HOUR; - - return NULL; - } - - if (parsed->minute > 59) { - parsed->error = PARSER_INVALID_MINUTE; - - return NULL; - } - - if (parsed->second > 59) { - parsed->error = PARSER_INVALID_SECOND; - - return NULL; - } - - // Subsecond - if (*c == '.' || *c == ',') { - c++; - - time = 0; - i = 0; - while (*c != '\0' && *c != 'Z' && *c != '+' && *c != '-') { - if (!(*c >= '0' && *c <='9')) { - parsed->error = PARSER_INVALID_SUBSECOND; - - return NULL; - } - - time = 10 * time + *c++ - '0'; - i++; - } - - // adjust to microseconds - if (i > 6) { - parsed->microsecond = time / pow(10, i - 6); - } else if (i <= 6) { - parsed->microsecond = time * pow(10, 6 - i); - } - } - - // Timezone - if (*c == 'Z') { - parsed->has_offset = 1; - parsed->tzname = "UTC"; - c++; - } else if (*c == '+' || *c == '-') { - tz_sign = 1; - if (*c == '-') { - tz_sign = -1; - } - - parsed->has_offset = 1; - c++; - - i = 0; - time = 0; - separators = 0; - while (*c != '\0') { - if (*c == ':') { - separators++; - c++; - continue; - } - - if (!(*c >= '0' && *c <= '9')) { - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - - time = 10 * time + *c++ - '0'; - i++; - } - - switch (i) { - case 2: - // hh Format - if (separators) { - // Extraneous separators - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - - parsed->offset = tz_sign * (time * 3600); - break; - case 4: - // hhmm Format - if (separators > 1) { - // Extraneous separators - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - - parsed->offset = tz_sign * ((time / 100 * 3600) + (time % 100 * 60)); - break; - default: - // Wrong format - parsed->error = PARSER_INVALID_TZ_OFFSET; - - return NULL; - } - } - } - - // At this point we should be at the end of the string - // If not, the string is invalid - if (*c != '\0') { - parsed->error = PARSER_INVALID_ISO8601; - - return NULL; - } - - return parsed; -} - - -Parsed* _parse_iso8601_duration(char *str, Parsed *parsed) { - char* c; - int value = 0; - int grabbed = 0; - int in_time = 0; - int in_fraction = 0; - int fraction_length = 0; - int has_fractional = 0; - int fraction = 0; - int has_ymd = 0; - int has_week = 0; - int has_year = 0; - int has_month = 0; - int has_day = 0; - int has_hour = 0; - int has_minute = 0; - int has_second = 0; - - c = str; - - // Removing P operator - c++; - - parsed->is_duration = 1; - - for (; *c != '\0'; c++) { - switch (*c) { - case 'Y': - if (!grabbed || in_time || has_week || has_ymd) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (fraction) { - parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED; - - return NULL; - } - - parsed->years = value; - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_ymd = 1; - has_year = 1; - - break; - case 'M': - if (!grabbed || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (in_time) { - if (has_second) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_fractional) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->minutes = value; - if (fraction) { - parsed->seconds = fraction * 6; - has_fractional = 1; - } - - has_minute = 1; - } else { - if (fraction) { - parsed->error = PARSER_INVALID_DURATION_FLOAT_YEAR_MONTH_NOT_SUPPORTED; - - return NULL; - } - - if (has_month || has_day) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->months = value; - has_ymd = 1; - has_month = 1; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - - break; - case 'D': - if (!grabbed || in_time || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_day) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->days = value; - if (fraction) { - parsed->hours = fraction * 2.4; - has_fractional = 1; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_ymd = 1; - has_day = 1; - - break; - case 'T': - if (grabbed) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - in_time = 1; - - break; - case 'H': - if (!grabbed || !in_time || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_hour || has_second || has_minute) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_fractional) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->hours = value; - if (fraction) { - parsed->minutes = fraction * 6; - has_fractional = 1; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_hour = 1; - - break; - case 'S': - if (!grabbed || !in_time || has_week) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_second) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (has_fractional) { - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - if (fraction) { - parsed->seconds = value; - if (fraction_length > 6) { - parsed->microseconds = fraction / pow(10, fraction_length - 6); - } else { - parsed->microseconds = fraction * pow(10, 6 - fraction_length); - } - has_fractional = 1; - } else { - parsed->seconds = value; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_second = 1; - - break; - case 'W': - if (!grabbed || in_time || has_ymd) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - parsed->weeks = value; - if (fraction) { - float days; - days = fraction * 0.7; - parsed->hours = (int) ((days - (int) days) * 24); - parsed->days = (int) days; - } - - grabbed = 0; - value = 0; - fraction = 0; - in_fraction = 0; - has_week = 1; - - break; - case '.': - if (!grabbed || has_fractional) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - in_fraction = 1; - - break; - case ',': - if (!grabbed || has_fractional) { - // No value grabbed - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - - in_fraction = 1; - - break; - default: - if (*c >= '0' && *c <='9') { - if (in_fraction) { - fraction = 10 * fraction + *c - '0'; - fraction_length++; - } else { - value = 10 * value + *c - '0'; - grabbed = 1; - } - break; - } - - parsed->error = PARSER_INVALID_DURATION; - - return NULL; - } - } - - return parsed; -} - - -PyObject* parse_iso8601(PyObject *self, PyObject *args) { - char* str; - PyObject *obj; - PyObject *tzinfo; - Parsed *parsed = new_parsed(); - - if (!PyArg_ParseTuple(args, "s", &str)) { - PyErr_SetString( - PyExc_ValueError, "Invalid parameters" - ); - return NULL; - } - - if (*str == 'P') { - // Duration (or interval) - if (_parse_iso8601_duration(str, parsed) == NULL) { - PyErr_SetString( - PyExc_ValueError, PARSER_ERRORS[parsed->error] - ); - - return NULL; - } - } else if (_parse_iso8601_datetime(str, parsed) == NULL) { - PyErr_SetString( - PyExc_ValueError, PARSER_ERRORS[parsed->error] - ); - - return NULL; - } - - if (parsed->is_date) { - // Date only - if (parsed->ambiguous) { - // We can "safely" assume that the ambiguous - // date was actually a time in the form hhmmss - parsed->hour = parsed->year / 100; - parsed->minute = parsed->year % 100; - parsed->second = parsed->month; - - obj = PyDateTimeAPI->Time_FromTime( - parsed->hour, parsed->minute, parsed->second, parsed->microsecond, - Py_BuildValue(""), - PyDateTimeAPI->TimeType - ); - } else { - obj = PyDateTimeAPI->Date_FromDate( - parsed->year, parsed->month, parsed->day, - PyDateTimeAPI->DateType - ); - } - } else if (parsed->is_datetime) { - if (!parsed->has_offset) { - tzinfo = Py_BuildValue(""); - } else { - tzinfo = new_fixed_offset(parsed->offset, parsed->tzname); - } - - obj = PyDateTimeAPI->DateTime_FromDateAndTime( - parsed->year, - parsed->month, - parsed->day, - parsed->hour, - parsed->minute, - parsed->second, - parsed->microsecond, - tzinfo, - PyDateTimeAPI->DateTimeType - ); - - Py_DECREF(tzinfo); - } else if (parsed->is_duration) { - obj = new_duration( - parsed->years, parsed->months, parsed->weeks, parsed->days, - parsed->hours, parsed->minutes, parsed->seconds, parsed->microseconds - ); - } else { - return NULL; - } - - free(parsed); - - return obj; -} - - -/* ------------------------------------------------------------------------- */ - -static PyMethodDef helpers_methods[] = { - { - "parse_iso8601", - (PyCFunction) parse_iso8601, - METH_VARARGS, - PyDoc_STR("Parses a ISO8601 string into a tuple.") - }, - {NULL} -}; - - -/* ------------------------------------------------------------------------- */ - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_iso8601", - NULL, - -1, - helpers_methods, - NULL, - NULL, - NULL, - NULL, -}; - -PyMODINIT_FUNC -PyInit__iso8601(void) -{ - PyObject *module; - - PyDateTime_IMPORT; - - module = PyModule_Create(&moduledef); - - if (module == NULL) - return NULL; - - // FixedOffset declaration - FixedOffset_type.tp_new = PyType_GenericNew; - FixedOffset_type.tp_base = PyDateTimeAPI->TZInfoType; - FixedOffset_type.tp_methods = FixedOffset_methods; - FixedOffset_type.tp_members = FixedOffset_members; - FixedOffset_type.tp_init = (initproc)FixedOffset_init; - - if (PyType_Ready(&FixedOffset_type) < 0) - return NULL; - - // Duration declaration - Duration_type.tp_new = PyType_GenericNew; - Duration_type.tp_members = Duration_members; - Duration_type.tp_init = (initproc)Duration_init; - - if (PyType_Ready(&Duration_type) < 0) - return NULL; - - Py_INCREF(&FixedOffset_type); - Py_INCREF(&Duration_type); - - PyModule_AddObject(module, "TZFixedOffset", (PyObject *)&FixedOffset_type); - PyModule_AddObject(module, "Duration", (PyObject *)&Duration_type); - - return module; -} diff --git a/pendulum/period.py b/pendulum/period.py deleted file mode 100644 index c66c6b9d..00000000 --- a/pendulum/period.py +++ /dev/null @@ -1,390 +0,0 @@ -from __future__ import absolute_import - -import operator - -from datetime import date -from datetime import datetime -from datetime import timedelta - -import pendulum - -from pendulum.utils._compat import _HAS_FOLD -from pendulum.utils._compat import decode - -from .constants import MONTHS_PER_YEAR -from .duration import Duration -from .helpers import precise_diff - - -class Period(Duration): - """ - Duration class that is aware of the datetimes that generated the - time difference. - """ - - def __new__(cls, start, end, absolute=False): - if isinstance(start, datetime) and isinstance(end, datetime): - if ( - start.tzinfo is None - and end.tzinfo is not None - or start.tzinfo is not None - and end.tzinfo is None - ): - raise TypeError("can't compare offset-naive and offset-aware datetimes") - - if absolute and start > end: - end, start = start, end - - _start = start - _end = end - if isinstance(start, pendulum.DateTime): - if _HAS_FOLD: - _start = datetime( - start.year, - start.month, - start.day, - start.hour, - start.minute, - start.second, - start.microsecond, - tzinfo=start.tzinfo, - fold=start.fold, - ) - else: - _start = datetime( - start.year, - start.month, - start.day, - start.hour, - start.minute, - start.second, - start.microsecond, - tzinfo=start.tzinfo, - ) - elif isinstance(start, pendulum.Date): - _start = date(start.year, start.month, start.day) - - if isinstance(end, pendulum.DateTime): - if _HAS_FOLD: - _end = datetime( - end.year, - end.month, - end.day, - end.hour, - end.minute, - end.second, - end.microsecond, - tzinfo=end.tzinfo, - fold=end.fold, - ) - else: - _end = datetime( - end.year, - end.month, - end.day, - end.hour, - end.minute, - end.second, - end.microsecond, - tzinfo=end.tzinfo, - ) - elif isinstance(end, pendulum.Date): - _end = date(end.year, end.month, end.day) - - # Fixing issues with datetime.__sub__() - # not handling offsets if the tzinfo is the same - if ( - isinstance(_start, datetime) - and isinstance(_end, datetime) - and _start.tzinfo is _end.tzinfo - ): - if _start.tzinfo is not None: - _start = (_start - start.utcoffset()).replace(tzinfo=None) - - if isinstance(end, datetime) and _end.tzinfo is not None: - _end = (_end - end.utcoffset()).replace(tzinfo=None) - - delta = _end - _start - - return super(Period, cls).__new__(cls, seconds=delta.total_seconds()) - - def __init__(self, start, end, absolute=False): - super(Period, self).__init__() - - if not isinstance(start, pendulum.Date): - if isinstance(start, datetime): - start = pendulum.instance(start) - else: - start = pendulum.date(start.year, start.month, start.day) - - _start = start - else: - if isinstance(start, pendulum.DateTime): - _start = datetime( - start.year, - start.month, - start.day, - start.hour, - start.minute, - start.second, - start.microsecond, - tzinfo=start.tzinfo, - ) - else: - _start = date(start.year, start.month, start.day) - - if not isinstance(end, pendulum.Date): - if isinstance(end, datetime): - end = pendulum.instance(end) - else: - end = pendulum.date(end.year, end.month, end.day) - - _end = end - else: - if isinstance(end, pendulum.DateTime): - _end = datetime( - end.year, - end.month, - end.day, - end.hour, - end.minute, - end.second, - end.microsecond, - tzinfo=end.tzinfo, - ) - else: - _end = date(end.year, end.month, end.day) - - self._invert = False - if start > end: - self._invert = True - - if absolute: - end, start = start, end - _end, _start = _start, _end - - self._absolute = absolute - self._start = start - self._end = end - self._delta = precise_diff(_start, _end) - - @property - def years(self): - return self._delta.years - - @property - def months(self): - return self._delta.months - - @property - def weeks(self): - return abs(self._delta.days) // 7 * self._sign(self._delta.days) - - @property - def days(self): - return self._days - - @property - def remaining_days(self): - return abs(self._delta.days) % 7 * self._sign(self._days) - - @property - def hours(self): - return self._delta.hours - - @property - def minutes(self): - return self._delta.minutes - - @property - def start(self): - return self._start - - @property - def end(self): - return self._end - - def in_years(self): - """ - Gives the duration of the Period in full years. - - :rtype: int - """ - return self.years - - def in_months(self): - """ - Gives the duration of the Period in full months. - - :rtype: int - """ - return self.years * MONTHS_PER_YEAR + self.months - - def in_weeks(self): - days = self.in_days() - sign = 1 - - if days < 0: - sign = -1 - - return sign * (abs(days) // 7) - - def in_days(self): - return self._delta.total_days - - def in_words(self, locale=None, separator=" "): - """ - Get the current interval in words in the current locale. - - Ex: 6 jours 23 heures 58 minutes - - :param locale: The locale to use. Defaults to current locale. - :type locale: str - - :param separator: The separator to use between each unit - :type separator: str - - :rtype: str - """ - periods = [ - ("year", self.years), - ("month", self.months), - ("week", self.weeks), - ("day", self.remaining_days), - ("hour", self.hours), - ("minute", self.minutes), - ("second", self.remaining_seconds), - ] - - if locale is None: - locale = pendulum.get_locale() - - locale = pendulum.locale(locale) - parts = [] - for period in periods: - unit, count = period - if abs(count) > 0: - translation = locale.translation( - "units.{}.{}".format(unit, locale.plural(abs(count))) - ) - parts.append(translation.format(count)) - - if not parts: - if abs(self.microseconds) > 0: - unit = "units.second.{}".format(locale.plural(1)) - count = "{:.2f}".format(abs(self.microseconds) / 1e6) - else: - unit = "units.microsecond.{}".format(locale.plural(0)) - count = 0 - translation = locale.translation(unit) - parts.append(translation.format(count)) - - return decode(separator.join(parts)) - - def range(self, unit, amount=1): - method = "add" - op = operator.le - if not self._absolute and self.invert: - method = "subtract" - op = operator.ge - - start, end = self.start, self.end - - i = amount - while op(start, end): - yield start - - start = getattr(self.start, method)(**{unit: i}) - - i += amount - - def as_interval(self): - """ - Return the Period as an Duration. - - :rtype: Duration - """ - return Duration(seconds=self.total_seconds()) - - def __iter__(self): - return self.range("days") - - def __contains__(self, item): - return self.start <= item <= self.end - - def __add__(self, other): - return self.as_interval().__add__(other) - - __radd__ = __add__ - - def __sub__(self, other): - return self.as_interval().__sub__(other) - - def __neg__(self): - return self.__class__(self.end, self.start, self._absolute) - - def __mul__(self, other): - return self.as_interval().__mul__(other) - - __rmul__ = __mul__ - - def __floordiv__(self, other): - return self.as_interval().__floordiv__(other) - - def __truediv__(self, other): - return self.as_interval().__truediv__(other) - - __div__ = __floordiv__ - - def __mod__(self, other): - return self.as_interval().__mod__(other) - - def __divmod__(self, other): - return self.as_interval().__divmod__(other) - - def __abs__(self): - return self.__class__(self.start, self.end, True) - - def __repr__(self): - return " {}]>".format(self._start, self._end) - - def __str__(self): - return self.__repr__() - - def _cmp(self, other): - # Only needed for PyPy - assert isinstance(other, timedelta) - - if isinstance(other, Period): - other = other.as_timedelta() - - td = self.as_timedelta() - - return 0 if td == other else 1 if td > other else -1 - - def _getstate(self, protocol=3): - start, end = self.start, self.end - - if self._invert and self._absolute: - end, start = start, end - - return (start, end, self._absolute) - - def __reduce__(self): - return self.__reduce_ex__(2) - - def __reduce_ex__(self, protocol): - return self.__class__, self._getstate(protocol) - - def __hash__(self): - return hash((self.start, self.end, self._absolute)) - - def __eq__(self, other): - if isinstance(other, Period): - return (self.start, self.end, self._absolute) == ( - other.start, - other.end, - other._absolute, - ) - else: - return self.as_interval() == other diff --git a/pendulum/tz/__init__.py b/pendulum/tz/__init__.py deleted file mode 100644 index e45a6cde..00000000 --- a/pendulum/tz/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import Tuple -from typing import Union - -import pytzdata - -from .local_timezone import get_local_timezone -from .local_timezone import set_local_timezone -from .local_timezone import test_local_timezone -from .timezone import UTC -from .timezone import FixedTimezone as _FixedTimezone -from .timezone import Timezone as _Timezone - - -PRE_TRANSITION = "pre" -POST_TRANSITION = "post" -TRANSITION_ERROR = "error" - -timezones = pytzdata.timezones # type: Tuple[str, ...] - - -_tz_cache = {} - - -def timezone(name, extended=True): # type: (Union[str, int], bool) -> _Timezone - """ - Return a Timezone instance given its name. - """ - if isinstance(name, int): - return fixed_timezone(name) - - if name.lower() == "utc": - return UTC - - if name in _tz_cache: - return _tz_cache[name] - - tz = _Timezone(name, extended=extended) - _tz_cache[name] = tz - - return tz - - -def fixed_timezone(offset): # type: (int) -> _FixedTimezone - """ - Return a Timezone instance given its offset in seconds. - """ - if offset in _tz_cache: - return _tz_cache[offset] # type: ignore - - tz = _FixedTimezone(offset) - _tz_cache[offset] = tz - - return tz - - -def local_timezone(): # type: () -> _Timezone - """ - Return the local timezone. - """ - return get_local_timezone() diff --git a/pendulum/tz/exceptions.py b/pendulum/tz/exceptions.py deleted file mode 100644 index b91fa062..00000000 --- a/pendulum/tz/exceptions.py +++ /dev/null @@ -1,23 +0,0 @@ -class TimezoneError(ValueError): - - pass - - -class NonExistingTime(TimezoneError): - - message = "The datetime {} does not exist." - - def __init__(self, dt): - message = self.message.format(dt) - - super(NonExistingTime, self).__init__(message) - - -class AmbiguousTime(TimezoneError): - - message = "The datetime {} is ambiguous." - - def __init__(self, dt): - message = self.message.format(dt) - - super(AmbiguousTime, self).__init__(message) diff --git a/pendulum/tz/local_timezone.py b/pendulum/tz/local_timezone.py deleted file mode 100644 index 08a6e4fa..00000000 --- a/pendulum/tz/local_timezone.py +++ /dev/null @@ -1,257 +0,0 @@ -import os -import re -import sys - -from contextlib import contextmanager -from typing import Iterator -from typing import Optional -from typing import Union - -from .timezone import Timezone -from .timezone import TimezoneFile -from .zoneinfo.exceptions import InvalidTimezone - - -try: - import _winreg as winreg -except ImportError: - try: - import winreg - except ImportError: - winreg = None - - -_mock_local_timezone = None -_local_timezone = None - - -def get_local_timezone(): # type: () -> Timezone - global _local_timezone - - if _mock_local_timezone is not None: - return _mock_local_timezone - - if _local_timezone is None: - tz = _get_system_timezone() - - _local_timezone = tz - - return _local_timezone - - -def set_local_timezone(mock=None): # type: (Optional[Union[str, Timezone]]) -> None - global _mock_local_timezone - - _mock_local_timezone = mock - - -@contextmanager -def test_local_timezone(mock): # type: (Timezone) -> Iterator[None] - set_local_timezone(mock) - - yield - - set_local_timezone() - - -def _get_system_timezone(): # type: () -> Timezone - if sys.platform == "win32": - return _get_windows_timezone() - elif "darwin" in sys.platform: - return _get_darwin_timezone() - - return _get_unix_timezone() - - -def _get_windows_timezone(): # type: () -> Timezone - from .data.windows import windows_timezones - - # Windows is special. It has unique time zone names (in several - # meanings of the word) available, but unfortunately, they can be - # translated to the language of the operating system, so we need to - # do a backwards lookup, by going through all time zones and see which - # one matches. - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - - tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" - localtz = winreg.OpenKey(handle, tz_local_key_name) - - timezone_info = {} - size = winreg.QueryInfoKey(localtz)[1] - for i in range(size): - data = winreg.EnumValue(localtz, i) - timezone_info[data[0]] = data[1] - - localtz.Close() - - if "TimeZoneKeyName" in timezone_info: - # Windows 7 (and Vista?) - - # For some reason this returns a string with loads of NUL bytes at - # least on some systems. I don't know if this is a bug somewhere, I - # just work around it. - tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0] - else: - # Windows 2000 or XP - - # This is the localized name: - tzwin = timezone_info["StandardName"] - - # Open the list of timezones to look up the real name: - tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" - tzkey = winreg.OpenKey(handle, tz_key_name) - - # Now, match this value to Time Zone information - tzkeyname = None - for i in range(winreg.QueryInfoKey(tzkey)[0]): - subkey = winreg.EnumKey(tzkey, i) - sub = winreg.OpenKey(tzkey, subkey) - - info = {} - size = winreg.QueryInfoKey(sub)[1] - for i in range(size): - data = winreg.EnumValue(sub, i) - info[data[0]] = data[1] - - sub.Close() - try: - if info["Std"] == tzwin: - tzkeyname = subkey - break - except KeyError: - # This timezone didn't have proper configuration. - # Ignore it. - pass - - tzkey.Close() - handle.Close() - - if tzkeyname is None: - raise LookupError("Can not find Windows timezone configuration") - - timezone = windows_timezones.get(tzkeyname) - if timezone is None: - # Nope, that didn't work. Try adding "Standard Time", - # it seems to work a lot of times: - timezone = windows_timezones.get(tzkeyname + " Standard Time") - - # Return what we have. - if timezone is None: - raise LookupError("Unable to find timezone " + tzkeyname) - - return Timezone(timezone) - - -def _get_darwin_timezone(): # type: () -> Timezone - # link will be something like /usr/share/zoneinfo/America/Los_Angeles. - link = os.readlink("/etc/localtime") - tzname = link[link.rfind("zoneinfo/") + 9 :] - - return Timezone(tzname) - - -def _get_unix_timezone(_root="/"): # type: (str) -> Timezone - tzenv = os.environ.get("TZ") - if tzenv: - try: - return _tz_from_env(tzenv) - except ValueError: - pass - - # Now look for distribution specific configuration files - # that contain the timezone name. - tzpath = os.path.join(_root, "etc/timezone") - if os.path.exists(tzpath): - with open(tzpath, "rb") as tzfile: - data = tzfile.read() - - # Issue #3 was that /etc/timezone was a zoneinfo file. - # That's a misconfiguration, but we need to handle it gracefully: - if data[:5] != "TZif2": - etctz = data.strip().decode() - # Get rid of host definitions and comments: - if " " in etctz: - etctz, dummy = etctz.split(" ", 1) - if "#" in etctz: - etctz, dummy = etctz.split("#", 1) - - return Timezone(etctz.replace(" ", "_")) - - # CentOS has a ZONE setting in /etc/sysconfig/clock, - # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and - # Gentoo has a TIMEZONE setting in /etc/conf.d/clock - # We look through these files for a timezone: - zone_re = re.compile(r'\s*ZONE\s*=\s*"') - timezone_re = re.compile(r'\s*TIMEZONE\s*=\s*"') - end_re = re.compile('"') - - for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"): - tzpath = os.path.join(_root, filename) - if not os.path.exists(tzpath): - continue - - with open(tzpath, "rt") as tzfile: - data = tzfile.readlines() - - for line in data: - # Look for the ZONE= setting. - match = zone_re.match(line) - if match is None: - # No ZONE= setting. Look for the TIMEZONE= setting. - match = timezone_re.match(line) - - if match is not None: - # Some setting existed - line = line[match.end() :] - etctz = line[: end_re.search(line).start()] - - parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep))) - tzpath = [] - while parts: - tzpath.insert(0, parts.pop(0)) - - try: - return Timezone(os.path.join(*tzpath)) - except InvalidTimezone: - pass - - # systemd distributions use symlinks that include the zone name, - # see manpage of localtime(5) and timedatectl(1) - tzpath = os.path.join(_root, "etc", "localtime") - if os.path.exists(tzpath) and os.path.islink(tzpath): - parts = list( - reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep)) - ) - tzpath = [] - while parts: - tzpath.insert(0, parts.pop(0)) - try: - return Timezone(os.path.join(*tzpath)) - except InvalidTimezone: - pass - - # No explicit setting existed. Use localtime - for filename in ("etc/localtime", "usr/local/etc/localtime"): - tzpath = os.path.join(_root, filename) - - if not os.path.exists(tzpath): - continue - - return TimezoneFile(tzpath) - - raise RuntimeError("Unable to find any timezone configuration") - - -def _tz_from_env(tzenv): # type: (str) -> Timezone - if tzenv[0] == ":": - tzenv = tzenv[1:] - - # TZ specifies a file - if os.path.exists(tzenv): - return TimezoneFile(tzenv) - - # TZ specifies a zoneinfo zone. - try: - return Timezone(tzenv) - except ValueError: - raise diff --git a/pendulum/tz/timezone.py b/pendulum/tz/timezone.py deleted file mode 100644 index 62810130..00000000 --- a/pendulum/tz/timezone.py +++ /dev/null @@ -1,377 +0,0 @@ -from datetime import datetime -from datetime import timedelta -from datetime import tzinfo -from typing import Optional -from typing import TypeVar -from typing import overload - -import pendulum - -from pendulum.helpers import local_time -from pendulum.helpers import timestamp -from pendulum.utils._compat import _HAS_FOLD - -from .exceptions import AmbiguousTime -from .exceptions import NonExistingTime -from .zoneinfo import read -from .zoneinfo import read_file -from .zoneinfo.transition import Transition - - -POST_TRANSITION = "post" -PRE_TRANSITION = "pre" -TRANSITION_ERROR = "error" - -_datetime = datetime -_D = TypeVar("_D", bound=datetime) - - -class Timezone(tzinfo): - """ - Represents a named timezone. - - The accepted names are those provided by the IANA time zone database. - - >>> from pendulum.tz.timezone import Timezone - >>> tz = Timezone('Europe/Paris') - """ - - def __init__(self, name, extended=True): # type: (str, bool) -> None - tz = read(name, extend=extended) - - self._name = name - self._transitions = tz.transitions - self._hint = {True: None, False: None} - - @property - def name(self): # type: () -> str - return self._name - - def convert(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D - """ - Converts a datetime in the current timezone. - - If the datetime is naive, it will be normalized. - - >>> from datetime import datetime - >>> from pendulum import timezone - >>> paris = timezone('Europe/Paris') - >>> dt = datetime(2013, 3, 31, 2, 30, fold=1) - >>> in_paris = paris.convert(dt) - >>> in_paris.isoformat() - '2013-03-31T03:30:00+02:00' - - If the datetime is aware, it will be properly converted. - - >>> new_york = timezone('America/New_York') - >>> in_new_york = new_york.convert(in_paris) - >>> in_new_york.isoformat() - '2013-03-30T21:30:00-04:00' - """ - if dt.tzinfo is None: - return self._normalize(dt, dst_rule=dst_rule) - - return self._convert(dt) - - def datetime( - self, year, month, day, hour=0, minute=0, second=0, microsecond=0 - ): # type: (int, int, int, int, int, int, int) -> _datetime - """ - Return a normalized datetime for the current timezone. - """ - if _HAS_FOLD: - return self.convert( - datetime(year, month, day, hour, minute, second, microsecond, fold=1) - ) - - return self.convert( - datetime(year, month, day, hour, minute, second, microsecond), - dst_rule=POST_TRANSITION, - ) - - def _normalize(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D - sec = timestamp(dt) - fold = 0 - transition = self._lookup_transition(sec) - - if not _HAS_FOLD and dst_rule is None: - dst_rule = POST_TRANSITION - - if dst_rule is None: - dst_rule = PRE_TRANSITION - if dt.fold == 1: - dst_rule = POST_TRANSITION - - if sec < transition.local: - if transition.is_ambiguous(sec): - # Ambiguous time - if dst_rule == TRANSITION_ERROR: - raise AmbiguousTime(dt) - - # We set the fold attribute for later - if dst_rule == POST_TRANSITION: - fold = 1 - elif transition.previous is not None: - transition = transition.previous - - if transition: - if transition.is_ambiguous(sec): - # Ambiguous time - if dst_rule == TRANSITION_ERROR: - raise AmbiguousTime(dt) - - # We set the fold attribute for later - if dst_rule == POST_TRANSITION: - fold = 1 - elif transition.is_missing(sec): - # Skipped time - if dst_rule == TRANSITION_ERROR: - raise NonExistingTime(dt) - - # We adjust accordingly - if dst_rule == POST_TRANSITION: - sec += transition.fix - fold = 1 - else: - sec -= transition.fix - - kwargs = {"tzinfo": self} - if _HAS_FOLD or isinstance(dt, pendulum.DateTime): - kwargs["fold"] = fold - - return dt.__class__(*local_time(sec, 0, dt.microsecond), **kwargs) - - def _convert(self, dt): # type: (_D) -> _D - if dt.tzinfo is self: - return self._normalize(dt, dst_rule=POST_TRANSITION) - - if not isinstance(dt.tzinfo, Timezone): - return dt.astimezone(self) - - stamp = timestamp(dt) - - if isinstance(dt.tzinfo, FixedTimezone): - offset = dt.tzinfo.offset - else: - transition = dt.tzinfo._lookup_transition(stamp) - offset = transition.ttype.offset - - if stamp < transition.local and transition.previous is not None: - if ( - transition.previous.is_ambiguous(stamp) - and getattr(dt, "fold", 1) == 0 - ): - pass - else: - offset = transition.previous.ttype.offset - - stamp -= offset - - transition = self._lookup_transition(stamp, is_utc=True) - if stamp < transition.at and transition.previous is not None: - transition = transition.previous - - offset = transition.ttype.offset - stamp += offset - fold = int(not transition.ttype.is_dst()) - - kwargs = {"tzinfo": self} - - if _HAS_FOLD or isinstance(dt, pendulum.DateTime): - kwargs["fold"] = fold - - return dt.__class__(*local_time(stamp, 0, dt.microsecond), **kwargs) - - def _lookup_transition( - self, stamp, is_utc=False - ): # type: (int, bool) -> Transition - lo, hi = 0, len(self._transitions) - hint = self._hint[is_utc] - if hint: - if stamp == hint[0]: - return self._transitions[hint[1]] - elif stamp < hint[0]: - hi = hint[1] - else: - lo = hint[1] - - if not is_utc: - while lo < hi: - mid = (lo + hi) // 2 - if stamp < self._transitions[mid].to: - hi = mid - else: - lo = mid + 1 - else: - while lo < hi: - mid = (lo + hi) // 2 - if stamp < self._transitions[mid].at: - hi = mid - else: - lo = mid + 1 - - if lo >= len(self._transitions): - # Beyond last transition - lo = len(self._transitions) - 1 - - self._hint[is_utc] = (stamp, lo) - - return self._transitions[lo] - - @overload - def utcoffset(self, dt): # type: (None) -> None - pass - - @overload - def utcoffset(self, dt): # type: (_datetime) -> timedelta - pass - - def utcoffset(self, dt): - if dt is None: - return - - transition = self._get_transition(dt) - - return transition.utcoffset() - - def dst( - self, dt # type: Optional[_datetime] - ): # type: (...) -> Optional[timedelta] - if dt is None: - return - - transition = self._get_transition(dt) - - if not transition.ttype.is_dst(): - return timedelta() - - return timedelta(seconds=transition.fix) - - def tzname(self, dt): # type: (Optional[_datetime]) -> Optional[str] - if dt is None: - return - - transition = self._get_transition(dt) - - return transition.ttype.abbreviation - - def _get_transition(self, dt): # type: (_datetime) -> Transition - if dt.tzinfo is not None and dt.tzinfo is not self: - dt = dt - dt.utcoffset() - - stamp = timestamp(dt) - - transition = self._lookup_transition(stamp, is_utc=True) - else: - stamp = timestamp(dt) - - transition = self._lookup_transition(stamp) - - if stamp < transition.local and transition.previous is not None: - fold = getattr(dt, "fold", 1) - if transition.is_ambiguous(stamp): - if fold == 0: - transition = transition.previous - elif transition.previous.is_ambiguous(stamp) and fold == 0: - pass - else: - transition = transition.previous - - return transition - - def fromutc(self, dt): # type: (_D) -> _D - stamp = timestamp(dt) - - transition = self._lookup_transition(stamp, is_utc=True) - if stamp < transition.at and transition.previous is not None: - transition = transition.previous - - stamp += transition.ttype.offset - - return dt.__class__(*local_time(stamp, 0, dt.microsecond), tzinfo=self) - - def __repr__(self): # type: () -> str - return "Timezone('{}')".format(self._name) - - def __getinitargs__(self): # type: () -> tuple - return (self._name,) - - -class FixedTimezone(Timezone): - def __init__(self, offset, name=None): - sign = "-" if offset < 0 else "+" - - minutes = offset / 60 - hour, minute = divmod(abs(int(minutes)), 60) - - if not name: - name = "{0}{1:02d}:{2:02d}".format(sign, hour, minute) - - self._name = name - self._offset = offset - self._utcoffset = timedelta(seconds=offset) - - @property - def offset(self): # type: () -> int - return self._offset - - def _normalize(self, dt, dst_rule=None): # type: (_D, Optional[str]) -> _D - if _HAS_FOLD: - dt = dt.__class__( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - tzinfo=self, - fold=0, - ) - else: - dt = dt.__class__( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - tzinfo=self, - ) - - return dt - - def _convert(self, dt): # type: (_D) -> _D - if dt.tzinfo is not self: - return dt.astimezone(self) - - return dt - - def utcoffset(self, dt): # type: (Optional[_datetime]) -> timedelta - return self._utcoffset - - def dst(self, dt): # type: (Optional[_datetime]) -> timedelta - return timedelta() - - def fromutc(self, dt): # type: (_D) -> _D - # Use the stdlib datetime's add method to avoid infinite recursion - return (datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self) - - def tzname(self, dt): # type: (Optional[_datetime]) -> Optional[str] - return self._name - - def __getinitargs__(self): # type: () -> tuple - return self._offset, self._name - - -class TimezoneFile(Timezone): - def __init__(self, path): - tz = read_file(path) - - self._name = "" - self._transitions = tz.transitions - self._hint = {True: None, False: None} - - -UTC = FixedTimezone(0, "UTC") diff --git a/pendulum/tz/zoneinfo/__init__.py b/pendulum/tz/zoneinfo/__init__.py deleted file mode 100644 index c1833650..00000000 --- a/pendulum/tz/zoneinfo/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .reader import Reader -from .timezone import Timezone - - -def read(name, extend=True): # type: (str, bool) -> Timezone - """ - Read the zoneinfo structure for a given timezone name. - """ - return Reader(extend=extend).read_for(name) - - -def read_file(path, extend=True): # type: (str, bool) -> Timezone - """ - Read the zoneinfo structure for a given path. - """ - return Reader(extend=extend).read(path) diff --git a/pendulum/tz/zoneinfo/exceptions.py b/pendulum/tz/zoneinfo/exceptions.py deleted file mode 100644 index 54121815..00000000 --- a/pendulum/tz/zoneinfo/exceptions.py +++ /dev/null @@ -1,18 +0,0 @@ -class ZoneinfoError(Exception): - - pass - - -class InvalidZoneinfoFile(ZoneinfoError): - - pass - - -class InvalidTimezone(ZoneinfoError): - def __init__(self, name): - super(InvalidTimezone, self).__init__('Invalid timezone "{}"'.format(name)) - - -class InvalidPosixSpec(ZoneinfoError): - def __init__(self, spec): - super(InvalidPosixSpec, self).__init__("Invalid POSIX spec: {}".format(spec)) diff --git a/pendulum/tz/zoneinfo/posix_timezone.py b/pendulum/tz/zoneinfo/posix_timezone.py deleted file mode 100644 index 74a32eba..00000000 --- a/pendulum/tz/zoneinfo/posix_timezone.py +++ /dev/null @@ -1,270 +0,0 @@ -""" -Parsing of a POSIX zone spec as described in the TZ part of section 8.3 in -http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html. -""" -import re - -from typing import Optional - -from pendulum.constants import MONTHS_OFFSETS -from pendulum.constants import SECS_PER_DAY - -from .exceptions import InvalidPosixSpec - - -_spec = re.compile( - "^" - r"(?P<.*?>|[^-+,\d]{3,})" - r"(?P([+-])?(\d{1,2})(:\d{2}(:\d{2})?)?)" - r"(?P" - r" (?P<.*?>|[^-+,\d]{3,})" - r" (?P([+-])?(\d{1,2})(:\d{2}(:\d{2})?)?)?" - r")?" - r"(?:,(?P" - r" (?P" - r" (?:J\d+|\d+|M\d{1,2}.\d.[0-6])" - r" (?:/(?P([+-])?(\d+)(:\d{2}(:\d{2})?)?))?" - " )" - " ," - r" (?P" - r" (?:J\d+|\d+|M\d{1,2}.\d.[0-6])" - r" (?:/(?P([+-])?(\d+)(:\d{2}(:\d{2})?)?))?" - " )" - "))?" - "$", - re.VERBOSE, -) - - -def posix_spec(spec): # type: (str) -> PosixTimezone - try: - return _posix_spec(spec) - except ValueError: - raise InvalidPosixSpec(spec) - - -def _posix_spec(spec): # type: (str) -> PosixTimezone - m = _spec.match(spec) - if not m: - raise ValueError("Invalid posix spec") - - std_abbr = _parse_abbr(m.group("std_abbr")) - std_offset = _parse_offset(m.group("std_offset")) - - dst_abbr = None - dst_offset = None - if m.group("dst_info"): - dst_abbr = _parse_abbr(m.group("dst_abbr")) - if m.group("dst_offset"): - dst_offset = _parse_offset(m.group("dst_offset")) - else: - dst_offset = std_offset + 3600 - - dst_start = None - dst_end = None - if m.group("rules"): - dst_start = _parse_rule(m.group("dst_start")) - dst_end = _parse_rule(m.group("dst_end")) - - return PosixTimezone(std_abbr, std_offset, dst_abbr, dst_offset, dst_start, dst_end) - - -def _parse_abbr(text): # type: (str) -> str - return text.lstrip("<").rstrip(">") - - -def _parse_offset(text, sign=-1): # type: (str, int) -> int - if text.startswith(("+", "-")): - if text.startswith("-"): - sign *= -1 - - text = text[1:] - - minutes = 0 - seconds = 0 - - parts = text.split(":") - hours = int(parts[0]) - - if len(parts) > 1: - minutes = int(parts[1]) - - if len(parts) > 2: - seconds = int(parts[2]) - - return sign * ((((hours * 60) + minutes) * 60) + seconds) - - -def _parse_rule(rule): # type: (str) -> PosixTransition - klass = NPosixTransition - args = () - - if rule.startswith("M"): - rule = rule[1:] - parts = rule.split(".") - month = int(parts[0]) - week = int(parts[1]) - day = int(parts[2].split("/")[0]) - - args += (month, week, day) - klass = MPosixTransition - elif rule.startswith("J"): - rule = rule[1:] - args += (int(rule.split("/")[0]),) - klass = JPosixTransition - else: - args += (int(rule.split("/")[0]),) - - # Checking offset - parts = rule.split("/") - if len(parts) > 1: - offset = _parse_offset(parts[-1], sign=1) - else: - offset = 7200 - - args += (offset,) - - return klass(*args) - - -class PosixTransition(object): - def __init__(self, offset): # type: (int) -> None - self._offset = offset - - @property - def offset(self): # type: () -> int - return self._offset - - def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int - raise NotImplementedError() - - -class JPosixTransition(PosixTransition): - def __init__(self, day, offset): # type: (int, int) -> None - self._day = day - - super(JPosixTransition, self).__init__(offset) - - @property - def day(self): # type: () -> int - """ - day of non-leap year [1:365] - """ - return self._day - - def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int - days = self._day - if not is_leap or days < MONTHS_OFFSETS[1][3]: - days -= 1 - - return (days * SECS_PER_DAY) + self._offset - - -class NPosixTransition(PosixTransition): - def __init__(self, day, offset): # type: (int, int) -> None - self._day = day - - super(NPosixTransition, self).__init__(offset) - - @property - def day(self): # type: () -> int - """ - day of year [0:365] - """ - return self._day - - def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int - days = self._day - - return (days * SECS_PER_DAY) + self._offset - - -class MPosixTransition(PosixTransition): - def __init__(self, month, week, weekday, offset): - # type: (int, int, int, int) -> None - self._month = month - self._week = week - self._weekday = weekday - - super(MPosixTransition, self).__init__(offset) - - @property - def month(self): # type: () -> int - """ - month of year [1:12] - """ - return self._month - - @property - def week(self): # type: () -> int - """ - week of month [1:5] (5==last) - """ - return self._week - - @property - def weekday(self): # type: () -> int - """ - 0==Sun, ..., 6=Sat - """ - return self._weekday - - def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int - last_week = self._week == 5 - days = MONTHS_OFFSETS[is_leap][self._month + int(last_week)] - weekday = (jan1_weekday + days) % 7 - if last_week: - days -= (weekday + 7 - 1 - self._weekday) % 7 + 1 - else: - days += (self._weekday + 7 - weekday) % 7 - days += (self._week - 1) * 7 - - return (days * SECS_PER_DAY) + self._offset - - -class PosixTimezone: - """ - The entirety of a POSIX-string specified time-zone rule. - - The standard abbreviation and offset are always given. - """ - - def __init__( - self, - std_abbr, # type: str - std_offset, # type: int - dst_abbr, # type: Optional[str] - dst_offset, # type: Optional[int] - dst_start=None, # type: Optional[PosixTransition] - dst_end=None, # type: Optional[PosixTransition] - ): - self._std_abbr = std_abbr - self._std_offset = std_offset - self._dst_abbr = dst_abbr - self._dst_offset = dst_offset - self._dst_start = dst_start - self._dst_end = dst_end - - @property - def std_abbr(self): # type: () -> str - return self._std_abbr - - @property - def std_offset(self): # type: () -> int - return self._std_offset - - @property - def dst_abbr(self): # type: () -> Optional[str] - return self._dst_abbr - - @property - def dst_offset(self): # type: () -> Optional[int] - return self._dst_offset - - @property - def dst_start(self): # type: () -> Optional[PosixTransition] - return self._dst_start - - @property - def dst_end(self): # type: () -> Optional[PosixTransition] - return self._dst_end diff --git a/pendulum/tz/zoneinfo/reader.py b/pendulum/tz/zoneinfo/reader.py deleted file mode 100644 index f4c1fa60..00000000 --- a/pendulum/tz/zoneinfo/reader.py +++ /dev/null @@ -1,224 +0,0 @@ -import os - -from collections import namedtuple -from struct import unpack -from typing import IO -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Tuple - -import pytzdata - -from pytzdata.exceptions import TimezoneNotFound - -from pendulum.utils._compat import PY2 - -from .exceptions import InvalidTimezone -from .exceptions import InvalidZoneinfoFile -from .posix_timezone import PosixTimezone -from .posix_timezone import posix_spec -from .timezone import Timezone -from .transition import Transition -from .transition_type import TransitionType - - -_offset = namedtuple("offset", "utc_total_offset is_dst abbr_idx") - -header = namedtuple( - "header", - "version " "utclocals " "stdwalls " "leaps " "transitions " "types " "abbr_size", -) - - -class Reader: - """ - Reads compiled zoneinfo TZif (\0, 2 or 3) files. - """ - - def __init__(self, extend=True): # type: (bool) -> None - self._extend = extend - - def read_for(self, timezone): # type: (str) -> Timezone - """ - Read the zoneinfo structure for a given timezone name. - - :param timezone: The timezone. - """ - try: - file_path = pytzdata.tz_path(timezone) - except TimezoneNotFound: - raise InvalidTimezone(timezone) - - return self.read(file_path) - - def read(self, file_path): # type: (str) -> Timezone - """ - Read a zoneinfo structure from the given path. - - :param file_path: The path of a zoneinfo file. - """ - if not os.path.exists(file_path): - raise InvalidZoneinfoFile("The tzinfo file does not exist") - - with open(file_path, "rb") as fd: - return self._parse(fd) - - def _check_read(self, fd, nbytes): # type: (...) -> bytes - """ - Reads the given number of bytes from the given file - and checks that the correct number of bytes could be read. - """ - result = fd.read(nbytes) - - if (not result and nbytes > 0) or len(result) != nbytes: - raise InvalidZoneinfoFile( - "Expected {} bytes reading {}, " - "but got {}".format(nbytes, fd.name, len(result) if result else 0) - ) - - if PY2: - return bytearray(result) - - return result - - def _parse(self, fd): # type: (...) -> Timezone - """ - Parse a zoneinfo file. - """ - hdr = self._parse_header(fd) - - if hdr.version in (2, 3): - # We're skipping the entire v1 file since - # at least the same data will be found in TZFile 2. - fd.seek( - hdr.transitions * 5 - + hdr.types * 6 - + hdr.abbr_size - + hdr.leaps * 4 - + hdr.stdwalls - + hdr.utclocals, - 1, - ) - - # Parse the second header - hdr = self._parse_header(fd) - - if hdr.version != 2 and hdr.version != 3: - raise InvalidZoneinfoFile( - "Header versions mismatch for file {}".format(fd.name) - ) - - # Parse the v2 data - trans = self._parse_trans_64(fd, hdr.transitions) - type_idx = self._parse_type_idx(fd, hdr.transitions) - types = self._parse_types(fd, hdr.types) - abbrs = self._parse_abbrs(fd, hdr.abbr_size, types) - - fd.seek(hdr.leaps * 8 + hdr.stdwalls + hdr.utclocals, 1) - - trule = self._parse_posix_tz(fd) - else: - # TZFile v1 - trans = self._parse_trans_32(fd, hdr.transitions) - type_idx = self._parse_type_idx(fd, hdr.transitions) - types = self._parse_types(fd, hdr.types) - abbrs = self._parse_abbrs(fd, hdr.abbr_size, types) - trule = None - - types = [ - TransitionType(off, is_dst, abbrs[abbr]) for off, is_dst, abbr in types - ] - - transitions = [] - previous = None - for trans, idx in zip(trans, type_idx): - transition = Transition(trans, types[idx], previous) - transitions.append(transition) - - previous = transition - - if not transitions: - transitions.append(Transition(0, types[0], None)) - - return Timezone(transitions, posix_rule=trule, extended=self._extend) - - def _parse_header(self, fd): # type: (...) -> header - buff = self._check_read(fd, 44) - - if buff[:4] != b"TZif": - raise InvalidZoneinfoFile( - 'The file "{}" has an invalid header.'.format(fd.name) - ) - - version = {0x00: 1, 0x32: 2, 0x33: 3}.get(buff[4]) - - if version is None: - raise InvalidZoneinfoFile( - 'The file "{}" has an invalid version.'.format(fd.name) - ) - - hdr = header(version, *unpack(">6l", buff[20:44])) - - return hdr - - def _parse_trans_64(self, fd, n): # type: (IO[Any], int) -> List[int] - trans = [] - for _ in range(n): - buff = self._check_read(fd, 8) - trans.append(unpack(">q", buff)[0]) - - return trans - - def _parse_trans_32(self, fd, n): # type: (IO[Any], int) -> List[int] - trans = [] - for _ in range(n): - buff = self._check_read(fd, 4) - trans.append(unpack(">i", buff)[0]) - - return trans - - def _parse_type_idx(self, fd, n): # type: (IO[Any], int) -> List[int] - buff = self._check_read(fd, n) - - return list(unpack("{}B".format(n), buff)) - - def _parse_types( - self, fd, n - ): # type: (IO[Any], int) -> List[Tuple[Any, bool, int]] - types = [] - - for _ in range(n): - buff = self._check_read(fd, 6) - offset = unpack(">l", buff[:4])[0] - is_dst = buff[4] == 1 - types.append((offset, is_dst, buff[5])) - - return types - - def _parse_abbrs( - self, fd, n, types - ): # type: (IO[Any], int, List[Tuple[Any, bool, int]]) -> Dict[int, str] - abbrs = {} - buff = self._check_read(fd, n) - - for offset, is_dst, idx in types: - if idx not in abbrs: - abbr = buff[idx : buff.find(b"\0", idx)].decode("utf-8") - abbrs[idx] = abbr - - return abbrs - - def _parse_posix_tz(self, fd): # type: (...) -> Optional[PosixTimezone] - s = fd.read().decode("utf-8") - - if not s.startswith("\n") or not s.endswith("\n"): - raise InvalidZoneinfoFile('Invalid posix rule in file "{}"'.format(fd.name)) - - s = s.strip() - - if not s: - return - - return posix_spec(s) diff --git a/pendulum/tz/zoneinfo/timezone.py b/pendulum/tz/zoneinfo/timezone.py deleted file mode 100644 index abdb0ec4..00000000 --- a/pendulum/tz/zoneinfo/timezone.py +++ /dev/null @@ -1,128 +0,0 @@ -from datetime import datetime -from typing import List -from typing import Optional - -from pendulum.constants import DAYS_PER_YEAR -from pendulum.constants import SECS_PER_YEAR -from pendulum.helpers import is_leap -from pendulum.helpers import local_time -from pendulum.helpers import timestamp -from pendulum.helpers import week_day - -from .posix_timezone import PosixTimezone -from .transition import Transition -from .transition_type import TransitionType - - -class Timezone: - def __init__( - self, - transitions, # type: List[Transition] - posix_rule=None, # type: Optional[PosixTimezone] - extended=True, # type: bool - ): - self._posix_rule = posix_rule - self._transitions = transitions - - if extended: - self._extends() - - @property - def transitions(self): # type: () -> List[Transition] - return self._transitions - - @property - def posix_rule(self): - return self._posix_rule - - def _extends(self): - if not self._posix_rule: - return - - posix = self._posix_rule - - if not posix.dst_abbr: - # std only - # The future specification should match the last/default transition - ttype = self._transitions[-1].ttype - if not self._check_ttype(ttype, posix.std_offset, False, posix.std_abbr): - raise ValueError("Posix spec does not match last transition") - - return - - if len(self._transitions) < 2: - raise ValueError("Too few transitions for POSIX spec") - - # Extend the transitions for an additional 400 years - # using the future specification - - # The future specification should match the last two transitions, - # and those transitions should have different is_dst flags. - tr0 = self._transitions[-1] - tr1 = self._transitions[-2] - tt0 = tr0.ttype - tt1 = tr1.ttype - if tt0.is_dst(): - dst = tt0 - std = tt1 - else: - dst = tt1 - std = tt0 - - self._check_ttype(dst, posix.dst_offset, True, posix.dst_abbr) - self._check_ttype(std, posix.std_offset, False, posix.std_abbr) - - # Add the transitions to tr1 and back to tr0 for each extra year. - last_year = local_time(tr0.local, 0, 0)[0] - leap_year = is_leap(last_year) - jan1 = datetime(last_year, 1, 1) - jan1_time = timestamp(jan1) - jan1_weekday = week_day(jan1.year, jan1.month, jan1.day) % 7 - - if local_time(tr1.local, 0, 0)[0] != last_year: - # Add a single extra transition to align to a calendar year. - if tt0.is_dst(): - pt1 = posix.dst_end - else: - pt1 = posix.dst_start - - tr1_offset = pt1.trans_offset(leap_year, jan1_weekday) - tr = Transition(jan1_time + tr1_offset - tt0.offset, tr1.ttype, tr0) - tr0 = tr - tr1 = tr0 - tt0 = tr0.ttype - tt1 = tr1.ttype - - if tt0.is_dst(): - pt1 = posix.dst_end - pt0 = posix.dst_start - else: - pt1 = posix.dst_start - pt0 = posix.dst_end - - tr = tr0 - for year in range(last_year + 1, last_year + 401): - jan1_time += SECS_PER_YEAR[leap_year] - jan1_weekday = (jan1_weekday + DAYS_PER_YEAR[leap_year]) % 7 - leap_year = not leap_year and is_leap(year) - - tr1_offset = pt1.trans_offset(leap_year, jan1_weekday) - tr = Transition(jan1_time + tr1_offset - tt0.offset, tt1, tr) - self._transitions.append(tr) - - tr0_offset = pt0.trans_offset(leap_year, jan1_weekday) - tr = Transition(jan1_time + tr0_offset - tt1.offset, tt0, tr) - self._transitions.append(tr) - - def _check_ttype( - self, - ttype, # type: TransitionType - offset, # type: int - is_dst, # type: bool - abbr, # type: str - ): # type: (...) -> bool - return ( - ttype.offset == offset - and ttype.is_dst() == is_dst - and ttype.abbreviation == abbr - ) diff --git a/pendulum/tz/zoneinfo/transition.py b/pendulum/tz/zoneinfo/transition.py deleted file mode 100644 index dcbd5d31..00000000 --- a/pendulum/tz/zoneinfo/transition.py +++ /dev/null @@ -1,77 +0,0 @@ -from datetime import timedelta -from typing import Optional - -from .transition_type import TransitionType - - -class Transition: - def __init__( - self, - at, # type: int - ttype, # type: TransitionType - previous, # type: Optional[Transition] - ): - self._at = at - - if previous: - self._local = at + previous.ttype.offset - else: - self._local = at + ttype.offset - - self._ttype = ttype - self._previous = previous - - if self.previous: - self._fix = self._ttype.offset - self.previous.ttype.offset - else: - self._fix = 0 - - self._to = self._local + self._fix - self._to_utc = self._at + self._fix - self._utcoffset = timedelta(seconds=ttype.offset) - - @property - def at(self): # type: () -> int - return self._at - - @property - def local(self): # type: () -> int - return self._local - - @property - def to(self): # type: () -> int - return self._to - - @property - def to_utc(self): # type: () -> int - return self._to - - @property - def ttype(self): # type: () -> TransitionType - return self._ttype - - @property - def previous(self): # type: () -> Optional[Transition] - return self._previous - - @property - def fix(self): # type: () -> int - return self._fix - - def is_ambiguous(self, stamp): # type: (int) -> bool - return self._to <= stamp < self._local - - def is_missing(self, stamp): # type: (int) -> bool - return self._local <= stamp < self._to - - def utcoffset(self): # type: () -> timedelta - return self._utcoffset - - def __contains__(self, stamp): # type: (int) -> bool - if self.previous is None: - return stamp < self.local - - return self.previous.local <= stamp < self.local - - def __repr__(self): # type: () -> str - return "Transition({} -> {}, {})".format(self._local, self._to, self._ttype) diff --git a/pendulum/tz/zoneinfo/transition_type.py b/pendulum/tz/zoneinfo/transition_type.py deleted file mode 100644 index c2c33c67..00000000 --- a/pendulum/tz/zoneinfo/transition_type.py +++ /dev/null @@ -1,35 +0,0 @@ -from datetime import timedelta - -from pendulum.utils._compat import PY2 -from pendulum.utils._compat import encode - - -class TransitionType: - def __init__(self, offset, is_dst, abbr): - self._offset = offset - self._is_dst = is_dst - self._abbr = abbr - - self._utcoffset = timedelta(seconds=offset) - - @property - def offset(self): # type: () -> int - return self._offset - - @property - def abbreviation(self): # type: () -> str - if PY2: - return encode(self._abbr) - - return self._abbr - - def is_dst(self): # type: () -> bool - return self._is_dst - - def utcoffset(self): # type: () -> timedelta - return self._utcoffset - - def __repr__(self): # type: () -> str - return "TransitionType({}, {}, {})".format( - self._offset, self._is_dst, self._abbr - ) diff --git a/pendulum/utils/_compat.py b/pendulum/utils/_compat.py deleted file mode 100644 index 4893979b..00000000 --- a/pendulum/utils/_compat.py +++ /dev/null @@ -1,54 +0,0 @@ -import sys - - -PY2 = sys.version_info < (3, 0) -PY36 = sys.version_info >= (3, 6) -PYPY = hasattr(sys, "pypy_version_info") - -_HAS_FOLD = PY36 - - -try: # Python 2 - long = long - unicode = unicode - basestring = basestring -except NameError: # Python 3 - long = int - unicode = str - basestring = str - - -def decode(string, encodings=None): - if not PY2 and not isinstance(string, bytes): - return string - - if PY2 and isinstance(string, unicode): - return string - - encodings = encodings or ["utf-8", "latin1", "ascii"] - - for encoding in encodings: - try: - return string.decode(encoding) - except (UnicodeEncodeError, UnicodeDecodeError): - pass - - return string.decode(encodings[0], errors="ignore") - - -def encode(string, encodings=None): - if not PY2 and isinstance(string, bytes): - return string - - if PY2 and isinstance(string, str): - return string - - encodings = encodings or ["utf-8", "latin1", "ascii"] - - for encoding in encodings: - try: - return string.encode(encoding) - except (UnicodeEncodeError, UnicodeDecodeError): - pass - - return string.encode(encodings[0], errors="ignore") diff --git a/poetry.lock b/poetry.lock index b9120f42..4c80164b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,1128 +1,1338 @@ -[[package]] -category = "dev" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -name = "appdirs" -optional = false -python-versions = "*" -version = "1.4.4" - -[[package]] -category = "dev" -description = "A few extensions to pyyaml." -name = "aspy.yaml" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.3.0" - -[package.dependencies] -pyyaml = "*" - -[[package]] -category = "dev" -description = "Atomic file writes." -name = "atomicwrites" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.0" - -[[package]] -category = "dev" -description = "Classes Without Boilerplate" -name = "attrs" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.3.0" - -[package.extras] -azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] -dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] -docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] -category = "dev" -description = "Internationalization utilities" name = "babel" +version = "2.16.0" +description = "Internationalization utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.8.0" - -[package.dependencies] -pytz = ">=2015.7" - -[[package]] -category = "dev" -description = "Backport of functools.lru_cache" -name = "backports.functools-lru-cache" -optional = false -python-versions = ">=2.6" -version = "1.6.1" +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov"] - -[[package]] -category = "dev" -description = "The uncompromising code formatter." -name = "black" -optional = false -python-versions = ">=3.6" -version = "19.10b0" +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "cachetools" +version = "5.5.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +groups = ["benchmark"] +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] [package.dependencies] -appdirs = "*" -attrs = ">=18.1.0" -click = ">=6.5" -pathspec = ">=0.6,<1" -regex = "*" -toml = ">=0.9.4" -typed-ast = ">=1.4.0" - -[package.extras] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +pycparser = "*" [[package]] -category = "dev" -description = "Validate configuration and produce human readable error messages." name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.1" +python-versions = ">=3.8" +groups = ["lint"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] -[package.dependencies] -six = "*" +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] [[package]] -category = "dev" -description = "Cleo allows you to create beautiful and testable command-line interfaces." name = "cleo" +version = "2.1.0" +description = "Cleo allows you to create beautiful and testable command-line interfaces." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.8.1" +python-versions = ">=3.7,<4.0" +groups = ["dev"] +markers = "python_version < \"4.0\"" +files = [ + {file = "cleo-2.1.0-py3-none-any.whl", hash = "sha256:4a31bd4dd45695a64ee3c4758f583f134267c2bc518d8ae9a29cf237d009b07e"}, + {file = "cleo-2.1.0.tar.gz", hash = "sha256:0b2c880b5d13660a7ea651001fb4acb527696c01f15c9ee650f377aa543fd523"}, +] [package.dependencies] -clikit = ">=0.6.0,<0.7.0" +crashtest = ">=0.4.1,<0.5.0" +rapidfuzz = ">=3.0.0,<4.0.0" [[package]] -category = "dev" -description = "Composable command line interface toolkit" name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.2" - -[[package]] -category = "dev" -description = "CliKit is a group of utilities to build beautiful and testable command line interfaces." -name = "clikit" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.6.2" +python-versions = ">=3.7" +groups = ["doc"] +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] -pastel = ">=0.2.0,<0.3.0" -pylev = ">=1.3,<2.0" -crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} -enum34 = {version = ">=1.1,<2.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} -typing = {version = ">=3.6,<4.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\""} -typing-extensions = {version = ">=3.6,<4.0", markers = "python_version >= \"3.5\" and python_full_version < \"3.5.4\""} +colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] -category = "dev" -description = "Cross-platform colored terminal text." name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.3" - -[[package]] -category = "dev" -description = "Updated configparser from Python 3.7 for Python 2.6+." -name = "configparser" -optional = false -python-versions = ">=2.6" -version = "4.0.2" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytest-flake8", "pytest-black-multipy"] - -[[package]] -category = "dev" -description = "Backports and enhancements for the contextlib module" -name = "contextlib2" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.6.0.post1" - -[[package]] -category = "dev" -description = "Code coverage measurement for Python" -name = "coverage" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.2" - -[package.extras] -toml = ["toml"] +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["benchmark", "dev", "doc", "test"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {benchmark = "sys_platform == \"win32\"", doc = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""} [[package]] -category = "dev" -description = "Manage Python errors with ease" name = "crashtest" +version = "0.4.1" +description = "Manage Python errors with ease" optional = false -python-versions = ">=3.6,<4.0" -version = "0.3.0" +python-versions = ">=3.7,<4.0" +groups = ["dev"] +markers = "python_version < \"4.0\"" +files = [ + {file = "crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5"}, + {file = "crashtest-0.4.1.tar.gz", hash = "sha256:80d7b1f316ebfbd429f648076d6275c877ba30ba48979de4191714a75266f0ce"}, +] [[package]] -category = "dev" -description = "Distribution utilities" name = "distlib" +version = "0.3.9" +description = "Distribution utilities" optional = false python-versions = "*" -version = "0.3.1" +groups = ["dev", "lint"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] [[package]] -category = "dev" -description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4" -name = "enum34" +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" optional = false -python-versions = "*" -version = "1.1.10" +python-versions = ">=3.7" +groups = ["benchmark", "test"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] -[[package]] -category = "dev" -description = "A platform independent file lock." -name = "filelock" -optional = false -python-versions = "*" -version = "3.0.12" +[package.extras] +test = ["pytest (>=6)"] [[package]] -category = "dev" -description = "Let your Python tests travel through time" -name = "freezegun" +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.3.15" +python-versions = ">=3.8" +groups = ["dev", "lint"] +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] -[package.dependencies] -python-dateutil = ">=1.0,<2.0 || >2.0" -six = "*" +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] -category = "dev" -description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" -name = "funcsigs" +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." optional = false python-versions = "*" -version = "1.0.2" +groups = ["doc"] +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] -[[package]] -category = "dev" -description = "Backport of the concurrent.futures package from Python 3.2" -name = "futures" -optional = false -python-versions = "*" -version = "3.1.1" +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] [[package]] -category = "dev" -description = "File identification library for Python" name = "identify" +version = "2.6.3" +description = "File identification library for Python" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "1.4.23" +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, +] [package.extras] -license = ["editdistance"] +license = ["ukkonen"] [[package]] -category = "dev" -description = "Read metadata from Python packages" -name = "importlib-metadata" +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.7.0" - -[package.dependencies] -zipp = ">=0.5" -configparser = {version = ">=3.5", markers = "python_version < \"3\""} -contextlib2 = {version = "*", markers = "python_version < \"3\""} -pathlib2 = {version = "*", markers = "python_version < \"3\""} - -[package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] +python-versions = ">=3.7" +groups = ["benchmark", "test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] [[package]] -category = "dev" -description = "Read resources from Python packages" -name = "importlib-resources" +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "3.0.0" +python-versions = ">=3.7" +groups = ["doc"] +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] [package.dependencies] -contextlib2 = {version = "*", markers = "python_version < \"3\""} -pathlib2 = {version = "*", markers = "python_version < \"3\""} -singledispatch = {version = "*", markers = "python_version < \"3.4\""} -typing = {version = "*", markers = "python_version < \"3.5\""} -zipp = {version = ">=0.4", markers = "python_version < \"3.8\""} +MarkupSafe = ">=2.0" [package.extras] -docs = ["sphinx", "rst.linker", "jaraco.packaging"] +i18n = ["Babel (>=2.7)"] [[package]] -category = "dev" -description = "A Python utility / library to sort Python imports." -name = "isort" +name = "markdown" +version = "3.7" +description = "Python implementation of John Gruber's Markdown." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "4.3.21" +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, +] [package.extras] -pipfile = ["pipreqs", "requirementslib"] -pyproject = ["toml"] -requirements = ["pipreqs", "pip-api"] -xdg_home = ["appdirs (>=1.4.0)"] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] [[package]] -category = "dev" -description = "A very fast and expressive template engine." -name = "jinja2" +name = "markdown-include" +version = "0.8.1" +description = "A Python-Markdown extension which provides an 'include' function" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.11.2" +python-versions = ">=3.7" +groups = ["doc"] +files = [ + {file = "markdown-include-0.8.1.tar.gz", hash = "sha256:1d0623e0fc2757c38d35df53752768356162284259d259c486b4ab6285cdbbe3"}, + {file = "markdown_include-0.8.1-py3-none-any.whl", hash = "sha256:32f0635b9cfef46997b307e2430022852529f7a5b87c0075c504283e7cc7db53"}, +] [package.dependencies] -MarkupSafe = ">=0.23" +markdown = ">=3.0" [package.extras] -i18n = ["Babel (>=0.8)"] +tests = ["pytest"] [[package]] -category = "dev" -description = "Python LiveReload is an awesome tool for web developers" -name = "livereload" +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" optional = false -python-versions = "*" -version = "2.6.2" +python-versions = ">=3.8" +groups = ["benchmark"] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] [package.dependencies] -six = "*" -tornado = {version = "*", markers = "python_version > \"2.7\""} +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] -category = "dev" -description = "Python implementation of Markdown." -name = "markdown" +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "3.1.1" +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "maturin" +version = "1.7.6" +description = "Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages" +optional = false +python-versions = ">=3.7" +groups = ["build"] +files = [ + {file = "maturin-1.7.6-py3-none-linux_armv6l.whl", hash = "sha256:8c23309b75624cf4dc76682bbfe587ce42c9ba595bdc954c1c0b35ef3869470e"}, + {file = "maturin-1.7.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:85eb76c502f3d9923371623fa153f67afc07b81aa3a28a2620340564bf521e6a"}, + {file = "maturin-1.7.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:37f42a6e15cd49e12a13475b105239e1da20763d50213d541ad56c78d900df9d"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:f64b3a30f3af59fbdbeba980508c7a8294b5f5202a292f41800d22cb8ab69238"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:41d3f0af4a15ee328aa16ba5581f1bfdf0ad88f2a3e1ee9ebf77d2fe269d05af"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:41395b4b4d8c35fb2c86143bc3a8808024076a60ed72bfa0002f032f2913ee3d"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:534c0663c10b590f9c1de8c49f06c0d7da7e1d3078f3975b0191b139a73f051b"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:517a0b469199fab8a5e05a2f2477e156c90f80ed160e28e6ee42d5315c2c424b"}, + {file = "maturin-1.7.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44c39226a22c2c587e3b886890c76b6ba950ab0f7b129932f8f0498441d47981"}, + {file = "maturin-1.7.6-py3-none-win32.whl", hash = "sha256:8455cecb948c01ff20689a953a2fd034d4ef94f2bf256cf817beb12572e3051c"}, + {file = "maturin-1.7.6-py3-none-win_amd64.whl", hash = "sha256:84382c7a10d3c84cdfeb230d9b88f78fd99c2aebbd121fd8f04efc706ff65507"}, + {file = "maturin-1.7.6-py3-none-win_arm64.whl", hash = "sha256:cc5a14f42d6f2cf3eff944f2d00d0ce45fc6060d61e51aa8b8c407efbea4dea8"}, + {file = "maturin-1.7.6.tar.gz", hash = "sha256:18c3f192c0f48e820fe684c9b89cc099f0107fd93845d39d6001610e3b1b94c4"}, +] + +[package.dependencies] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["coverage", "pyyaml"] +patchelf = ["patchelf"] +zig = ["ziglang (>=0.10.0,<0.13.0)"] [[package]] -category = "dev" -description = "This is an extension to Python-Markdown which provides an \"include\" function, similar to that found in LaTeX (and also the C pre-processor and Fortran). I originally wrote it for my FORD Fortran auto-documentation generator." -name = "markdown-include" +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" optional = false -python-versions = "*" -version = "0.5.1" - -[package.dependencies] -markdown = "*" +python-versions = ">=3.7" +groups = ["benchmark"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] [[package]] -category = "dev" -description = "Safely add untrusted strings to HTML/XML markup." -name = "markupsafe" +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.1.1" +python-versions = ">=3.6" +groups = ["doc"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] [[package]] -category = "dev" -description = "Project documentation with Markdown." name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." optional = false -python-versions = ">=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.0.4" +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] [package.dependencies] -Jinja2 = ">=2.7.1" -Markdown = ">=2.3.1" -PyYAML = ">=3.10" -click = ">=3.3" -livereload = ">=2.5.1" -tornado = ">=5.0" +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] -category = "dev" -description = "More routines for operating on iterables, beyond itertools" -name = "more-itertools" +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" optional = false -python-versions = "*" -version = "5.0.0" +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] [package.dependencies] -six = ">=1.0.0,<2.0.0" +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +groups = ["typing"] +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] -[[package]] -category = "dev" -description = "Node.js virtual environment builder" -name = "nodeenv" -optional = false -python-versions = "*" -version = "1.4.0" +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] [[package]] -category = "dev" -description = "Core utilities for Python packages" -name = "packaging" +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.4" - -[package.dependencies] -pyparsing = ">=2.0.2" -six = "*" +python-versions = ">=3.5" +groups = ["typing"] +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]] -category = "dev" -description = "Bring colors to your terminal." -name = "pastel" +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.2.0" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["lint"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] [[package]] -category = "dev" -description = "Object-oriented filesystem paths" -name = "pathlib2" +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" optional = false -python-versions = "*" -version = "2.3.5" - -[package.dependencies] -six = "*" -scandir = {version = "*", markers = "python_version < \"3.5\""} +python-versions = ">=3.8" +groups = ["benchmark", "dev", "doc", "test"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] [[package]] -category = "dev" -description = "Utility library for gitignore style pattern matching of file paths." name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.8.0" +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] [[package]] -category = "dev" -description = "Backport of PEP 562." -name = "pep562" +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = "*" -version = "1.0" +python-versions = ">=3.8" +groups = ["dev", "doc", "lint"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] -category = "dev" -description = "plugin and hook calling mechanisms for python" name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.13.1" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +python-versions = ">=3.8" +groups = ["benchmark", "dev", "test"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] [package.extras] dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] -category = "dev" -description = "A framework for managing and maintaining multi-language pre-commit hooks." name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.21.0" +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] [package.dependencies] -"aspy.yaml" = "*" cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" -pyyaml = "*" -six = "*" -toml = "*" -virtualenv = ">=15.2" -futures = {version = "*", markers = "python_version < \"3.2\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -importlib-resources = {version = "*", markers = "python_version < \"3.7\""} +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" [[package]] -category = "dev" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -name = "py" +name = "py-cpuinfo" +version = "9.0.0" +description = "Get CPU info with pure Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.9.0" +python-versions = "*" +groups = ["test"] +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]] -category = "dev" -description = "Pygments is a syntax highlighting package written in Python." -name = "pygments" +name = "pycparser" +version = "2.22" +description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.5.2" +python-versions = ">=3.8" +groups = ["benchmark"] +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] [[package]] -category = "dev" -description = "A pure Python Levenshtein implementation that's not freaking GPL'd." -name = "pylev" +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = "*" -version = "1.3.0" +python-versions = ">=3.8" +groups = ["benchmark", "doc"] +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] -category = "dev" -description = "Extension pack for Python Markdown." name = "pymdown-extensions" +version = "10.12" +description = "Extension pack for Python Markdown." optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "6.2.1" +python-versions = ">=3.8" +groups = ["doc"] +files = [ + {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, + {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, +] [package.dependencies] -Markdown = ">=3.0.1" -pep562 = "*" +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] [[package]] -category = "dev" -description = "Python parsing module" -name = "pyparsing" +name = "pyproject-api" +version = "1.8.0" +description = "API to interact with the python pyproject.toml based projects" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.7" +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"}, + {file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"}, +] + +[package.dependencies] +packaging = ">=24.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] [[package]] -category = "dev" -description = "pytest: simple powerful testing with Python" name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "4.6.11" +python-versions = ">=3.7" +groups = ["benchmark", "test"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] [package.dependencies] -atomicwrites = ">=1.0" -attrs = ">=17.4.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -six = ">=1.10.0" -wcwidth = "*" -colorama = {version = "*", markers = "sys_platform == \"win32\" and python_version != \"3.4\""} -funcsigs = {version = ">=1.0", markers = "python_version < \"3.0\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -pathlib2 = {version = ">=2.2.0", markers = "python_version < \"3.6\""} - -[[package.dependencies.more-itertools]] -markers = "python_version <= \"2.7\"" -version = ">=4.0.0,<6.0.0" - -[[package.dependencies.more-itertools]] -markers = "python_version > \"2.7\"" -version = ">=4.0.0" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] -category = "dev" -description = "Pytest plugin for measuring coverage." -name = "pytest-cov" +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 = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.10.0" +python-versions = ">=3.7" +groups = ["test"] +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-codspeed" +version = "3.2.0" +description = "Pytest plugin to create CodSpeed benchmarks" +optional = false +python-versions = ">=3.9" +groups = ["benchmark"] +files = [ + {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5165774424c7ab8db7e7acdb539763a0e5657996effefdf0664d7fd95158d34"}, + {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bd55f92d772592c04a55209950c50880413ae46876e66bd349ef157075ca26c"}, + {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf6f56067538f4892baa8d7ab5ef4e45bb59033be1ef18759a2c7fc55b32035"}, + {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a687b05c3d145642061b45ea78e47e12f13ce510104d1a2cda00eee0e36f58"}, + {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46a1afaaa1ac4c2ca5b0700d31ac46d80a27612961d031067d73c6ccbd8d3c2b"}, + {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48ce3af3dfa78413ed3d69d1924043aa1519048dbff46edccf8f35a25dab3c2"}, + {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66692506d33453df48b36a84703448cb8b22953eea51f03fbb2eb758dc2bdc4f"}, + {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:479774f80d0bdfafa16112700df4dbd31bf2a6757fac74795fd79c0a7b3c389b"}, + {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:109f9f4dd1088019c3b3f887d003b7d65f98a7736ca1d457884f5aa293e8e81c"}, + {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2f69a03b52c9bb041aec1b8ee54b7b6c37a6d0a948786effa4c71157765b6da"}, + {file = "pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39"}, + {file = "pytest_codspeed-3.2.0.tar.gz", hash = "sha256:f9d1b1a3b2c69cdc0490a1e8b1ced44bffbd0e8e21d81a7160cfdd923f6e8155"}, +] [package.dependencies] -coverage = ">=4.4" -pytest = ">=4.6" +cffi = ">=1.17.1" +pytest = ">=3.8" +rich = ">=13.8.1" [package.extras] -testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] +compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"] +lint = ["mypy (>=1.11.2,<1.12.0)", "ruff (>=0.6.5,<0.7.0)"] +test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"] [[package]] -category = "main" -description = "Extensions to the standard Python datetime module" name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -version = "2.8.1" +groups = ["main", "doc"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] [package.dependencies] six = ">=1.5" [[package]] -category = "dev" -description = "World timezone definitions, modern and historical" -name = "pytz" +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" optional = false -python-versions = "*" -version = "2020.1" - -[[package]] -category = "main" -description = "The Olson timezone database for Python." -name = "pytzdata" +python-versions = ">=3.8" +groups = ["doc", "lint"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2020.1" +python-versions = ">=3.6" +groups = ["doc"] +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] -[[package]] -category = "dev" -description = "YAML parser and emitter for Python" -name = "pyyaml" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "5.3.1" +[package.dependencies] +pyyaml = "*" [[package]] -category = "dev" -description = "Alternative regular expression module, to replace re." -name = "regex" -optional = false -python-versions = "*" -version = "2020.6.8" +name = "rapidfuzz" +version = "3.10.1" +description = "rapid fuzzy string matching" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version < \"4.0\"" +files = [ + {file = "rapidfuzz-3.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f17d9f21bf2f2f785d74f7b0d407805468b4c173fa3e52c86ec94436b338e74a"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b31f358a70efc143909fb3d75ac6cd3c139cd41339aa8f2a3a0ead8315731f2b"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4f43f2204b56a61448ec2dd061e26fd344c404da99fb19f3458200c5874ba2"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d81bf186a453a2757472133b24915768abc7c3964194406ed93e170e16c21cb"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3611c8f45379a12063d70075c75134f2a8bd2e4e9b8a7995112ddae95ca1c982"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c3b537b97ac30da4b73930fa8a4fe2f79c6d1c10ad535c5c09726612cd6bed9"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231ef1ec9cf7b59809ce3301006500b9d564ddb324635f4ea8f16b3e2a1780da"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed4f3adc1294834955b7e74edd3c6bd1aad5831c007f2d91ea839e76461a5879"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7b6015da2e707bf632a71772a2dbf0703cff6525732c005ad24987fe86e8ec32"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1b35a118d61d6f008e8e3fb3a77674d10806a8972c7b8be433d6598df4d60b01"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bc308d79a7e877226f36bdf4e149e3ed398d8277c140be5c1fd892ec41739e6d"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f017dbfecc172e2d0c37cf9e3d519179d71a7f16094b57430dffc496a098aa17"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-win32.whl", hash = "sha256:36c0e1483e21f918d0f2f26799fe5ac91c7b0c34220b73007301c4f831a9c4c7"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:10746c1d4c8cd8881c28a87fd7ba0c9c102346dfe7ff1b0d021cdf093e9adbff"}, + {file = "rapidfuzz-3.10.1-cp310-cp310-win_arm64.whl", hash = "sha256:dfa64b89dcb906835e275187569e51aa9d546a444489e97aaf2cc84011565fbe"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:92958ae075c87fef393f835ed02d4fe8d5ee2059a0934c6c447ea3417dfbf0e8"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba7521e072c53e33c384e78615d0718e645cab3c366ecd3cc8cb732befd94967"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d02cbd75d283c287471b5b3738b3e05c9096150f93f2d2dfa10b3d700f2db9"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efa1582a397da038e2f2576c9cd49b842f56fde37d84a6b0200ffebc08d82350"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12912acee1f506f974f58de9fdc2e62eea5667377a7e9156de53241c05fdba8"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666d5d8b17becc3f53447bcb2b6b33ce6c2df78792495d1fa82b2924cd48701a"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26f71582c0d62445067ee338ddad99b655a8f4e4ed517a90dcbfbb7d19310474"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8a2ef08b27167bcff230ffbfeedd4c4fa6353563d6aaa015d725dd3632fc3de7"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:365e4fc1a2b95082c890f5e98489b894e6bf8c338c6ac89bb6523c2ca6e9f086"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1996feb7a61609fa842e6b5e0c549983222ffdedaf29644cc67e479902846dfe"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:cf654702f144beaa093103841a2ea6910d617d0bb3fccb1d1fd63c54dde2cd49"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec108bf25de674781d0a9a935030ba090c78d49def3d60f8724f3fc1e8e75024"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-win32.whl", hash = "sha256:031f8b367e5d92f7a1e27f7322012f3c321c3110137b43cc3bf678505583ef48"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:f98f36c6a1bb9a6c8bbec99ad87c8c0e364f34761739b5ea9adf7b48129ae8cf"}, + {file = "rapidfuzz-3.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:f1da2028cb4e41be55ee797a82d6c1cf589442504244249dfeb32efc608edee7"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1340b56340896bede246f612b6ecf685f661a56aabef3d2512481bfe23ac5835"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2316515169b7b5a453f0ce3adbc46c42aa332cae9f2edb668e24d1fc92b2f2bb"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e06fe6a12241ec1b72c0566c6b28cda714d61965d86569595ad24793d1ab259"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d99c1cd9443b19164ec185a7d752f4b4db19c066c136f028991a480720472e23"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1d9aa156ed52d3446388ba4c2f335e312191d1ca9d1f5762ee983cf23e4ecf6"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54bcf4efaaee8e015822be0c2c28214815f4f6b4f70d8362cfecbd58a71188ac"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0c955e32afdbfdf6e9ee663d24afb25210152d98c26d22d399712d29a9b976b"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:191633722203f5b7717efcb73a14f76f3b124877d0608c070b827c5226d0b972"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:195baad28057ec9609e40385991004e470af9ef87401e24ebe72c064431524ab"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0fff4a6b87c07366662b62ae994ffbeadc472e72f725923f94b72a3db49f4671"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4ffed25f9fdc0b287f30a98467493d1e1ce5b583f6317f70ec0263b3c97dbba6"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d02cf8e5af89a9ac8f53c438ddff6d773f62c25c6619b29db96f4aae248177c0"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-win32.whl", hash = "sha256:f3bb81d4fe6a5d20650f8c0afcc8f6e1941f6fecdb434f11b874c42467baded0"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:aaf83e9170cb1338922ae42d320699dccbbdca8ffed07faeb0b9257822c26e24"}, + {file = "rapidfuzz-3.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:c5da802a0d085ad81b0f62828fb55557996c497b2d0b551bbdfeafd6d447892f"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc22d69a1c9cccd560a5c434c0371b2df0f47c309c635a01a913e03bbf183710"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38b0dac2c8e057562b8f0d8ae5b663d2d6a28c5ab624de5b73cef9abb6129a24"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fde3bbb14e92ce8fcb5c2edfff72e474d0080cadda1c97785bf4822f037a309"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9141fb0592e55f98fe9ac0f3ce883199b9c13e262e0bf40c5b18cdf926109d16"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:237bec5dd1bfc9b40bbd786cd27949ef0c0eb5fab5eb491904c6b5df59d39d3c"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18123168cba156ab5794ea6de66db50f21bb3c66ae748d03316e71b27d907b95"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b75fe506c8e02769cc47f5ab21ce3e09b6211d3edaa8f8f27331cb6988779be"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da82aa4b46973aaf9e03bb4c3d6977004648c8638febfc0f9d237e865761270"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c34c022d5ad564f1a5a57a4a89793bd70d7bad428150fb8ff2760b223407cdcf"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e96c84d6c2a0ca94e15acb5399118fff669f4306beb98a6d8ec6f5dccab4412"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e8e154b84a311263e1aca86818c962e1fa9eefdd643d1d5d197fcd2738f88cb9"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:335fee93188f8cd585552bb8057228ce0111bd227fa81bfd40b7df6b75def8ab"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-win32.whl", hash = "sha256:6729b856166a9e95c278410f73683957ea6100c8a9d0a8dbe434c49663689255"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:0e06d99ad1ad97cb2ef7f51ec6b1fedd74a3a700e4949353871cf331d07b382a"}, + {file = "rapidfuzz-3.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:8d1b7082104d596a3eb012e0549b2634ed15015b569f48879701e9d8db959dbb"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:779027d3307e1a2b1dc0c03c34df87a470a368a1a0840a9d2908baf2d4067956"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:440b5608ab12650d0390128d6858bc839ae77ffe5edf0b33a1551f2fa9860651"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cac41a411e07a6f3dc80dfbd33f6be70ea0abd72e99c59310819d09f07d945"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:958473c9f0bca250590200fd520b75be0dbdbc4a7327dc87a55b6d7dc8d68552"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ef60dfa73749ef91cb6073be1a3e135f4846ec809cc115f3cbfc6fe283a5584"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7fbac18f2c19fc983838a60611e67e3262e36859994c26f2ee85bb268de2355"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a0d519ff39db887cd73f4e297922786d548f5c05d6b51f4e6754f452a7f4296"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bebb7bc6aeb91cc57e4881b222484c26759ca865794187217c9dcea6c33adae6"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe07f8b9c3bb5c5ad1d2c66884253e03800f4189a60eb6acd6119ebaf3eb9894"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfa48a4a2d45a41457f0840c48e579db157a927f4e97acf6e20df8fc521c79de"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2cf44d01bfe8ee605b7eaeecbc2b9ca64fc55765f17b304b40ed8995f69d7716"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e6bbca9246d9eedaa1c84e04a7f555493ba324d52ae4d9f3d9ddd1b740dcd87"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-win32.whl", hash = "sha256:567f88180f2c1423b4fe3f3ad6e6310fc97b85bdba574801548597287fc07028"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6b2cd7c29d6ecdf0b780deb587198f13213ac01c430ada6913452fd0c40190fc"}, + {file = "rapidfuzz-3.10.1-cp39-cp39-win_arm64.whl", hash = "sha256:9f912d459e46607ce276128f52bea21ebc3e9a5ccf4cccfef30dd5bddcf47be8"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ac4452f182243cfab30ba4668ef2de101effaedc30f9faabb06a095a8c90fd16"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:565c2bd4f7d23c32834652b27b51dd711814ab614b4e12add8476be4e20d1cf5"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:187d9747149321607be4ccd6f9f366730078bed806178ec3eeb31d05545e9e8f"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:616290fb9a8fa87e48cb0326d26f98d4e29f17c3b762c2d586f2b35c1fd2034b"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:073a5b107e17ebd264198b78614c0206fa438cce749692af5bc5f8f484883f50"}, + {file = "rapidfuzz-3.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39c4983e2e2ccb9732f3ac7d81617088822f4a12291d416b09b8a1eadebb3e29"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ac7adee6bcf0c6fee495d877edad1540a7e0f5fc208da03ccb64734b43522d7a"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:425f4ac80b22153d391ee3f94bc854668a0c6c129f05cf2eaf5ee74474ddb69e"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65a2fa13e8a219f9b5dcb9e74abe3ced5838a7327e629f426d333dfc8c5a6e66"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75561f3df9a906aaa23787e9992b228b1ab69007932dc42070f747103e177ba8"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edd062490537e97ca125bc6c7f2b7331c2b73d21dc304615afe61ad1691e15d5"}, + {file = "rapidfuzz-3.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfcc8feccf63245a22dfdd16e222f1a39771a44b870beb748117a0e09cbb4a62"}, + {file = "rapidfuzz-3.10.1.tar.gz", hash = "sha256:5a15546d847a915b3f42dc79ef9b0c78b998b4e2c53b252e7166284066585979"}, +] -[[package]] -category = "dev" -description = "scandir, a better directory iterator and faster os.walk()" -name = "scandir" -optional = false -python-versions = "*" -version = "1.10.0" +[package.extras] +all = ["numpy"] [[package]] -category = "dev" -description = "This library brings functools.singledispatch from Python 3.4 to Python 2.6-3.3." -name = "singledispatch" +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = "*" -version = "3.4.0.3" +python-versions = ">=3.8.0" +groups = ["benchmark"] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] [package.dependencies] -six = "*" +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] -category = "main" -description = "Python 2 and 3 compatibility utilities" name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.15.0" +groups = ["main", "doc"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "time-machine" +version = "3.2.0" +description = "Travel through time in your tests." +optional = false +python-versions = ">=3.10" +groups = ["main", "test"] +files = [ + {file = "time_machine-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:68142c070e78b62215d8029ec7394905083a4f9aacb0a2a11514ce70b5951b13"}, + {file = "time_machine-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:161bbd0648802ffdfcb4bb297ecb26b3009684a47d3a4dedb90bc549df4fa2ad"}, + {file = "time_machine-3.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1359ba8c258be695ba69253bc84db882fd616fe69b426cc6056536da2c7bf68e"}, + {file = "time_machine-3.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c85b169998ca2c24a78fb214586ec11c4cad56d9c38f55ad8326235cb481c884"}, + {file = "time_machine-3.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65b9367cb8a10505bc8f67da0da514ba20fa816fc47e11f434f7c60350322b4c"}, + {file = "time_machine-3.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9faca6a0f1973d7df3233c951fc2a11ff0c54df74087d8aaf41ae3deb19d0893"}, + {file = "time_machine-3.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:213b1ada7f385d467e598999b642eda4a8e89ae10ad5dc4f5d8f672cbf604261"}, + {file = "time_machine-3.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:160b6afd94c39855af04d39c58e4cf602406abd6d79427ab80e830ea71789cfb"}, + {file = "time_machine-3.2.0-cp310-cp310-win32.whl", hash = "sha256:c15d9ac257c78c124d112e4fc91fa9f3dcb004bdda913c19f0e7368d713cf080"}, + {file = "time_machine-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:3bf0f428487f93b8fe9d27aa01eccc817885da3290b467341b4a4a795e1d1891"}, + {file = "time_machine-3.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:347f6be2129fcd35b1c94b9387fcb2cbe7949b1e649228c5f22949a811b78976"}, + {file = "time_machine-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c188a9dda9fcf975022f1b325b466651b96a4dfc223c523ed7ed8d979f9bf3e8"}, + {file = "time_machine-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17245f1cc2dd13f9d63a174be59bb2684a9e5e0a112ab707e37be92068cd655f"}, + {file = "time_machine-3.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9bd1de1996e76efd36ae15970206c5089fb3728356794455bd5cd8d392b5537"}, + {file = "time_machine-3.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98493cd50e8b7f941eab69b9e18e697ad69db1a0ec1959f78f3d7b0387107e5c"}, + {file = "time_machine-3.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31f2a33d595d9f91eb9bc7f157f0dc5721f5789f4c4a9e8b852cdedb2a7d9b16"}, + {file = "time_machine-3.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9f78ac4213c10fbc44283edd1a29cfb7d3382484f4361783ddc057292aaa1889"}, + {file = "time_machine-3.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c1326b09e947b360926d529a96d1d9e126ce120359b63b506ecdc6ee20755c23"}, + {file = "time_machine-3.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f2949f03d15264cc15c38918a2cda8966001f0f4ebe190cbfd9c56d91aed8ac"}, + {file = "time_machine-3.2.0-cp311-cp311-win32.whl", hash = "sha256:6dfe48e0499e6e16751476b9799e67be7514e6ef04cdf39571ef95a279645831"}, + {file = "time_machine-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:809bdf267a29189c304154873620fe0bcc0c9513295fa46b19e21658231c4915"}, + {file = "time_machine-3.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:a3f4c17fa90f54902a3f8692c75caf67be87edc3429eeb71cb4595da58198f8e"}, + {file = "time_machine-3.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc4bee5b0214d7dc4ebc91f4a4c600f1a598e9b5606ac751f42cb6f6740b1dbb"}, + {file = "time_machine-3.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ca036304b4460ae2fdc1b52dd8b1fa7cf1464daa427fc49567413c09aa839c1"}, + {file = "time_machine-3.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5442735b41d7a2abc2f04579b4ca6047ed4698a8338a4fec92c7c9423e7938cb"}, + {file = "time_machine-3.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:97da3e971e505cb637079fb07ab0bcd36e33279f8ecac888ff131f45ef1e4d8d"}, + {file = "time_machine-3.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3cdda6dee4966e38aeb487309bb414c6cb23a81fc500291c77a8fcd3098832e7"}, + {file = "time_machine-3.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33d9efd302a6998bcc8baa4d84f259f8a4081105bd3d7f7af7f1d0abd3b1c8aa"}, + {file = "time_machine-3.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3a0b0a33971f14145853c9bd95a6ab0353cf7e0019fa2a7aa1ae9fddfe8eab50"}, + {file = "time_machine-3.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2d0be9e5f22c38082d247a2cdcd8a936504e9db60b7b3606855fb39f299e9548"}, + {file = "time_machine-3.2.0-cp312-cp312-win32.whl", hash = "sha256:3f74623648b936fdce5f911caf386c0a0b579456410975de8c0dfeaaffece1d8"}, + {file = "time_machine-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:34e26a41d994b5e4b205136a90e9578470386749cc9a2ecf51ca18f83ce25e23"}, + {file = "time_machine-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:0615d3d82c418d6293f271c348945c5091a71f37e37173653d5c26d0e74b13a8"}, + {file = "time_machine-3.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c421a8eb85a4418a7675a41bf8660224318c46cc62e4751c8f1ceca752059090"}, + {file = "time_machine-3.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4e758f7727d0058c4950c66b58200c187072122d6f7a98b610530a4233ea7b"}, + {file = "time_machine-3.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:154bd3f75c81f70218b2585cc12b60762fb2665c507eec5ec5037d8756d9b4e0"}, + {file = "time_machine-3.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50cfe5ebea422c896ad8d278af9648412b7533b8ea6adeeee698a3fd9b1d3b7"}, + {file = "time_machine-3.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636576501724bd6a9124e69d86e5aef263479e89ef739c5db361469f0463a0a1"}, + {file = "time_machine-3.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40e6f40c57197fcf7ec32d2c563f4df0a82c42cdcc3cab27f688e98f6060df10"}, + {file = "time_machine-3.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a1bcf0b846bbfc19a79bc19e3fa04d8c7b1e8101c1b70340ffdb689cd801ea53"}, + {file = "time_machine-3.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae55a56c179f4fe7a62575ad5148b6ed82f6c7e5cf2f9a9ec65f2f5b067db5f5"}, + {file = "time_machine-3.2.0-cp313-cp313-win32.whl", hash = "sha256:a66fe55a107e46916007a391d4030479df8864ec6ad6f6a6528221befc5c886e"}, + {file = "time_machine-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:30c9ce57165df913e4f74e285a8ab829ff9b7aa3e5ec0973f88f642b9a7b3d15"}, + {file = "time_machine-3.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:89cad7e179e9bdcc84dcf09efe52af232c4cc7a01b3de868356bbd59d95bd9b8"}, + {file = "time_machine-3.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:59d71545e62525a4b85b6de9ab5c02ee3c61110fd7f636139914a2335dcbfc9c"}, + {file = "time_machine-3.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:999672c621c35362bc28e03ca0c7df21500195540773c25993421fd8d6cc5003"}, + {file = "time_machine-3.2.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5faf7397f0580c7b9d67288522c8d7863e85f0cffadc0f1fccdb2c3dfce5783e"}, + {file = "time_machine-3.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3dd886ec49f1fa5a00e844f5947e5c0f98ce574750c24b7424c6f77fc1c3e87"}, + {file = "time_machine-3.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0ecd96bc7bbe450acaaabe569d84e81688f1be8ad58d1470e42371d145fb53"}, + {file = "time_machine-3.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:158220e946c1c4fb8265773a0282c88c35a7e3bb5d78e3561214e3b3231166f3"}, + {file = "time_machine-3.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c1aee29bc54356f248d5d7dfdd131e12ca825e850a08c0ebdb022266d073013"}, + {file = "time_machine-3.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ed2224f09d25b1c2fc98683613aca12f90f682a427eabb68fc824d27014e4a"}, + {file = "time_machine-3.2.0-cp313-cp313t-win32.whl", hash = "sha256:3498719f8dab51da76d29a20c1b5e52ee7db083dddf3056af7fa69c1b94e1fe6"}, + {file = "time_machine-3.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e0d90bee170b219e1d15e6a58164aa808f5170090e4f090bd0670303e34181b1"}, + {file = "time_machine-3.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:051de220fdb6e20d648111bbad423d9506fdbb2e44d4429cef3dc0382abf1fc2"}, + {file = "time_machine-3.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1398980c017fe5744d66f419e0115ee48a53b00b146d738e1416c225eb610b82"}, + {file = "time_machine-3.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f8f4e35f4191ef70c2ab8ff490761ee9051b891afce2bf86dde3918eb7b537b"}, + {file = "time_machine-3.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6db498686ecf6163c5aa8cf0bcd57bbe0f4081184f247edf3ee49a2612b584f9"}, + {file = "time_machine-3.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:027c1807efb74d0cd58ad16524dec94212fbe900115d70b0123399883657ac0f"}, + {file = "time_machine-3.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92432610c05676edd5e6946a073c6f0c926923123ce7caee1018dc10782c713d"}, + {file = "time_machine-3.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c25586b62480eb77ef3d953fba273209478e1ef49654592cd6a52a68dfe56a67"}, + {file = "time_machine-3.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6bf3a2fa738d15e0b95d14469a0b8ea42635467408d8b490e263d5d45c9a177f"}, + {file = "time_machine-3.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ce76b82276d7ad2a66cdc85dad4df19d1422b69183170a34e8fbc4c3f35502f7"}, + {file = "time_machine-3.2.0-cp314-cp314-win32.whl", hash = "sha256:14d6778273c543441863dff712cd1d7803dee946b18de35921eb8df10714539d"}, + {file = "time_machine-3.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbf821da96dbc80d349fa9e7c36e670b41d68a878d28c8850057992fed430eef"}, + {file = "time_machine-3.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:71c75d71f8e68abc8b669bca26ed2ddd558430a6c171e32b8620288565f18c0e"}, + {file = "time_machine-3.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4e374779021446fc2b5c29d80457ec9a3b1a5df043dc2aae07d7c1415d52323c"}, + {file = "time_machine-3.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:122310a6af9c36e9a636da32830e591e7923e8a07bdd0a43276c3a36c6821c90"}, + {file = "time_machine-3.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba3eeb0f018cc362dd8128befa3426696a2e16dd223c3fb695fde184892d4d8c"}, + {file = "time_machine-3.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:77d38ba664b381a7793f8786efc13b5004f0d5f672dae814430445b8202a67a6"}, + {file = "time_machine-3.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09abeb8f03f044d72712207e0489a62098ad3ad16dac38927fcf80baca4d6a7"}, + {file = "time_machine-3.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b28367ce4f73987a55e230e1d30a57a3af85da8eb1a140074eb6e8c7e6ef19f"}, + {file = "time_machine-3.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:903c7751c904581da9f7861c3015bed7cdc40047321291d3694a3cdc783bbca3"}, + {file = "time_machine-3.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:528217cad85ede5f85c8bc78b0341868d3c3cfefc6ecb5b622e1cacb6c73247b"}, + {file = "time_machine-3.2.0-cp314-cp314t-win32.whl", hash = "sha256:75724762ffd517e7e80aaec1fad1ff5a7414bd84e2b3ee7a0bacfeb67c14926e"}, + {file = "time_machine-3.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2526abbd053c5bca898d1b3e7898eec34626b12206718d8c7ce88fd12c1c9c5c"}, + {file = "time_machine-3.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7f2fb6784b414edbe2c0b558bfaab0c251955ba27edd62946cce4a01675a992c"}, + {file = "time_machine-3.2.0.tar.gz", hash = "sha256:a4ddd1cea17b8950e462d1805a42b20c81eb9aafc8f66b392dd5ce997e037d79"}, +] +markers = {main = "implementation_name != \"pypy\" and extra == \"test\""} -[[package]] -category = "dev" -description = "Python Library for Tom's Obvious, Minimal Language" -name = "toml" -optional = false -python-versions = "*" -version = "0.10.1" - -[[package]] -category = "dev" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -name = "tornado" -optional = false -python-versions = ">= 3.5" -version = "6.0.4" +[package.extras] +cli = ["tokenize-rt"] +dateutil = ["python-dateutil (>=2.8.2)"] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["benchmark", "build", "dev", "test", "typing"] +markers = "python_version == \"3.10\"" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] [[package]] -category = "dev" -description = "tox is a generic virtualenv management and test command line tool" name = "tox" +version = "4.23.2" +description = "tox is a generic virtualenv management and test command line tool" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "3.16.1" +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38"}, + {file = "tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"}, +] [package.dependencies] -filelock = ">=3.0.0" -packaging = ">=14" -pluggy = ">=0.12.0" -py = ">=1.4.17" -six = ">=1.14.0" -toml = ">=0.9.4" -virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" -colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} +cachetools = ">=5.5" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.16.1" +packaging = ">=24.1" +platformdirs = ">=4.3.6" +pluggy = ">=1.5" +pyproject-api = ">=1.8" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""} +virtualenv = ">=20.26.6" [package.extras] -docs = ["sphinx (>=2.0.0)", "towncrier (>=18.5.0)", "pygments-github-lexers (>=0.0.5)", "sphinxcontrib-autoprogram (>=0.1.5)"] -testing = ["freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-xdist (>=1.22.2)", "pytest-randomly (>=1.0.0)", "flaky (>=3.4.0)", "psutil (>=5.6.1)"] +test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"] [[package]] -category = "dev" -description = "a fork of Python 2 and 3 ast modules with type comment support" -name = "typed-ast" +name = "types-python-dateutil" +version = "2.9.0.20241003" +description = "Typing stubs for python-dateutil" optional = false -python-versions = "*" -version = "1.4.1" +python-versions = ">=3.8" +groups = ["typing"] +files = [ + {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, + {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"}, +] [[package]] -category = "main" -description = "Type Hints for Python" -name = "typing" +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = "*" -version = "3.7.4.1" +python-versions = ">=3.8" +groups = ["benchmark", "dev", "typing"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] +markers = {benchmark = "python_version == \"3.10\"", dev = "python_version == \"3.10\""} [[package]] -category = "dev" -description = "Backported and Experimental Type Hints for Python 3.5+" -name = "typing-extensions" +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" optional = false -python-versions = "*" -version = "3.7.4.2" +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] [[package]] -category = "dev" -description = "Virtual Python Environment builder" name = "virtualenv" +version = "20.28.0" +description = "Virtual Python Environment builder" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "20.0.26" +python-versions = ">=3.8" +groups = ["dev", "lint"] +files = [ + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, +] [package.dependencies] -appdirs = ">=1.4.3,<2" -distlib = ">=0.3.1,<1" -filelock = ">=3.0.0,<4" -six = ">=1.9.0,<2" -importlib-metadata = {version = ">=0.12,<2", markers = "python_version < \"3.8\""} -importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} -pathlib2 = {version = ">=2.3.3,<3", markers = "python_version < \"3.4\" and sys_platform != \"win32\""} +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2)"] -testing = ["pytest (>=4)", "coverage (>=5)", "coverage-enable-subprocess (>=1)", "pytest-xdist (>=1.31.0)", "pytest-mock (>=2)", "pytest-env (>=0.6.2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-freezegun (>=0.4.1)", "flaky (>=3)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] - -[[package]] -category = "dev" -description = "Measures the displayed width of unicode strings in a terminal" -name = "wcwidth" -optional = false -python-versions = "*" -version = "0.2.5" - -[package.dependencies] -"backports.functools-lru-cache" = {version = ">=1.2.1", markers = "python_version < \"3.2\""} - -[[package]] -category = "dev" -description = "Backport of pathlib-compatible object wrapper for zip files" -name = "zipp" -optional = false -python-versions = ">=2.7" -version = "1.2.0" - -[package.dependencies] -contextlib2 = {version = "*", markers = "python_version < \"3.4\""} +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +groups = ["doc"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] +watchmedo = ["PyYAML (>=3.10)"] -[metadata] -content-hash = "9a0542f32380e0fef3eb8d37b90903742a3fcad3b7e6b22f6ea8e4faac269e28" -python-versions = "~2.7 || ^3.5" +[extras] +test = ["time-machine"] -[metadata.files] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] -"aspy.yaml" = [ - {file = "aspy.yaml-1.3.0-py2.py3-none-any.whl", hash = "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc"}, - {file = "aspy.yaml-1.3.0.tar.gz", hash = "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, - {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, -] -babel = [ - {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, - {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, -] -"backports.functools-lru-cache" = [ - {file = "backports.functools_lru_cache-1.6.1-py2.py3-none-any.whl", hash = "sha256:0bada4c2f8a43d533e4ecb7a12214d9420e66eb206d54bf2d682581ca4b80848"}, - {file = "backports.functools_lru_cache-1.6.1.tar.gz", hash = "sha256:8fde5f188da2d593bd5bc0be98d9abc46c95bb8a9dde93429570192ee6cc2d4a"}, -] -black = [ - {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, - {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, -] -cfgv = [ - {file = "cfgv-2.0.1-py2.py3-none-any.whl", hash = "sha256:fbd93c9ab0a523bf7daec408f3be2ed99a980e20b2d19b50fc184ca6b820d289"}, - {file = "cfgv-2.0.1.tar.gz", hash = "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144"}, -] -cleo = [ - {file = "cleo-0.8.1-py2.py3-none-any.whl", hash = "sha256:141cda6dc94a92343be626bb87a0b6c86ae291dfc732a57bf04310d4b4201753"}, - {file = "cleo-0.8.1.tar.gz", hash = "sha256:3d0e22d30117851b45970b6c14aca4ab0b18b1b53c8af57bed13208147e4069f"}, -] -click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, -] -clikit = [ - {file = "clikit-0.6.2-py2.py3-none-any.whl", hash = "sha256:71268e074e68082306e23d7369a7b99f824a0ef926e55ba2665e911f7208489e"}, - {file = "clikit-0.6.2.tar.gz", hash = "sha256:442ee5db9a14120635c5990bcdbfe7c03ada5898291f0c802f77be71569ded59"}, -] -colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, -] -configparser = [ - {file = "configparser-4.0.2-py2.py3-none-any.whl", hash = "sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c"}, - {file = "configparser-4.0.2.tar.gz", hash = "sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df"}, -] -contextlib2 = [ - {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, - {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, -] -coverage = [ - {file = "coverage-5.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a"}, - {file = "coverage-5.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10"}, - {file = "coverage-5.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62"}, - {file = "coverage-5.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613"}, - {file = "coverage-5.2-cp27-cp27m-win32.whl", hash = "sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4"}, - {file = "coverage-5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a"}, - {file = "coverage-5.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70"}, - {file = "coverage-5.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee"}, - {file = "coverage-5.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b"}, - {file = "coverage-5.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913"}, - {file = "coverage-5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c"}, - {file = "coverage-5.2-cp35-cp35m-win32.whl", hash = "sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b"}, - {file = "coverage-5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e"}, - {file = "coverage-5.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0"}, - {file = "coverage-5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f"}, - {file = "coverage-5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405"}, - {file = "coverage-5.2-cp36-cp36m-win32.whl", hash = "sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40"}, - {file = "coverage-5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e"}, - {file = "coverage-5.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6"}, - {file = "coverage-5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1"}, - {file = "coverage-5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d"}, - {file = "coverage-5.2-cp37-cp37m-win32.whl", hash = "sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec"}, - {file = "coverage-5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703"}, - {file = "coverage-5.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032"}, - {file = "coverage-5.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d"}, - {file = "coverage-5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e"}, - {file = "coverage-5.2-cp38-cp38-win32.whl", hash = "sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7"}, - {file = "coverage-5.2-cp38-cp38-win_amd64.whl", hash = "sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b"}, - {file = "coverage-5.2-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d"}, - {file = "coverage-5.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d"}, - {file = "coverage-5.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c"}, - {file = "coverage-5.2-cp39-cp39-win32.whl", hash = "sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c"}, - {file = "coverage-5.2-cp39-cp39-win_amd64.whl", hash = "sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2"}, - {file = "coverage-5.2.tar.gz", hash = "sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404"}, -] -crashtest = [ - {file = "crashtest-0.3.0-py3-none-any.whl", hash = "sha256:06069a9267c54be31c42b03574b72407bf780e13c82cb0238f24ea69cf25b6dd"}, - {file = "crashtest-0.3.0.tar.gz", hash = "sha256:e9c06cc96400939ab5327123a3f699078eaad8a6283247d7b2ae0f6afffadf14"}, -] -distlib = [ - {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, - {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, -] -enum34 = [ - {file = "enum34-1.1.10-py2-none-any.whl", hash = "sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53"}, - {file = "enum34-1.1.10-py3-none-any.whl", hash = "sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328"}, - {file = "enum34-1.1.10.tar.gz", hash = "sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248"}, -] -filelock = [ - {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, - {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, -] -freezegun = [ - {file = "freezegun-0.3.15-py2.py3-none-any.whl", hash = "sha256:82c757a05b7c7ca3e176bfebd7d6779fd9139c7cb4ef969c38a28d74deef89b2"}, - {file = "freezegun-0.3.15.tar.gz", hash = "sha256:e2062f2c7f95cc276a834c22f1a17179467176b624cc6f936e8bc3be5535ad1b"}, -] -funcsigs = [ - {file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"}, - {file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"}, -] -futures = [ - {file = "futures-3.1.1-py2-none-any.whl", hash = "sha256:c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f"}, - {file = "futures-3.1.1-py3-none-any.whl", hash = "sha256:3a44f286998ae64f0cc083682fcfec16c406134a81a589a5de445d7bb7c2751b"}, - {file = "futures-3.1.1.tar.gz", hash = "sha256:51ecb45f0add83c806c68e4b06106f90db260585b25ef2abfcda0bd95c0132fd"}, -] -identify = [ - {file = "identify-1.4.23-py2.py3-none-any.whl", hash = "sha256:882c4b08b4569517b5f2257ecca180e01f38400a17f429f5d0edff55530c41c7"}, - {file = "identify-1.4.23.tar.gz", hash = "sha256:f89add935982d5bc62913ceee16c9297d8ff14b226e9d3072383a4e38136b656"}, -] -importlib-metadata = [ - {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, - {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, -] -importlib-resources = [ - {file = "importlib_resources-3.0.0-py2.py3-none-any.whl", hash = "sha256:d028f66b66c0d5732dae86ba4276999855e162a749c92620a38c1d779ed138a7"}, - {file = "importlib_resources-3.0.0.tar.gz", hash = "sha256:19f745a6eca188b490b1428c8d1d4a0d2368759f32370ea8fb89cad2ab1106c3"}, -] -isort = [ - {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, - {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, -] -jinja2 = [ - {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, - {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, -] -livereload = [ - {file = "livereload-2.6.2.tar.gz", hash = "sha256:d1eddcb5c5eb8d2ca1fa1f750e580da624c0f7fcb734aa5780dc81b7dcbd89be"}, -] -markdown = [ - {file = "Markdown-3.1.1-py2.py3-none-any.whl", hash = "sha256:56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"}, - {file = "Markdown-3.1.1.tar.gz", hash = "sha256:2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a"}, -] -markdown-include = [ - {file = "markdown-include-0.5.1.tar.gz", hash = "sha256:72a45461b589489a088753893bc95c5fa5909936186485f4ed55caa57d10250f"}, -] -markupsafe = [ - {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, -] -mkdocs = [ - {file = "mkdocs-1.0.4-py2.py3-none-any.whl", hash = "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"}, - {file = "mkdocs-1.0.4.tar.gz", hash = "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939"}, -] -more-itertools = [ - {file = "more-itertools-5.0.0.tar.gz", hash = "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4"}, - {file = "more_itertools-5.0.0-py2-none-any.whl", hash = "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc"}, - {file = "more_itertools-5.0.0-py3-none-any.whl", hash = "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"}, -] -nodeenv = [ - {file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"}, -] -packaging = [ - {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, - {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, -] -pastel = [ - {file = "pastel-0.2.0-py2.py3-none-any.whl", hash = "sha256:18b559dc3ad4ba9b8bd5baebe6503f25f36d21460f021cf27a8d889cb5d17840"}, - {file = "pastel-0.2.0.tar.gz", hash = "sha256:46155fc523bdd4efcd450bbcb3f2b94a6e3b25edc0eb493e081104ad09e1ca36"}, -] -pathlib2 = [ - {file = "pathlib2-2.3.5-py2.py3-none-any.whl", hash = "sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db"}, - {file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"}, -] -pathspec = [ - {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, - {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, -] -pep562 = [ - {file = "pep562-1.0-py2.py3-none-any.whl", hash = "sha256:d2a48b178ebf5f8dd31709cc26a19808ef794561fa2fe50ea01ea2bad4d667ef"}, - {file = "pep562-1.0.tar.gz", hash = "sha256:58cb1cc9ee63d93e62b4905a50357618d526d289919814bea1f0da8f53b79395"}, -] -pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, -] -pre-commit = [ - {file = "pre_commit-1.21.0-py2.py3-none-any.whl", hash = "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"}, - {file = "pre_commit-1.21.0.tar.gz", hash = "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850"}, -] -py = [ - {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, -] -pygments = [ - {file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"}, - {file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"}, -] -pylev = [ - {file = "pylev-1.3.0-py2.py3-none-any.whl", hash = "sha256:1d29a87beb45ebe1e821e7a3b10da2b6b2f4c79b43f482c2df1a1f748a6e114e"}, - {file = "pylev-1.3.0.tar.gz", hash = "sha256:063910098161199b81e453025653ec53556c1be7165a9b7c50be2f4d57eae1c3"}, -] -pymdown-extensions = [ - {file = "pymdown-extensions-6.2.1.tar.gz", hash = "sha256:3bbe6048275f8a0d13a0fe44e0ea201e67268aa7bb40c2544eef16abbf168f7b"}, - {file = "pymdown_extensions-6.2.1-py2.py3-none-any.whl", hash = "sha256:dce5e17b93be0572322b7d06c9a13c13a9d98694d6468277911d50ca87d26f29"}, -] -pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, -] -pytest = [ - {file = "pytest-4.6.11-py2.py3-none-any.whl", hash = "sha256:a00a7d79cbbdfa9d21e7d0298392a8dd4123316bfac545075e6f8f24c94d8c97"}, - {file = "pytest-4.6.11.tar.gz", hash = "sha256:50fa82392f2120cc3ec2ca0a75ee615be4c479e66669789771f1758332be4353"}, -] -pytest-cov = [ - {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, - {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, -] -python-dateutil = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, -] -pytz = [ - {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, - {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, -] -pytzdata = [ - {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, - {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"}, -] -pyyaml = [ - {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, - {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, - {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, - {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, - {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, -] -regex = [ - {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"}, - {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"}, - {file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"}, - {file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"}, - {file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"}, - {file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"}, - {file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"}, - {file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"}, - {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"}, -] -scandir = [ - {file = "scandir-1.10.0-cp27-cp27m-win32.whl", hash = "sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188"}, - {file = "scandir-1.10.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac"}, - {file = "scandir-1.10.0-cp34-cp34m-win32.whl", hash = "sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f"}, - {file = "scandir-1.10.0-cp34-cp34m-win_amd64.whl", hash = "sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e"}, - {file = "scandir-1.10.0-cp35-cp35m-win32.whl", hash = "sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f"}, - {file = "scandir-1.10.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32"}, - {file = "scandir-1.10.0-cp36-cp36m-win32.whl", hash = "sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022"}, - {file = "scandir-1.10.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4"}, - {file = "scandir-1.10.0-cp37-cp37m-win32.whl", hash = "sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173"}, - {file = "scandir-1.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d"}, - {file = "scandir-1.10.0.tar.gz", hash = "sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae"}, -] -singledispatch = [ - {file = "singledispatch-3.4.0.3-py2.py3-none-any.whl", hash = "sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8"}, - {file = "singledispatch-3.4.0.3.tar.gz", hash = "sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c"}, -] -six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, -] -toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, -] -tornado = [ - {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, - {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, - {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, - {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, - {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, - {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, - {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, - {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, - {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, -] -tox = [ - {file = "tox-3.16.1-py2.py3-none-any.whl", hash = "sha256:60c3793f8ab194097ec75b5a9866138444f63742b0f664ec80be1222a40687c5"}, - {file = "tox-3.16.1.tar.gz", hash = "sha256:9a746cda9cadb9e1e05c7ab99f98cfcea355140d2ecac5f97520be94657c3bc7"}, -] -typed-ast = [ - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, - {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, - {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, - {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, - {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, -] -typing = [ - {file = "typing-3.7.4.1-py2-none-any.whl", hash = "sha256:c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36"}, - {file = "typing-3.7.4.1-py3-none-any.whl", hash = "sha256:f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"}, - {file = "typing-3.7.4.1.tar.gz", hash = "sha256:91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23"}, -] -typing-extensions = [ - {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, - {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, - {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, -] -virtualenv = [ - {file = "virtualenv-20.0.26-py2.py3-none-any.whl", hash = "sha256:c11a475400e98450403c0364eb3a2d25d42f71cf1493da64390487b666de4324"}, - {file = "virtualenv-20.0.26.tar.gz", hash = "sha256:e10cc66f40cbda459720dfe1d334c4dc15add0d80f09108224f171006a97a172"}, -] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] -zipp = [ - {file = "zipp-1.2.0-py2.py3-none-any.whl", hash = "sha256:e0d9e63797e483a30d27e09fffd308c59a700d365ec34e93cc100844168bf921"}, - {file = "zipp-1.2.0.tar.gz", hash = "sha256:c70410551488251b0fee67b460fb9a536af8d6f9f008ad10ac51f615b6a521b1"}, -] +[metadata] +lock-version = "2.1" +python-versions = ">=3.10" +content-hash = "edee83acf5459cf49258c09ff8fbebcc491cf24d7ff5979f0585b9ac54355431" diff --git a/pyproject.toml b/pyproject.toml index e7e461fe..03ccdeb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,81 +1,226 @@ -[tool.poetry] +[project] name = "pendulum" -version = "2.1.1" +version = "3.2.0" description = "Python datetimes made easy" -authors = ["Sébastien Eustace "] -license = "MIT" -readme = 'README.rst' -homepage = "https://pendulum.eustace.io" -repository = "https://github.com/sdispater/pendulum" -documentation = "https://pendulum.eustace.io/docs" +readme = "README.rst" +requires-python = ">=3.10" +license = { text = "MIT License" } +authors = [{ name = "Sébastien Eustace", email = "sebastien@eustace.io" }] keywords = ['datetime', 'date', 'time'] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] -packages = [ - {include = "pendulum"}, - #{include = "tests", format = "sdist"}, +dependencies = [ + "python-dateutil>=2.6", + "tzdata>=2020.1", ] -include = [ - {path = "pendulum/py.typed"}, - # C extensions must be included in the wheel distributions - {path = "pendulum/_extensions/*.so", format = "wheel"}, - {path = "pendulum/_extensions/*.pyd", format = "wheel"}, - {path = "pendulum/parsing/*.so", format = "wheel"}, - {path = "pendulum/parsing/*.pyd", format = "wheel"}, + +[project.optional-dependencies] +test = [ + 'time-machine>=3.0.0,<4.0.0; implementation_name != "pypy"', ] +[project.urls] +Homepage = "https://pendulum.eustace.io" +Documentation = "https://pendulum.eustace.io/docs" +Repository = "https://github.com/sdispater/pendulum" + -[tool.poetry.dependencies] -python = "~2.7 || ^3.5" -python-dateutil = "^2.6" -pytzdata = ">=2020.1" - -# typing is needed for Python < 3.5 -typing = { version = "^3.6", python = "<3.5" } - -[tool.poetry.dev-dependencies] -pytest = "^4.6" -pytest-cov = "^2.5" -pytz = ">=2018.3" -babel = "^2.5" -cleo = "^0.8.1" -tox = "^3.0" -black = { version = "^19.3b0", markers = "python_version >= '3.6' and python_version < '4.0' and implementation_name != 'pypy'" } -isort = { version = "^4.3.21", markers = "python_version >= '3.6' and python_version < '4.0'" } -pre-commit = "^1.10" -mkdocs = { version = "^1.0", python = "^3.5" } -pymdown-extensions = "^6.0" +[tool.poetry.group.test.dependencies] +pytest = "^7.1.2" +time-machine = ">=3.0.0" +pytest-benchmark = "^4.0.0" + +[tool.poetry.group.doc.dependencies] +mkdocs = "^1.0" +pymdown-extensions = ">=6,<11" pygments = "^2.2" -markdown-include = "^0.5.1" -freezegun = "^0.3.15" - -[tool.poetry.build] -generate-setup-file = false -script = "build.py" - -[tool.isort] -line_length = 88 -force_single_line = true -force_grid_wrap = 0 -atomic = true -include_trailing_comma = true -lines_after_imports = 2 -lines_between_types = 1 -multi_line_output = 3 -use_parentheses = true -not_skip = "__init__.py" -skip_glob = ["*/setup.py"] -filter_files = true - -known_first_party = "pendulum" -known_third_party = [ +markdown-include = "^0.8.1" + +[tool.poetry.group.lint.dependencies] +pre-commit = "^3.0.0" + +[tool.poetry.group.typing.dependencies] +mypy = "^1.3.0" +types-python-dateutil = "^2.8.19" + +[tool.poetry.group.dev.dependencies] +babel = "^2.10.3" +cleo = { version = "^2.0.1", python = ">=3.8,<4.0" } +tox = "^4.0.0" + +[tool.poetry.group.benchmark.dependencies] +pytest-codspeed = "^3.0.0" + +[tool.poetry.group.build.dependencies] +maturin = ">=1.0,<2.0" + +[tool.maturin] +module-name = "pendulum._pendulum" +features = ["pyo3/extension-module"] +python-packages = ["pendulum"] +include = [ + { path = "LICENSE", format = "sdist" }, +] +[tool.ruff] +fix = true +line-length = 88 +target-version = "py310" +extend-exclude = [ + # External to the project's coding standards: + "docs/*", + # Machine-generated, too many false-positives + "src/pendulum/locales/*", + # ruff disagrees with black when it comes to formatting + "*.pyi", +] + +[tool.ruff.lint] +extend-select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "ERA", # flake8-eradicate/eradicate + "I", # isort + "N", # pep8-naming + "PIE", # flake8-pie + "PGH", # pygrep + "RUF", # ruff checks + "SIM", # flake8-simplify + "T20", # flake8-print + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade +] +ignore = [ + "B904", # use 'raise ... from err' + "B905", # use explicit 'strict=' parameter with 'zip()' + "N818", + "RUF001" +] +extend-safe-fixes = [ + "TCH", # move import from and to TYPE_CHECKING blocks +] +unfixable = [ + "ERA", # do not autoremove commented out code +] + +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.lint.isort] +force-single-line = true +lines-between-types = 1 +lines-after-imports = 2 +known-first-party = ["pendulum"] +known-third-party = [ "babel", "cleo", "dateutil", - "freezegun", + "time_machine", "pytzdata", ] +required-imports = ["from __future__ import annotations"] +[tool.ruff.lint.extend-per-file-ignores] +"build.py" = ["I002"] +"clock" = ["RUF012"] + +[tool.mypy] +strict = true +files = "src, tests" +show_error_codes = true +pretty = true +warn_unused_ignores = true +exclude = [ + "^build\\.py$" +] + +# The following whitelist is used to allow for incremental adoption +# of Mypy. Modules should be removed from this whitelist as and when +# their respective type errors have been addressed. No new modules +# should be added to this whitelist. + +[[tool.mypy.overrides]] +module = [ + "pendulum.mixins.default", + "tests.test_parsing", + "tests.date.test_add", + "tests.date.test_behavior", + "tests.date.test_construct", + "tests.date.test_comparison", + "tests.date.test_day_of_week_modifiers", + "tests.date.test_diff", + "tests.date.test_fluent_setters", + "tests.date.test_getters", + "tests.date.test_start_end_of", + "tests.date.test_strings", + "tests.date.test_sub", + "tests.datetime.test_add", + "tests.datetime.test_behavior", + "tests.datetime.test_construct", + "tests.datetime.test_comparison", + "tests.datetime.test_create_from_timestamp", + "tests.datetime.test_day_of_week_modifiers", + "tests.datetime.test_diff", + "tests.datetime.test_fluent_setters", + "tests.datetime.test_from_format", + "tests.datetime.test_getters", + "tests.datetime.test_naive", + "tests.datetime.test_replace", + "tests.datetime.test_start_end_of", + "tests.datetime.test_strings", + "tests.datetime.test_sub", + "tests.datetime.test_timezone", + "tests.duration.test_add_sub", + "tests.duration.test_arithmetic", + "tests.duration.test_behavior", + "tests.duration.test_construct", + "tests.duration.test_in_methods", + "tests.duration.test_in_words", + "tests.duration.test_total_methods", + "tests.formatting.test_formatter", + "tests.helpers.test_local_time", + "tests.localization.*", + "tests.parsing.test_parsing", + "tests.parsing.test_parsing_duration", + "tests.parsing.test_parse_iso8601", + "tests.interval.test_add_subtract", + "tests.interval.test_arithmetic", + "tests.interval.test_behavior", + "tests.interval.test_construct", + "tests.interval.test_hashing", + "tests.interval.test_in_words", + "tests.interval.test_range", + "tests.time.test_add", + "tests.time.test_behavior", + "tests.time.test_comparison", + "tests.time.test_construct", + "tests.time.test_diff", + "tests.time.test_fluent_setters", + "tests.time.test_strings", + "tests.time.test_sub", + "tests.tz.test_helpers", + "tests.tz.test_local_timezone", + "tests.tz.test_timezone", + "tests.tz.test_timezones", +] +ignore_errors = true + +[tool.coverage.run] +omit = [ + "pendulum/locales/*", + "pendulum/__version__.py,", + "pendulum/_extensions/*", + "pendulum/parsing/iso8601.py", + "pendulum/utils/_compat.py", +] [build-system] -requires = ["poetry-core>=1.0.0a9"] -build-backend = "poetry.core.masonry.api" +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml new file mode 100644 index 00000000..f0ba8af4 --- /dev/null +++ b/rust/.cargo/config.toml @@ -0,0 +1,15 @@ +[build] +rustflags = [] + +# see https://pyo3.rs/main/building_and_distribution.html#macos +[target.x86_64-apple-darwin] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] + +[target.aarch64-apple-darwin] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 00000000..fd6978d4 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,204 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "_pendulum" +version = "3.2.0" +dependencies = [ + "pyo3", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" +dependencies = [ + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" +dependencies = [ + "python3-dll-a", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "python3-dll-a" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d381ef313ae70b4da5f95f8a4de773c6aa5cd28f73adec4b4a31df70b66780d8" +dependencies = [ + "cc", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 00000000..097321fe --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "_pendulum" +version = "3.2.0" +edition = "2021" + +[lib] +name = "_pendulum" +crate-type = ["cdylib"] + +[profile.release] +lto = "fat" +codegen-units = 1 +strip = true +overflow-checks = false + +[dependencies] +pyo3 = { version = "0.27", features = ["extension-module", "generate-import-lib"] } + +[features] +extension-module = ["pyo3/extension-module"] diff --git a/rust/src/constants.rs b/rust/src/constants.rs new file mode 100644 index 00000000..3fea9c02 --- /dev/null +++ b/rust/src/constants.rs @@ -0,0 +1,56 @@ +pub const EPOCH_YEAR: u32 = 1970; + +pub const DAYS_PER_N_YEAR: u32 = 365; +pub const DAYS_PER_L_YEAR: u32 = 366; + +pub const SECS_PER_MIN: u32 = 60; +pub const SECS_PER_HOUR: u32 = SECS_PER_MIN * 60; +pub const SECS_PER_DAY: u32 = SECS_PER_HOUR * 24; + +// 400-year chunks always have 146097 days (20871 weeks). +pub const DAYS_PER_400_YEARS: u32 = 146_097; +pub const SECS_PER_400_YEARS: u64 = DAYS_PER_400_YEARS as u64 * SECS_PER_DAY as u64; + +// The number of seconds in an aligned 100-year chunk, for those that +// do not begin with a leap year and those that do respectively. +pub const SECS_PER_100_YEARS: [u64; 2] = [ + (76 * DAYS_PER_N_YEAR as u64 + 24 * DAYS_PER_L_YEAR as u64) * SECS_PER_DAY as u64, + (75 * DAYS_PER_N_YEAR as u64 + 25 * DAYS_PER_L_YEAR as u64) * SECS_PER_DAY as u64, +]; + +// The number of seconds in an aligned 4-year chunk, for those that +// do not begin with a leap year and those that do respectively. +#[allow(clippy::erasing_op)] +pub const SECS_PER_4_YEARS: [u32; 2] = [ + (4 * DAYS_PER_N_YEAR + 0 * DAYS_PER_L_YEAR) * SECS_PER_DAY, + (3 * DAYS_PER_N_YEAR + DAYS_PER_L_YEAR) * SECS_PER_DAY, +]; + +// The number of seconds in non-leap and leap years respectively. +pub const SECS_PER_YEAR: [u32; 2] = [ + DAYS_PER_N_YEAR * SECS_PER_DAY, + DAYS_PER_L_YEAR * SECS_PER_DAY, +]; + +// The month lengths in non-leap and leap years respectively. +pub const DAYS_PER_MONTHS: [[i32; 13]; 2] = [ + [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], + [-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], +]; + +// The day offsets of the beginning of each (1-based) month in non-leap +// and leap years respectively. +// For example, in a leap year there are 335 days before December. +pub const MONTHS_OFFSETS: [[i32; 14]; 2] = [ + [ + -1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365, + ], + [ + -1, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366, + ], +]; + +pub const DAY_OF_WEEK_TABLE: [u32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]; + +pub const TM_JANUARY: usize = 0; +pub const TM_DECEMBER: usize = 11; diff --git a/rust/src/helpers.rs b/rust/src/helpers.rs new file mode 100644 index 00000000..b1b6142b --- /dev/null +++ b/rust/src/helpers.rs @@ -0,0 +1,122 @@ +use crate::constants::{ + DAYS_PER_L_YEAR, DAYS_PER_N_YEAR, DAY_OF_WEEK_TABLE, EPOCH_YEAR, MONTHS_OFFSETS, + SECS_PER_100_YEARS, SECS_PER_400_YEARS, SECS_PER_4_YEARS, SECS_PER_DAY, SECS_PER_HOUR, + SECS_PER_MIN, SECS_PER_YEAR, TM_DECEMBER, TM_JANUARY, +}; + +fn p(year: i32) -> i32 { + year + year / 4 - year / 100 + year / 400 +} + +pub fn is_leap(year: i32) -> bool { + year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) +} + +pub fn is_long_year(year: i32) -> bool { + (p(year) % 7 == 4) || (p(year - 1) % 7 == 3) +} + +pub fn days_in_year(year: i32) -> u32 { + if is_leap(year) { + return DAYS_PER_L_YEAR; + } + + DAYS_PER_N_YEAR +} + +pub fn week_day(year: i32, month: u32, day: u32) -> u32 { + let y: i32 = year - i32::from(month < 3); + + let w: i32 = (p(y) + DAY_OF_WEEK_TABLE[(month - 1) as usize] as i32 + day as i32) % 7; + + if w == 0 { + return 7; + } + + w.unsigned_abs() +} + +pub fn day_number(year: i32, month: u8, day: u8) -> i32 { + let m = i32::from((month + 9) % 12); + let y = year - m / 10; + + 365 * y + y / 4 - y / 100 + y / 400 + (m * 306 + 5) / 10 + (i32::from(day) - 1) +} + +pub fn local_time( + unix_time: f64, + utc_offset: isize, + microsecond: usize, +) -> (usize, usize, usize, usize, usize, usize, usize) { + let mut year: usize = EPOCH_YEAR as usize; + let mut seconds: i64 = unix_time.floor() as i64; + + // Shift to a base year that is 400-year aligned. + if seconds >= 0 { + seconds -= 10957 * SECS_PER_DAY as i64; + year += 30; // == 2000 + } else { + seconds += (146_097 - 10957) * SECS_PER_DAY as i64; + year -= 370; // == 1600 + } + + seconds += utc_offset as i64; + + // Handle years in chunks of 400/100/4/1 + year += 400 * (seconds / SECS_PER_400_YEARS as i64) as usize; + seconds %= SECS_PER_400_YEARS as i64; + if seconds < 0 { + seconds += SECS_PER_400_YEARS as i64; + year -= 400; + } + + let mut leap_year = 1; // 4-century aligned + let mut sec_per_100years = SECS_PER_100_YEARS[leap_year].try_into().unwrap(); + + while seconds >= sec_per_100years { + seconds -= sec_per_100years; + year += 100; + leap_year = 0; // 1-century, non 4-century aligned + sec_per_100years = SECS_PER_100_YEARS[leap_year].try_into().unwrap(); + } + + let mut sec_per_4years = SECS_PER_4_YEARS[leap_year].into(); + while seconds >= sec_per_4years { + seconds -= sec_per_4years; + year += 4; + leap_year = 1; // 4-year, non century aligned + sec_per_4years = SECS_PER_4_YEARS[leap_year].into(); + } + + let mut sec_per_year = SECS_PER_YEAR[leap_year].into(); + while seconds >= sec_per_year { + seconds -= sec_per_year; + year += 1; + leap_year = 0; // non 4-year aligned + sec_per_year = SECS_PER_YEAR[leap_year].into(); + } + + // Handle months and days + let mut month = TM_DECEMBER + 1; + let mut day: usize = (seconds / (SECS_PER_DAY as i64) + 1) as usize; + seconds %= SECS_PER_DAY as i64; + + let mut month_offset: usize; + while month != (TM_JANUARY + 1) { + month_offset = MONTHS_OFFSETS[leap_year][month] as usize; + if day > month_offset { + day -= month_offset; + break; + } + + month -= 1; + } + + // Handle hours, minutes and seconds + let hour: usize = (seconds / SECS_PER_HOUR as i64) as usize; + seconds %= SECS_PER_HOUR as i64; + let minute: usize = (seconds / SECS_PER_MIN as i64) as usize; + let second: usize = (seconds % SECS_PER_MIN as i64) as usize; + + (year, month, day, hour, minute, second, microsecond) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 00000000..aac55da7 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,8 @@ +extern crate core; + +mod constants; +mod helpers; +mod parsing; +mod python; + +pub use python::_pendulum; diff --git a/rust/src/parsing.rs b/rust/src/parsing.rs new file mode 100644 index 00000000..374fe3cf --- /dev/null +++ b/rust/src/parsing.rs @@ -0,0 +1,906 @@ +use core::str; +use std::{fmt, str::CharIndices}; + +use crate::{ + constants::MONTHS_OFFSETS, + helpers::{days_in_year, is_leap, is_long_year, week_day}, +}; + +#[derive(Debug, Clone)] +pub struct ParseError { + index: usize, + message: String, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} (Position: {})", self.message, self.index) + } +} + +pub struct ParsedDateTime { + pub year: u32, + pub month: u32, + pub day: u32, + pub hour: u32, + pub minute: u32, + pub second: u32, + pub microsecond: u32, + pub offset: Option, + #[allow(dead_code)] + pub has_offset: bool, + pub tzname: Option, + pub has_date: bool, + pub has_time: bool, + pub extended_date_format: bool, + pub time_is_midnight: bool, +} + +impl ParsedDateTime { + pub fn new() -> ParsedDateTime { + ParsedDateTime { + year: 0, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + microsecond: 0, + offset: None, + has_offset: false, + tzname: None, + has_date: false, + has_time: false, + extended_date_format: false, + time_is_midnight: false, + } + } +} + +pub struct ParsedDuration { + pub years: u32, + pub months: u32, + pub weeks: u32, + pub days: u32, + pub hours: u32, + pub minutes: u32, + pub seconds: u32, + pub microseconds: u32, +} + +impl ParsedDuration { + pub fn new() -> ParsedDuration { + ParsedDuration { + years: 0, + months: 0, + weeks: 0, + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + microseconds: 0, + } + } +} + +pub struct Parsed { + pub datetime: Option, + pub duration: Option, + pub second_datetime: Option, +} + +impl Parsed { + pub fn new() -> Parsed { + Parsed { + datetime: None, + duration: None, + second_datetime: None, + } + } +} + +pub struct Parser<'a> { + /// Input to parse. + src: &'a str, + /// Iterator used for getting characters from `src`. + chars: CharIndices<'a>, + /// Current byte offset into `src`. + idx: usize, + /// Current character + current: char, +} + +impl<'a> Parser<'a> { + /// Creates a new parser from a &str. + pub fn new(input: &'a str) -> Parser<'a> { + let mut p = Parser { + src: input, + chars: input.char_indices(), + idx: 0, + current: '\0', + }; + p.inc(); + p + } + + /// Increments the parser if the end of the input has not been reached. + /// Returns whether or not it was able to advance. + fn inc(&mut self) -> Option { + if let Some((i, ch)) = self.chars.next() { + self.idx = i; + self.current = ch; + Some(ch) + } else { + self.idx = self.src.len(); + self.current = '\0'; + None + } + } + + fn parse_error(&mut self, message: String) -> ParseError { + ParseError { + index: self.idx, + message, + } + } + + fn unexpected_character_error( + &mut self, + field_name: &str, + expected_character_count: usize, + ) -> ParseError { + if self.end() { + return self.parse_error(format!( + "Unexpected end of string while parsing {}. Expected {} more character{}.", + field_name, + expected_character_count, + if expected_character_count == 1 { + "" + } else { + "s" + } + )); + } + + self.parse_error(format!( + "Invalid character while parsing {}: {}.", + field_name, self.current, + )) + } + + /// Returns true if the parser has reached the end of the input. + fn end(&self) -> bool { + self.idx >= self.src.len() + } + + fn parse_integer(&mut self, length: usize, field_name: &str) -> Result { + let mut value: u32 = 0; + + for i in 0..length { + if self.end() { + return Err(self.parse_error(format!( + "Unexpected end of string while parsing \"{}\". Expected {} more character{}", + field_name, + length - i, + if (length - i) != 1 { "s" } else { "" } + ))); + } + + if let Some(digit) = self.current.to_digit(10) { + value = 10 * value + digit; + self.inc(); + } else { + return Err(self.unexpected_character_error(field_name, length - i)); + } + } + + Ok(value) + } + + pub fn parse(&mut self) -> Result { + let mut parsed = Parsed::new(); + + if self.current == 'P' { + // Duration (and possibly time interval) + self.parse_duration(&mut parsed)?; + } else { + self.parse_datetime(&mut parsed)?; + } + + Ok(parsed) + } + + fn parse_datetime(&mut self, parsed: &mut Parsed) -> Result<(), ParseError> { + let mut datetime = ParsedDateTime::new(); + + if self.current == 'T' { + self.parse_time(&mut datetime, false)?; + + if !self.end() { + return Err(self.parse_error("Unconverted data remains".to_string())); + } + + match &parsed.datetime { + Some(_) => { + parsed.second_datetime = Some(datetime); + } + None => match &parsed.duration { + Some(_) => { + parsed.second_datetime = Some(datetime); + } + None => { + parsed.datetime = Some(datetime); + } + }, + } + + return Ok(()); + } + + datetime.year = self.parse_integer(2, "year")?; + + if self.current == ':' { + // Time in extended format + datetime.hour = datetime.year; + datetime.year = 0; + datetime.extended_date_format = true; + self.parse_time(&mut datetime, true)?; + + if !self.end() { + return Err(self.parse_error("Unconverted data remains".to_string())); + } + + match &parsed.datetime { + Some(_) => { + parsed.second_datetime = Some(datetime); + } + None => match &parsed.duration { + Some(_) => { + parsed.second_datetime = Some(datetime); + } + None => { + parsed.datetime = Some(datetime); + } + }, + } + + return Ok(()); + } + + datetime.has_date = true; + datetime.year = datetime.year * 100 + self.parse_integer(2, "year")?; + + if self.current == '-' { + self.inc(); + datetime.extended_date_format = true; + + if self.current == 'W' { + // ISO week and day in extended format (i.e. Www-D) + self.inc(); + + let iso_week = self.parse_integer(2, "iso week")?; + let mut iso_day: u32 = 1; + + if !self.end() && self.current != ' ' && self.current != 'T' { + // Optional day + if self.current != '-' { + return Err(self.parse_error(format!( + "Invalid character \"{}\" while parsing {}", + self.current, "date separator" + ))); + } + + self.inc(); + + iso_day = self.parse_integer(1, "iso day")?; + } + + let (year, month, day) = self.iso_to_ymd(datetime.year, iso_week, iso_day)?; + + datetime.year = year; + datetime.month = month; + datetime.day = day; + } else { + /* + Month and day in extended format (MM-DD) or ordinal date (DDD) + We'll assume first that the next part is a month and adjust if not. + */ + datetime.month = self.parse_integer(2, "month")?; + + if !self.end() && self.current != ' ' && self.current != 'T' { + if self.current == '-' { + // Optional day + self.inc(); + datetime.day = self.parse_integer(2, "day")?; + } else { + // Ordinal day + let ordinal_day = + (datetime.month * 10 + self.parse_integer(1, "ordinal day")?) as i32; + + let (year, month, day) = + self.ordinal_to_ymd(datetime.year, ordinal_day, false)?; + + datetime.year = year; + datetime.month = month; + datetime.day = day; + } + } else { + datetime.day = 1; + } + } + } else if self.current == 'W' { + // Compact ISO week and day (WwwD) + self.inc(); + + let iso_week = self.parse_integer(2, "iso week")?; + let mut iso_day: u32 = 1; + + if !self.end() && self.current != ' ' && self.current != 'T' { + iso_day = self.parse_integer(1, "iso day")?; + } + + match self.iso_to_ymd(datetime.year, iso_week, iso_day) { + Ok((year, month, day)) => { + datetime.year = year; + datetime.month = month; + datetime.day = day; + } + Err(error) => return Err(error), + } + } else { + /* + Month and day in compact format (MMDD) or ordinal date (DDD) + We'll assume first that the next part is a month and adjust if not. + */ + datetime.month = self.parse_integer(2, "month")?; + let mut ordinal_day = self.parse_integer(1, "ordinal day")? as i32; + + if self.end() || self.current == ' ' || self.current == 'T' { + // Ordinal day + ordinal_day += datetime.month as i32 * 10; + + let (year, month, day) = self.ordinal_to_ymd(datetime.year, ordinal_day, false)?; + + datetime.year = year; + datetime.month = month; + datetime.day = day; + } else { + // Day + datetime.day = ordinal_day as u32 * 10 + self.parse_integer(1, "day")?; + } + } + + if !self.end() { + self.parse_time(&mut datetime, false)?; + } + + if !self.end() { + if self.current == '/' && parsed.datetime.is_none() && parsed.duration.is_none() { + // Interval + parsed.datetime = Some(datetime); + + self.inc(); + + if self.current == 'P' { + // Duration + self.parse_duration(parsed)?; + } else { + self.parse_datetime(parsed)?; + } + + return Ok(()); + } + + return Err(self.parse_error("Unconverted data remains".to_string())); + } + + match &parsed.datetime { + Some(_) => { + parsed.second_datetime = Some(datetime); + } + None => match &parsed.duration { + Some(_) => { + parsed.second_datetime = Some(datetime); + } + None => { + parsed.datetime = Some(datetime); + } + }, + } + + Ok(()) + } + + fn parse_time( + &mut self, + datetime: &mut ParsedDateTime, + skip_hour: bool, + ) -> Result<(), ParseError> { + // TODO: Add support for decimal units + + // Date/Time separator + if self.current != 'T' && self.current != ' ' && !skip_hour { + return Err(self.parse_error(format!( + "Invalid character \"{}\" while parsing {}", + self.current, "date and time separator (\"T\" or \" \")" + ))); + } + + datetime.has_time = true; + + if !skip_hour { + self.inc(); + + // Hour + datetime.hour = self.parse_integer(2, "hour")?; + } + + if !self.end() && self.current != 'Z' && self.current != '+' && self.current != '-' { + // Optional minute and second + if self.current == ':' { + // Minute and second in extended format (mm:ss) + self.inc(); + + // Minute + datetime.minute = self.parse_integer(2, "minute")?; + + if !self.end() && self.current != 'Z' && self.current != '+' && self.current != '-' + { + // Optional second + if self.current != ':' { + return Err(self.parse_error(format!( + "Invalid character \"{}\" while parsing {}", + self.current, "time separator (\":\")" + ))); + } + + self.inc(); + + // Second + datetime.second = self.parse_integer(2, "second")?; + + if self.current == '.' || self.current == ',' { + // Optional fractional second + self.inc(); + + datetime.microsecond = 0; + let mut i: u8 = 0; + + while i < 6 { + if let Some(digit) = self.current.to_digit(10) { + datetime.microsecond = datetime.microsecond * 10 + digit; + } else if i == 0 { + // One digit minimum is required + return Err(self.unexpected_character_error("subsecond", 1)); + } else { + break; + } + + self.inc(); + i += 1; + } + + // Drop extraneous digits + while self.current.is_ascii_digit() { + self.inc(); + } + + // Expand missing microsecond + while i < 6 { + datetime.microsecond *= 10; + i += 1; + } + } + + if !datetime.extended_date_format { + return Err(self.parse_error("Cannot combine \"basic\" date format with \"extended\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`).".to_string())); + } + } + } else { + // Minute and second in compact format (mmss) + + // Minute + datetime.minute = self.parse_integer(2, "minute")?; + + if !self.end() && self.current != 'Z' && self.current != '+' && self.current != '-' + { + // Optional second + + datetime.second = self.parse_integer(2, "second")?; + + if self.current == '.' || self.current == ',' { + // Optional fractional second + self.inc(); + + datetime.microsecond = 0; + let mut i: u8 = 0; + + while i < 6 { + if let Some(digit) = self.current.to_digit(10) { + datetime.microsecond = datetime.microsecond * 10 + digit; + } else if i == 0 { + // One digit minimum is required + return Err(self.unexpected_character_error("subsecond", 1)); + } else { + break; + } + + self.inc(); + i += 1; + } + + // Drop extraneous digits + while self.current.is_ascii_digit() { + self.inc(); + } + + // Expand missing microsecond + while i < 6 { + datetime.microsecond *= 10; + i += 1; + } + } + } + + if datetime.extended_date_format { + return Err(self.parse_error("Cannot combine \"extended\" date format with \"basic\" time format (Should be either `YYYY-MM-DDThh:mm:ss` or `YYYYMMDDThhmmss`).".to_string())); + } + } + } + + if datetime.hour == 24 + && datetime.minute == 0 + && datetime.second == 0 + && datetime.microsecond == 0 + { + // Special case for 24:00:00, which is valid for ISO 8601. + // This is equivalent to 00:00:00 the next day. + // We will store the information for now. + datetime.time_is_midnight = true; + } + + if self.current == 'Z' { + // UTC + datetime.offset = Some(0); + datetime.tzname = Some("UTC".to_string()); + self.inc(); + } else if matches!(self.current, '+' | '-') { + // Optional timezone offset + let tzsign = if self.current == '+' { 1 } else { -1 }; + self.inc(); + // Offset hour + let tzhour = self.parse_integer(2, "timezone hour")? as i32; + if self.current == ':' { + // Optional separator + self.inc(); + } + let mut tzminute = if self.end() { + 0 + } else { + // Optional minute + self.parse_integer(2, "timezone minute")? as i32 + }; + tzminute += tzhour * 60; + tzminute *= tzsign; + if tzminute > 24 * 60 { + return Err(self.parse_error("Timezone offset is too large".to_string())); + } + datetime.offset = Some(tzminute * 60); + } + + Ok(()) + } + + fn parse_duration(&mut self, parsed: &mut Parsed) -> Result<(), ParseError> { + // Removing P operator + self.inc(); + + let mut duration: ParsedDuration = ParsedDuration::new(); + let mut got_t: bool = false; + let mut last_had_fraction = false; + + loop { + match self.current { + 'T' => { + if got_t { + return Err( + self.parse_error("Repeated time declaration in duration".to_string()) + ); + } + + got_t = true; + } + _c => { + let (value, op_fraction) = self.parse_duration_number_frac()?; + if last_had_fraction { + return Err(self.parse_error("Invalid duration fraction".to_string())); + } + + if op_fraction.is_some() { + last_had_fraction = true; + } + + if got_t { + match self.current { + 'H' => { + if duration.minutes != 0 + || duration.seconds != 0 + || duration.microseconds != 0 + { + return Err( + self.parse_error("Duration units out of order".to_string()) + ); + } + + duration.hours += value; + + if let Some(fraction) = op_fraction { + let extra_minutes = fraction * 60_f64; + let extra_full_minutes: f64 = extra_minutes.trunc(); + duration.minutes += extra_full_minutes as u32; + let extra_seconds = + ((extra_minutes - extra_full_minutes) * 60.0).round(); + let extra_full_seconds = extra_seconds.trunc(); + duration.seconds += extra_full_seconds as u32; + let micro_extra = ((extra_seconds - extra_full_seconds) + * 1_000_000.0) + .round() + as u32; + duration.microseconds += micro_extra; + } + } + 'M' => { + if duration.seconds != 0 || duration.microseconds != 0 { + return Err( + self.parse_error("Duration units out of order".to_string()) + ); + } + + duration.minutes += value; + + if let Some(fraction) = op_fraction { + let extra_seconds = fraction * 60_f64; + let extra_full_seconds = extra_seconds.trunc(); + duration.seconds += extra_full_seconds as u32; + let micro_extra = ((extra_seconds - extra_full_seconds) + * 1_000_000.0) + .round() + as u32; + duration.microseconds += micro_extra; + } + } + 'S' => { + duration.seconds = value; + + if let Some(fraction) = op_fraction { + duration.microseconds += + (fraction * 1_000_000.0).round() as u32; + } + } + _ => { + return Err( + self.parse_error("Invalid duration time unit".to_string()) + ) + } + } + } else { + match self.current { + 'Y' => { + if last_had_fraction { + return Err(self.parse_error( + "Fractional years in duration are not supported" + .to_string(), + )); + } + + if duration.months != 0 || duration.days != 0 { + return Err( + self.parse_error("Duration units out of order".to_string()) + ); + } + + duration.years = value; + } + 'M' => { + if last_had_fraction { + return Err(self.parse_error( + "Fractional months in duration are not supported" + .to_string(), + )); + } + + if duration.days != 0 { + return Err( + self.parse_error("Duration units out of order".to_string()) + ); + } + + duration.months = value; + } + 'W' => { + if duration.years != 0 || duration.months != 0 { + return Err(self.parse_error( + "Basic format durations cannot have weeks".to_string(), + )); + } + + duration.weeks = value; + + if let Some(fraction) = op_fraction { + let extra_days = fraction * 7_f64; + let extra_full_days = extra_days.trunc(); + duration.days += extra_full_days as u32; + let extra_hours = (extra_days - extra_full_days) * 24.0; + let extra_full_hours = extra_hours.trunc(); + duration.hours += extra_full_hours as u32; + let extra_minutes = + ((extra_hours - extra_full_hours) * 60.0).round(); + let extra_full_minutes: f64 = extra_minutes.trunc(); + duration.minutes += extra_full_minutes as u32; + let extra_seconds = + ((extra_minutes - extra_full_minutes) * 60.0).round(); + let extra_full_seconds = extra_seconds.trunc(); + duration.seconds += extra_full_seconds as u32; + let micro_extra = ((extra_seconds - extra_full_seconds) + * 1_000_000.0) + .round() + as u32; + duration.microseconds += micro_extra; + } + } + 'D' => { + if duration.weeks != 0 { + return Err(self.parse_error( + "Week format durations cannot have days".to_string(), + )); + } + + duration.days += value; + if let Some(fraction) = op_fraction { + let extra_hours = fraction * 24.0; + let extra_full_hours = extra_hours.trunc(); + duration.hours += extra_full_hours as u32; + let extra_minutes = + ((extra_hours - extra_full_hours) * 60.0).round(); + let extra_full_minutes: f64 = extra_minutes.trunc(); + duration.minutes += extra_full_minutes as u32; + let extra_seconds = + ((extra_minutes - extra_full_minutes) * 60.0).round(); + let extra_full_seconds = extra_seconds.trunc(); + duration.seconds += extra_full_seconds as u32; + let micro_extra = ((extra_seconds - extra_full_seconds) + * 1_000_000.0) + .round() + as u32; + duration.microseconds += micro_extra; + } + } + _ => { + return Err( + self.parse_error("Invalid duration time unit".to_string()) + ) + } + } + } + } + } + self.inc(); + + if self.end() { + break; + } + } + + parsed.duration = Some(duration); + + Ok(()) + } + + fn parse_duration_number_frac(&mut self) -> Result<(u32, Option), ParseError> { + let value = self.parse_duration_number()?; + let fraction = matches!(self.current, '.' | ',').then(|| { + let mut decimal = 0_f64; + let mut denominator = 1_f64; + + while let Some(digit) = self.inc().and_then(|ch| ch.to_digit(10)) { + decimal *= 10.0; + decimal += f64::from(digit); + denominator *= 10.0; + } + + decimal / denominator + }); + + Ok((value, fraction)) + } + + fn parse_duration_number(&mut self) -> Result { + let Some(mut value) = self.current.to_digit(10) else { + return Err(self.parse_error("Invalid number in duration".to_string())); + }; + + while let Some(digit) = self.inc().and_then(|ch| ch.to_digit(10)) { + value *= 10; + value += digit; + } + + Ok(value) + } + + fn iso_to_ymd( + &mut self, + iso_year: u32, + iso_week: u32, + iso_day: u32, + ) -> Result<(u32, u32, u32), ParseError> { + if iso_week > 53 || iso_week > 52 && !is_long_year(iso_year as i32) { + return Err(ParseError { + index: self.idx, + message: format!( + "Invalid ISO date: week {iso_week} out of range for year {iso_year}" + ), + }); + } + + if iso_day > 7 { + return Err(ParseError { + index: self.idx, + message: "Invalid ISO date: week day is invalid".to_string(), + }); + } + + let ordinal: i32 = + iso_week as i32 * 7 + iso_day as i32 - (week_day(iso_year as i32, 1, 4) as i32 + 3); + + self.ordinal_to_ymd(iso_year, ordinal, true) + } + + fn ordinal_to_ymd( + &mut self, + year: u32, + ordinal: i32, + allow_out_of_bounds: bool, + ) -> Result<(u32, u32, u32), ParseError> { + let mut ord: i32 = ordinal; + let mut y: u32 = year; + let mut leap: usize = usize::from(is_leap(y as i32)); + + if ord < 1 { + if !allow_out_of_bounds { + return Err(self.parse_error(format!( + "Invalid ordinal day: {ordinal} is too small for year {year}" + ))); + } + // Previous year + ord += days_in_year((year - 1) as i32) as i32; + y -= 1; + leap = usize::from(is_leap(y as i32)); + } + + if ord > days_in_year(y as i32) as i32 { + if !allow_out_of_bounds { + return Err(self.parse_error(format!( + "Invalid ordinal day: {ordinal} is too large for year {year}" + ))); + } + + // Next year + ord -= days_in_year(y as i32) as i32; + y += 1; + leap = usize::from(is_leap(y as i32)); + } + + for i in 1..14 { + if ord <= MONTHS_OFFSETS[leap][i] { + let day = ord as u32 - MONTHS_OFFSETS[leap][i - 1] as u32; + let month = (i - 1) as u32; + + return Ok((y, month, day)); + } + } + + Err(self.parse_error(format!( + "Invalid ordinal day: {ordinal} is too large for year {year}" + ))) + } +} diff --git a/rust/src/python/helpers.rs b/rust/src/python/helpers.rs new file mode 100644 index 00000000..979258a0 --- /dev/null +++ b/rust/src/python/helpers.rs @@ -0,0 +1,377 @@ +#![allow(clippy::useless_conversion)] +use std::cmp::Ordering; + +use pyo3::{ + prelude::*, + types::{PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyString, PyTimeAccess}, + PyTypeInfo, +}; + +use crate::{ + constants::{DAYS_PER_MONTHS, SECS_PER_DAY, SECS_PER_HOUR, SECS_PER_MIN}, + helpers, +}; + +use crate::python::types::PreciseDiff; + +struct DateTimeInfo<'py> { + pub year: i32, + pub month: i32, + pub day: i32, + pub hour: i32, + pub minute: i32, + pub second: i32, + pub microsecond: i32, + pub total_seconds: i32, + pub offset: i32, + pub tz: &'py str, + pub is_datetime: bool, +} + +impl PartialEq for DateTimeInfo<'_> { + fn eq(&self, other: &Self) -> bool { + ( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + ) + .eq(&( + other.year, + other.month, + other.day, + other.hour, + other.minute, + other.second, + other.microsecond, + )) + } +} + +impl PartialOrd for DateTimeInfo<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + ( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + ) + .partial_cmp(&( + other.year, + other.month, + other.day, + other.hour, + other.minute, + other.second, + other.microsecond, + )) + } +} + +pub fn get_tz_name<'py>(dt: &Bound<'py, PyAny>) -> PyResult { + // let tz: &str = ""; + + if !PyDateTime::is_type_of(dt) { + return Ok(String::new()); + } + + let tzinfo: Bound<'py, PyAny> = dt.getattr("tzinfo")?; + + if tzinfo.is_none() { + return Ok(String::new()); + } + + let tzname_attr: Option>; + + if tzinfo.hasattr("key")? { + tzname_attr = Some(tzinfo.getattr("key")?); + } else if tzinfo.hasattr("name")? { + tzname_attr = Some(tzinfo.getattr("name")?); + } else if tzinfo.hasattr("zone")? { + tzname_attr = Some(tzinfo.getattr("zone")?); + } else { + tzname_attr = None; + } + + if let Some(tzname_attr) = tzname_attr { + let tzname: &Bound = tzname_attr.cast()?; + tzname.extract() + } else { + Ok(String::new()) + } +} + +pub fn get_offset(dt: &Bound) -> PyResult { + if !PyDateTime::is_type_of(dt) { + return Ok(0); + } + + let tzinfo = dt.getattr("tzinfo")?; + + if tzinfo.is_none() { + return Ok(0); + } + let binding = tzinfo.call_method1("utcoffset", (dt,))?; + let offset: &Bound = binding.cast()?; + + Ok(offset.get_days() * SECS_PER_DAY as i32 + offset.get_seconds()) +} + +#[pyfunction] +pub fn is_leap(year: i32) -> bool { + helpers::is_leap(year) +} + +#[pyfunction] +pub fn is_long_year(year: i32) -> bool { + helpers::is_long_year(year) +} + +#[pyfunction] +pub fn week_day(year: i32, month: u32, day: u32) -> u32 { + helpers::week_day(year, month, day) +} + +#[pyfunction] +pub fn days_in_year(year: i32) -> u32 { + helpers::days_in_year(year) +} + +#[pyfunction] +pub fn local_time( + unix_time: f64, + utc_offset: isize, + microsecond: usize, +) -> (usize, usize, usize, usize, usize, usize, usize) { + helpers::local_time(unix_time, utc_offset, microsecond) +} + +#[pyfunction] +pub fn precise_diff<'py>( + dt1: &Bound<'py, PyAny>, + dt2: &Bound<'py, PyAny>, +) -> PyResult { + let mut sign = 1; + let dt1_tz = get_tz_name(dt1)?; + let dt2_tz = get_tz_name(dt2)?; + let mut dtinfo1 = DateTimeInfo { + year: dt1.cast::()?.get_year(), + month: i32::from(dt1.cast::()?.get_month()), + day: i32::from(dt1.cast::()?.get_day()), + hour: 0, + minute: 0, + second: 0, + microsecond: 0, + total_seconds: 0, + tz: dt1_tz.as_str(), + offset: get_offset(dt1)?, + is_datetime: PyDateTime::is_type_of(dt1), + }; + let mut dtinfo2 = DateTimeInfo { + year: dt2.cast::()?.get_year(), + month: i32::from(dt2.cast::()?.get_month()), + day: i32::from(dt2.cast::()?.get_day()), + hour: 0, + minute: 0, + second: 0, + microsecond: 0, + total_seconds: 0, + tz: dt2_tz.as_str(), + offset: get_offset(dt2)?, + is_datetime: PyDateTime::is_exact_type_of(dt2), + }; + let in_same_tz: bool = dtinfo1.tz == dtinfo2.tz && !dtinfo1.tz.is_empty(); + let mut total_days = helpers::day_number(dtinfo2.year, dtinfo2.month as u8, dtinfo2.day as u8) + - helpers::day_number(dtinfo1.year, dtinfo1.month as u8, dtinfo1.day as u8); + + if dtinfo1.is_datetime { + let dt1dt: &Bound = dt1.cast()?; + + dtinfo1.hour = i32::from(dt1dt.get_hour()); + dtinfo1.minute = i32::from(dt1dt.get_minute()); + dtinfo1.second = i32::from(dt1dt.get_second()); + dtinfo1.microsecond = dt1dt.get_microsecond() as i32; + + if !in_same_tz && dtinfo1.offset != 0 || total_days == 0 { + dtinfo1.hour -= dtinfo1.offset / SECS_PER_HOUR as i32; + dtinfo1.offset %= SECS_PER_HOUR as i32; + dtinfo1.minute -= dtinfo1.offset / SECS_PER_MIN as i32; + dtinfo1.offset %= SECS_PER_MIN as i32; + dtinfo1.second -= dtinfo1.offset; + + if dtinfo1.second < 0 { + dtinfo1.second += 60; + dtinfo1.minute -= 1; + } else if dtinfo1.second > 60 { + dtinfo1.second -= 60; + dtinfo1.minute += 1; + } + + if dtinfo1.minute < 0 { + dtinfo1.minute += 60; + dtinfo1.hour -= 1; + } else if dtinfo1.minute > 60 { + dtinfo1.minute -= 60; + dtinfo1.hour += 1; + } + + if dtinfo1.hour < 0 { + dtinfo1.hour += 24; + dtinfo1.day -= 1; + } else if dtinfo1.hour > 24 { + dtinfo1.hour -= 24; + dtinfo1.day += 1; + } + } + + dtinfo1.total_seconds = dtinfo1.hour * SECS_PER_HOUR as i32 + + dtinfo1.minute * SECS_PER_MIN as i32 + + dtinfo1.second; + } + + if dtinfo2.is_datetime { + let dt2dt: &Bound = dt2.cast()?; + + dtinfo2.hour = i32::from(dt2dt.get_hour()); + dtinfo2.minute = i32::from(dt2dt.get_minute()); + dtinfo2.second = i32::from(dt2dt.get_second()); + dtinfo2.microsecond = dt2dt.get_microsecond() as i32; + + if !in_same_tz && dtinfo2.offset != 0 || total_days == 0 { + dtinfo2.hour -= dtinfo2.offset / SECS_PER_HOUR as i32; + dtinfo2.offset %= SECS_PER_HOUR as i32; + dtinfo2.minute -= dtinfo2.offset / SECS_PER_MIN as i32; + dtinfo2.offset %= SECS_PER_MIN as i32; + dtinfo2.second -= dtinfo2.offset; + + if dtinfo2.second < 0 { + dtinfo2.second += 60; + dtinfo2.minute -= 1; + } else if dtinfo2.second > 60 { + dtinfo2.second -= 60; + dtinfo2.minute += 1; + } + + if dtinfo2.minute < 0 { + dtinfo2.minute += 60; + dtinfo2.hour -= 1; + } else if dtinfo2.minute > 60 { + dtinfo2.minute -= 60; + dtinfo2.hour += 1; + } + + if dtinfo2.hour < 0 { + dtinfo2.hour += 24; + dtinfo2.day -= 1; + } else if dtinfo2.hour > 24 { + dtinfo2.hour -= 24; + dtinfo2.day += 1; + } + } + + dtinfo2.total_seconds = dtinfo2.hour * SECS_PER_HOUR as i32 + + dtinfo2.minute * SECS_PER_MIN as i32 + + dtinfo2.second; + } + + if dtinfo1 > dtinfo2 { + sign = -1; + (dtinfo1, dtinfo2) = (dtinfo2, dtinfo1); + + total_days = -total_days; + } + + let mut year_diff = dtinfo2.year - dtinfo1.year; + let mut month_diff = dtinfo2.month - dtinfo1.month; + let mut day_diff = dtinfo2.day - dtinfo1.day; + let mut hour_diff = dtinfo2.hour - dtinfo1.hour; + let mut minute_diff = dtinfo2.minute - dtinfo1.minute; + let mut second_diff = dtinfo2.second - dtinfo1.second; + let mut microsecond_diff = dtinfo2.microsecond - dtinfo1.microsecond; + + if microsecond_diff < 0 { + microsecond_diff += 1_000_000; + second_diff -= 1; + } + + if second_diff < 0 { + second_diff += 60; + minute_diff -= 1; + } + + if minute_diff < 0 { + minute_diff += 60; + hour_diff -= 1; + } + + if hour_diff < 0 { + hour_diff += 24; + day_diff -= 1; + } + + if day_diff < 0 { + // If we have a difference in days, + // we have to check if they represent months + let mut year = dtinfo2.year; + let mut month = dtinfo2.month; + + if month == 1 { + month = 12; + year -= 1; + } else { + month -= 1; + } + + let leap = helpers::is_leap(year); + + let days_in_last_month = DAYS_PER_MONTHS[usize::from(leap)][month as usize]; + let days_in_month = + DAYS_PER_MONTHS[usize::from(helpers::is_leap(dtinfo2.year))][dtinfo2.month as usize]; + + match day_diff.cmp(&(days_in_month - days_in_last_month)) { + Ordering::Less => { + // We don't have a full month, we calculate days + if days_in_last_month < dtinfo1.day { + day_diff += dtinfo1.day; + } else { + day_diff += days_in_last_month; + } + } + Ordering::Equal => { + // We have exactly a full month + // We remove the days difference + // and add one to the months difference + day_diff = 0; + month_diff += 1; + } + Ordering::Greater => { + // We have a full month + day_diff += days_in_last_month; + } + } + + month_diff -= 1; + } + + if month_diff < 0 { + month_diff += 12; + year_diff -= 1; + } + + Ok(PreciseDiff { + years: year_diff * sign, + months: month_diff * sign, + days: day_diff * sign, + hours: hour_diff * sign, + minutes: minute_diff * sign, + seconds: second_diff * sign, + microseconds: microsecond_diff * sign, + total_days: total_days * sign, + }) +} diff --git a/rust/src/python/mod.rs b/rust/src/python/mod.rs new file mode 100644 index 00000000..6adb0277 --- /dev/null +++ b/rust/src/python/mod.rs @@ -0,0 +1,24 @@ +use pyo3::prelude::*; + +mod helpers; +mod parsing; +mod types; + +use helpers::{days_in_year, is_leap, is_long_year, local_time, precise_diff, week_day}; +use parsing::parse_iso8601; +use types::{Duration, PreciseDiff}; + +#[pymodule] +pub fn _pendulum(_py: Python<'_>, m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(days_in_year, m)?)?; + m.add_function(wrap_pyfunction!(is_leap, m)?)?; + m.add_function(wrap_pyfunction!(is_long_year, m)?)?; + m.add_function(wrap_pyfunction!(local_time, m)?)?; + m.add_function(wrap_pyfunction!(week_day, m)?)?; + m.add_function(wrap_pyfunction!(parse_iso8601, m)?)?; + m.add_function(wrap_pyfunction!(precise_diff, m)?)?; + m.add_class::()?; + m.add_class::()?; + + Ok(()) +} diff --git a/rust/src/python/parsing.rs b/rust/src/python/parsing.rs new file mode 100644 index 00000000..90fc275c --- /dev/null +++ b/rust/src/python/parsing.rs @@ -0,0 +1,119 @@ +#![allow(clippy::useless_conversion)] +use pyo3::exceptions; +use pyo3::prelude::*; +use pyo3::types::PyDate; +use pyo3::types::PyDateTime; +use pyo3::types::PyTime; +use pyo3::IntoPyObjectExt; + +use crate::parsing::Parser; +use crate::python::types::{Duration, FixedTimezone}; + +#[pyfunction] +pub fn parse_iso8601(py: Python, input: &str) -> PyResult> { + let parsed = Parser::new(input).parse(); + + match parsed { + Ok(parsed) => match (parsed.datetime, parsed.duration, parsed.second_datetime) { + (Some(datetime), None, None) => match (datetime.has_date, datetime.has_time) { + (true, true) => match datetime.offset { + Some(offset) => { + let dt = PyDateTime::new( + py, + datetime.year as i32, + datetime.month as u8, + datetime.day as u8, + datetime.hour as u8, + datetime.minute as u8, + datetime.second as u8, + datetime.microsecond, + Some( + Py::new(py, FixedTimezone::new(offset, datetime.tzname))? + .into_any() + .cast_bound(py)?, + ), + )?; + + Ok(dt.into_any().unbind()) + } + None => { + let dt = PyDateTime::new( + py, + datetime.year as i32, + datetime.month as u8, + datetime.day as u8, + datetime.hour as u8, + datetime.minute as u8, + datetime.second as u8, + datetime.microsecond, + None, + )?; + + Ok(dt.into_any().unbind()) + } + }, + (true, false) => { + let dt = PyDate::new( + py, + datetime.year as i32, + datetime.month as u8, + datetime.day as u8, + )?; + + Ok(dt.into_any().unbind()) + } + (false, true) => match datetime.offset { + Some(offset) => { + let dt = PyTime::new( + py, + datetime.hour as u8, + datetime.minute as u8, + datetime.second as u8, + datetime.microsecond, + Some( + Py::new(py, FixedTimezone::new(offset, datetime.tzname))? + .into_any() + .cast_bound(py)?, + ), + )?; + + Ok(dt.into_any().unbind()) + } + None => { + let dt = PyTime::new( + py, + datetime.hour as u8, + datetime.minute as u8, + datetime.second as u8, + datetime.microsecond, + None, + )?; + + Ok(dt.into_any().unbind()) + } + }, + (_, _) => Err(exceptions::PyValueError::new_err( + "Parsing error".to_string(), + )), + }, + (None, Some(duration), None) => Ok(Py::new( + py, + Duration::new( + Some(duration.years), + Some(duration.months), + Some(duration.weeks), + Some(duration.days), + Some(duration.hours), + Some(duration.minutes), + Some(duration.seconds), + Some(duration.microseconds), + ), + )? + .into_py_any(py)?), + (_, _, _) => Err(exceptions::PyValueError::new_err( + "Not yet implemented".to_string(), + )), + }, + Err(error) => Err(exceptions::PyValueError::new_err(error.to_string())), + } +} diff --git a/rust/src/python/types/duration.rs b/rust/src/python/types/duration.rs new file mode 100644 index 00000000..fc18f4eb --- /dev/null +++ b/rust/src/python/types/duration.rs @@ -0,0 +1,59 @@ +use pyo3::prelude::*; + +#[pyclass(module = "_pendulum")] +pub struct Duration { + #[pyo3(get, set)] + pub years: u32, + #[pyo3(get, set)] + pub months: u32, + #[pyo3(get, set)] + pub weeks: u32, + #[pyo3(get, set)] + pub days: u32, + #[pyo3(get, set)] + pub hours: u32, + #[pyo3(get, set)] + pub minutes: u32, + #[pyo3(get, set)] + pub seconds: u32, + #[pyo3(get, set)] + pub microseconds: u32, +} + +#[pymethods] +impl Duration { + #[new] + #[pyo3(signature = (years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, microseconds=0))] + #[allow(clippy::too_many_arguments)] + pub fn new( + years: Option, + months: Option, + weeks: Option, + days: Option, + hours: Option, + minutes: Option, + seconds: Option, + microseconds: Option, + ) -> Self { + Self { + years: years.unwrap_or(0), + months: months.unwrap_or(0), + weeks: weeks.unwrap_or(0), + days: days.unwrap_or(0), + hours: hours.unwrap_or(0), + minutes: minutes.unwrap_or(0), + seconds: seconds.unwrap_or(0), + microseconds: microseconds.unwrap_or(0), + } + } + + #[getter] + fn remaining_days(&self) -> PyResult { + Ok(self.days) + } + + #[getter] + fn remaining_seconds(&self) -> PyResult { + Ok(self.seconds) + } +} diff --git a/rust/src/python/types/interval.rs b/rust/src/python/types/interval.rs new file mode 100644 index 00000000..71374934 --- /dev/null +++ b/rust/src/python/types/interval.rs @@ -0,0 +1,46 @@ +use pyo3::prelude::*; + +use pyo3::types::PyDelta; + +#[pyclass(extends=PyDelta)] +#[derive(Default)] +pub struct Interval { + pub days: i32, + pub seconds: i32, + pub microseconds: i32, +} + +#[pymethods] +impl Interval { + #[new] + #[pyo3(signature = (days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0))] + pub fn new( + py: Python, + days: Option, + seconds: Option, + microseconds: Option, + milliseconds: Option, + minutes: Option, + hours: Option, + weeks: Option, + ) -> PyResult { + println!("{} days", 31); + PyDelta::new( + py, + days.unwrap_or(0), + seconds.unwrap_or(0), + microseconds.unwrap_or(0), + true, + )?; + + let f = Ok(Self { + days: days.unwrap_or(0), + seconds: seconds.unwrap_or(0), + microseconds: microseconds.unwrap_or(0), + }); + + println!("{} days", 31); + + f + } +} diff --git a/rust/src/python/types/mod.rs b/rust/src/python/types/mod.rs new file mode 100644 index 00000000..cba11dfe --- /dev/null +++ b/rust/src/python/types/mod.rs @@ -0,0 +1,7 @@ +mod duration; +mod precise_diff; +mod timezone; + +pub use duration::Duration; +pub use precise_diff::PreciseDiff; +pub use timezone::FixedTimezone; diff --git a/rust/src/python/types/precise_diff.rs b/rust/src/python/types/precise_diff.rs new file mode 100644 index 00000000..64ca3a65 --- /dev/null +++ b/rust/src/python/types/precise_diff.rs @@ -0,0 +1,53 @@ +use pyo3::prelude::*; + +#[pyclass(module = "_pendulum")] +pub struct PreciseDiff { + #[pyo3(get, set)] + pub years: i32, + #[pyo3(get, set)] + pub months: i32, + #[pyo3(get, set)] + pub days: i32, + #[pyo3(get, set)] + pub hours: i32, + #[pyo3(get, set)] + pub minutes: i32, + #[pyo3(get, set)] + pub seconds: i32, + #[pyo3(get, set)] + pub microseconds: i32, + #[pyo3(get, set)] + pub total_days: i32, +} + +#[pymethods] +impl PreciseDiff { + #[new] + #[pyo3(signature = (years=0, months=0, days=0, hours=0, minutes=0, seconds=0, microseconds=0, total_days=0))] + #[allow(clippy::too_many_arguments)] + pub fn new( + years: Option, + months: Option, + days: Option, + hours: Option, + minutes: Option, + seconds: Option, + microseconds: Option, + total_days: Option, + ) -> Self { + Self { + years: years.unwrap_or(0), + months: months.unwrap_or(0), + days: days.unwrap_or(0), + hours: hours.unwrap_or(0), + minutes: minutes.unwrap_or(0), + seconds: seconds.unwrap_or(0), + microseconds: microseconds.unwrap_or(0), + total_days: total_days.unwrap_or(0), + } + } + + fn __repr__(&self) -> String { + format!("PreciseDiff(years={}, months={}, days={}, hours={}, minutes={}, seconds={}, microseconds={}, total_days={})", self.years, self.months, self.days, self.hours, self.minutes, self.seconds, self.microseconds, self.total_days) + } +} diff --git a/rust/src/python/types/timezone.rs b/rust/src/python/types/timezone.rs new file mode 100644 index 00000000..64dce026 --- /dev/null +++ b/rust/src/python/types/timezone.rs @@ -0,0 +1,62 @@ +#![allow(clippy::useless_conversion)] +use pyo3::prelude::*; +use pyo3::types::{PyDelta, PyDict, PyTzInfo}; + +#[pyclass(module = "_pendulum", extends = PyTzInfo)] +#[derive(Clone)] +pub struct FixedTimezone { + offset: i32, + name: Option, +} + +#[pymethods] +impl FixedTimezone { + #[new] + #[pyo3(signature = (offset, name=None))] + pub fn new(offset: i32, name: Option) -> Self { + Self { offset, name } + } + + fn utcoffset<'p>( + &self, + py: Python<'p>, + _dt: &Bound<'p, PyAny>, + ) -> Result, PyErr> { + PyDelta::new(py, 0, self.offset, 0, true) + } + + fn tzname(&self, _dt: &Bound) -> String { + self.__str__() + } + + fn dst<'p>( + &self, + py: Python<'p>, + _dt: &Bound<'p, PyAny>, + ) -> Result, PyErr> { + PyDelta::new(py, 0, 0, 0, true) + } + + fn __repr__(&self) -> String { + format!( + "FixedTimezone({}, name=\"{}\")", + self.offset, + self.__str__() + ) + } + + fn __str__(&self) -> String { + if let Some(n) = &self.name { + n.clone() + } else { + let sign = if self.offset < 0 { "-" } else { "+" }; + let minutes = self.offset.abs() / 60; + let (hour, minute) = (minutes / 60, minutes % 60); + format!("{sign}{hour:.2}:{minute:.2}") + } + } + + fn __deepcopy__(&self, py: Python, _memo: &Bound) -> PyResult> { + Py::new(py, self.clone()) + } +} diff --git a/src/pendulum/__init__.py b/src/pendulum/__init__.py new file mode 100644 index 00000000..16ae0865 --- /dev/null +++ b/src/pendulum/__init__.py @@ -0,0 +1,440 @@ +from __future__ import annotations + +import datetime as _datetime + +from functools import cache +from typing import TYPE_CHECKING +from typing import Any +from typing import cast +from typing import overload + +from pendulum.constants import DAYS_PER_WEEK +from pendulum.constants import HOURS_PER_DAY +from pendulum.constants import MINUTES_PER_HOUR +from pendulum.constants import MONTHS_PER_YEAR +from pendulum.constants import SECONDS_PER_DAY +from pendulum.constants import SECONDS_PER_HOUR +from pendulum.constants import SECONDS_PER_MINUTE +from pendulum.constants import WEEKS_PER_YEAR +from pendulum.constants import YEARS_PER_CENTURY +from pendulum.constants import YEARS_PER_DECADE +from pendulum.date import Date +from pendulum.datetime import DateTime +from pendulum.day import WeekDay +from pendulum.duration import Duration +from pendulum.formatting import Formatter +from pendulum.helpers import format_diff +from pendulum.helpers import get_locale +from pendulum.helpers import locale +from pendulum.helpers import set_locale +from pendulum.helpers import week_ends_at +from pendulum.helpers import week_starts_at +from pendulum.interval import Interval +from pendulum.parser import parse as parse +from pendulum.time import Time +from pendulum.tz import UTC +from pendulum.tz import fixed_timezone +from pendulum.tz import local_timezone +from pendulum.tz import set_local_timezone +from pendulum.tz import test_local_timezone +from pendulum.tz import timezones +from pendulum.tz.timezone import FixedTimezone +from pendulum.tz.timezone import Timezone + + +MONDAY = WeekDay.MONDAY +TUESDAY = WeekDay.TUESDAY +WEDNESDAY = WeekDay.WEDNESDAY +THURSDAY = WeekDay.THURSDAY +FRIDAY = WeekDay.FRIDAY +SATURDAY = WeekDay.SATURDAY +SUNDAY = WeekDay.SUNDAY + +_TEST_NOW: DateTime | None = None +_LOCALE = "en" +_WEEK_STARTS_AT: WeekDay = WeekDay.MONDAY +_WEEK_ENDS_AT: WeekDay = WeekDay.SUNDAY + +_formatter = Formatter() + + +@overload +def timezone(name: int) -> FixedTimezone: ... + + +@overload +def timezone(name: str) -> Timezone: ... + + +@overload +def timezone(name: str | int) -> Timezone | FixedTimezone: ... + + +def timezone(name: str | int) -> Timezone | FixedTimezone: + """ + Return a Timezone instance given its name. + """ + if isinstance(name, int): + return fixed_timezone(name) + + if name.lower() == "utc": + return UTC + + return Timezone(name) + + +def _safe_timezone( + obj: str | float | _datetime.tzinfo | Timezone | FixedTimezone | None, + dt: _datetime.datetime | None = None, +) -> Timezone | FixedTimezone: + """ + Creates a timezone instance + from a string, Timezone, TimezoneInfo or integer offset. + """ + if isinstance(obj, (Timezone, FixedTimezone)): + return obj + + if obj is None or obj == "local": + return local_timezone() + + if isinstance(obj, (int, float)): + obj = int(obj * 60 * 60) + elif isinstance(obj, _datetime.tzinfo): + # zoneinfo + if hasattr(obj, "key"): + obj = obj.key + # pytz + elif hasattr(obj, "localize"): + obj = obj.zone # type: ignore[attr-defined] + elif obj.tzname(None) == "UTC": + return UTC + else: + offset = obj.utcoffset(dt) + + if offset is None: + offset = _datetime.timedelta(0) + + obj = int(offset.total_seconds()) + + obj = cast("str | int", obj) + + return timezone(obj) + + +# Public API +def datetime( + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + tz: str | float | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, + fold: int = 1, + raise_on_unknown_times: bool = False, +) -> DateTime: + """ + Creates a new DateTime instance from a specific date and time. + """ + return DateTime.create( + year, + month, + day, + hour=hour, + minute=minute, + second=second, + microsecond=microsecond, + tz=tz, + fold=fold, + raise_on_unknown_times=raise_on_unknown_times, + ) + + +def local( + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, +) -> DateTime: + """ + Return a DateTime in the local timezone. + """ + return datetime( + year, month, day, hour, minute, second, microsecond, tz=local_timezone() + ) + + +def naive( + year: int, + month: int, + day: int, + hour: int = 0, + minute: int = 0, + second: int = 0, + microsecond: int = 0, + fold: int = 1, +) -> DateTime: + """ + Return a naive DateTime. + """ + return DateTime(year, month, day, hour, minute, second, microsecond, fold=fold) + + +def date(year: int, month: int, day: int) -> Date: + """ + Create a new Date instance. + """ + return Date(year, month, day) + + +def time(hour: int, minute: int = 0, second: int = 0, microsecond: int = 0) -> Time: + """ + Create a new Time instance. + """ + return Time(hour, minute, second, microsecond) + + +@overload +def instance( + obj: _datetime.datetime, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> DateTime: ... + + +@overload +def instance( + obj: _datetime.date, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> Date: ... + + +@overload +def instance( + obj: _datetime.time, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> Time: ... + + +def instance( + obj: _datetime.datetime | _datetime.date | _datetime.time, + tz: str | Timezone | FixedTimezone | _datetime.tzinfo | None = UTC, +) -> DateTime | Date | Time: + """ + Create a DateTime/Date/Time instance from a datetime/date/time native one. + """ + if isinstance(obj, (DateTime, Date, Time)): + return obj + + if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime): + return date(obj.year, obj.month, obj.day) + + if isinstance(obj, _datetime.time): + return Time.instance(obj, tz=tz) + + return DateTime.instance(obj, tz=tz) + + +def now(tz: str | Timezone | None = None) -> DateTime: + """ + Get a DateTime instance for the current date and time. + """ + return DateTime.now(tz) + + +def today(tz: str | Timezone = "local") -> DateTime: + """ + Create a DateTime instance for today. + """ + return now(tz).start_of("day") + + +def tomorrow(tz: str | Timezone = "local") -> DateTime: + """ + Create a DateTime instance for tomorrow. + """ + return today(tz).add(days=1) + + +def yesterday(tz: str | Timezone = "local") -> DateTime: + """ + Create a DateTime instance for yesterday. + """ + return today(tz).subtract(days=1) + + +def from_format( + string: str, + fmt: str, + tz: str | Timezone = UTC, + locale: str | None = None, +) -> DateTime: + """ + Creates a DateTime instance from a specific format. + """ + parts = _formatter.parse(string, fmt, now(tz=tz), locale=locale) + if parts["tz"] is None: + parts["tz"] = tz + + return datetime(**parts) + + +def from_timestamp(timestamp: int | float, tz: str | Timezone = UTC) -> DateTime: + """ + Create a DateTime instance from a timestamp. + """ + dt = _datetime.datetime.fromtimestamp(timestamp, tz=UTC) + + dt = datetime( + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond + ) + + if tz is not UTC or tz != "UTC": + dt = dt.in_timezone(tz) + + return dt + + +def duration( + days: float = 0, + seconds: float = 0, + microseconds: float = 0, + milliseconds: float = 0, + minutes: float = 0, + hours: float = 0, + weeks: float = 0, + years: float = 0, + months: float = 0, +) -> Duration: + """ + Create a Duration instance. + """ + return Duration( + days=days, + seconds=seconds, + microseconds=microseconds, + milliseconds=milliseconds, + minutes=minutes, + hours=hours, + weeks=weeks, + years=years, + months=months, + ) + + +def interval( + start: DateTime, end: DateTime, absolute: bool = False +) -> Interval[DateTime]: + """ + Create an Interval instance. + """ + return Interval(start, end, absolute=absolute) + + +if TYPE_CHECKING: + from pendulum.testing.traveller import Traveller + + _traveller = Traveller(DateTime) + freeze = _traveller.freeze + travel = _traveller.travel + travel_to = _traveller.travel_to + travel_back = _traveller.travel_back +else: + # We do this in an if-not-typing block so we don't have to duplicate the function signatures. + @cache + def _traveller() -> Traveller: + # Lazy load this, so we don't eagerly load Pytest if we don't need to + from pendulum.testing.traveller import Traveller + + return Traveller(DateTime) + + def freeze(*args, **kwargs) -> Traveller: + return _traveller().freeze(*args, **kwargs) + + def travel(*args, **kwargs): + return _traveller().travel(*args, **kwargs) + + def travel_to(*args, **kwargs): + return _traveller().travel_to(*args, **kwargs) + + def travel_back(*args, **kwargs): + return _traveller().travel_back(*args, **kwargs) + + +def __getattr__(name: str) -> Any: + if name == "Traveller": + # This wasn't in `__all__`, but it was defined before, so keep it for back compat + from pendulum.testing.traveller import Traveller + + return Traveller + + if name == "__version__": + import importlib.metadata + import warnings + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " Pendulum 3.4. Use 'importlib.metadata.version(\"pendulum\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("pendulum") + + raise AttributeError(name) + + +__all__ = [ + "DAYS_PER_WEEK", + "HOURS_PER_DAY", + "MINUTES_PER_HOUR", + "MONTHS_PER_YEAR", + "SECONDS_PER_DAY", + "SECONDS_PER_HOUR", + "SECONDS_PER_MINUTE", + "UTC", + "WEEKS_PER_YEAR", + "YEARS_PER_CENTURY", + "YEARS_PER_DECADE", + "Date", + "DateTime", + "Duration", + "FixedTimezone", + "Formatter", + "Interval", + "Time", + "Timezone", + "WeekDay", + "date", + "datetime", + "duration", + "format_diff", + "freeze", + "from_format", + "from_timestamp", + "get_locale", + "instance", + "interval", + "local", + "local_timezone", + "locale", + "naive", + "now", + "parse", + "set_local_timezone", + "set_locale", + "test_local_timezone", + "time", + "timezone", + "timezones", + "today", + "tomorrow", + "travel", + "travel_back", + "travel_to", + "week_ends_at", + "week_starts_at", + "yesterday", +] diff --git a/pendulum/_extensions/helpers.py b/src/pendulum/_helpers.py similarity index 63% rename from pendulum/_extensions/helpers.py rename to src/pendulum/_helpers.py index 0132c0c9..bb989625 100644 --- a/pendulum/_extensions/helpers.py +++ b/src/pendulum/_helpers.py @@ -1,64 +1,69 @@ +from __future__ import annotations + import datetime import math -import typing - -from collections import namedtuple - -from ..constants import DAY_OF_WEEK_TABLE -from ..constants import DAYS_PER_L_YEAR -from ..constants import DAYS_PER_MONTHS -from ..constants import DAYS_PER_N_YEAR -from ..constants import EPOCH_YEAR -from ..constants import MONTHS_OFFSETS -from ..constants import SECS_PER_4_YEARS -from ..constants import SECS_PER_100_YEARS -from ..constants import SECS_PER_400_YEARS -from ..constants import SECS_PER_DAY -from ..constants import SECS_PER_HOUR -from ..constants import SECS_PER_MIN -from ..constants import SECS_PER_YEAR -from ..constants import TM_DECEMBER -from ..constants import TM_JANUARY - - -class PreciseDiff( - namedtuple( - "PreciseDiff", - "years months days " "hours minutes seconds microseconds " "total_days", - ) -): - def __repr__(self): + +from typing import TYPE_CHECKING +from typing import NamedTuple +from typing import cast + +from pendulum.constants import DAY_OF_WEEK_TABLE +from pendulum.constants import DAYS_PER_L_YEAR +from pendulum.constants import DAYS_PER_MONTHS +from pendulum.constants import DAYS_PER_N_YEAR +from pendulum.constants import EPOCH_YEAR +from pendulum.constants import MONTHS_OFFSETS +from pendulum.constants import SECS_PER_4_YEARS +from pendulum.constants import SECS_PER_100_YEARS +from pendulum.constants import SECS_PER_400_YEARS +from pendulum.constants import SECS_PER_DAY +from pendulum.constants import SECS_PER_HOUR +from pendulum.constants import SECS_PER_MIN +from pendulum.constants import SECS_PER_YEAR +from pendulum.constants import TM_DECEMBER +from pendulum.constants import TM_JANUARY + + +if TYPE_CHECKING: + import zoneinfo + + from pendulum.tz.timezone import Timezone + + +class PreciseDiff(NamedTuple): + years: int + months: int + days: int + hours: int + minutes: int + seconds: int + microseconds: int + total_days: int + + def __repr__(self) -> str: return ( - "{years} years " - "{months} months " - "{days} days " - "{hours} hours " - "{minutes} minutes " - "{seconds} seconds " - "{microseconds} microseconds" - ).format( - years=self.years, - months=self.months, - days=self.days, - hours=self.hours, - minutes=self.minutes, - seconds=self.seconds, - microseconds=self.microseconds, + f"{self.years} years " + f"{self.months} months " + f"{self.days} days " + f"{self.hours} hours " + f"{self.minutes} minutes " + f"{self.seconds} seconds " + f"{self.microseconds} microseconds" ) -def is_leap(year): # type: (int) -> bool +def is_leap(year: int) -> bool: return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) -def is_long_year(year): # type: (int) -> bool - def p(y): +def is_long_year(year: int) -> bool: + def p(y: int) -> int: return y + y // 4 - y // 100 + y // 400 return p(year) % 7 == 4 or p(year - 1) % 7 == 3 -def week_day(year, month, day): # type: (int, int, int) -> int +def week_day(year: int, month: int, day: int) -> int: if month < 3: year -= 1 @@ -77,50 +82,22 @@ def week_day(year, month, day): # type: (int, int, int) -> int return w -def days_in_year(year): # type: (int) -> int +def days_in_year(year: int) -> int: if is_leap(year): return DAYS_PER_L_YEAR return DAYS_PER_N_YEAR -def timestamp(dt): # type: (datetime.datetime) -> int - year = dt.year - - result = (year - 1970) * 365 + MONTHS_OFFSETS[0][dt.month] - result += (year - 1968) // 4 - result -= (year - 1900) // 100 - result += (year - 1600) // 400 - - if is_leap(year) and dt.month < 3: - result -= 1 - - result += dt.day - 1 - result *= 24 - result += dt.hour - result *= 60 - result += dt.minute - result *= 60 - result += dt.second - - return result - - def local_time( - unix_time, utc_offset, microseconds -): # type: (int, int, int) -> typing.Tuple[int, int, int, int, int, int, int] + unix_time: int, utc_offset: int, microseconds: int +) -> tuple[int, int, int, int, int, int, int]: """ - Returns a UNIX time as a broken down time + Returns a UNIX time as a broken-down time for a particular transition type. - - :type unix_time: int - :type utc_offset: int - :type microseconds: int - - :rtype: tuple """ year = EPOCH_YEAR - seconds = int(math.floor(unix_time)) + seconds = math.floor(unix_time) # Shift to a base year that is 400-year aligned. if seconds >= 0: @@ -175,41 +152,35 @@ def local_time( month -= 1 # Handle hours, minutes, seconds and microseconds - hour = seconds // SECS_PER_HOUR - seconds %= SECS_PER_HOUR - minute = seconds // SECS_PER_MIN - second = seconds % SECS_PER_MIN + hour, seconds = divmod(seconds, SECS_PER_HOUR) + minute, second = divmod(seconds, SECS_PER_MIN) - return (year, month, day, hour, minute, second, microseconds) + return year, month, day, hour, minute, second, microseconds def precise_diff( - d1, d2 -): # type: (typing.Union[datetime.datetime, datetime.date], typing.Union[datetime.datetime, datetime.date]) -> PreciseDiff + d1: datetime.datetime | datetime.date, d2: datetime.datetime | datetime.date +) -> PreciseDiff: """ Calculate a precise difference between two datetimes. :param d1: The first datetime - :type d1: datetime.datetime or datetime.date - :param d2: The second datetime - :type d2: datetime.datetime or datetime.date - - :rtype: PreciseDiff """ sign = 1 if d1 == d2: return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0) - tzinfo1 = d1.tzinfo if isinstance(d1, datetime.datetime) else None - tzinfo2 = d2.tzinfo if isinstance(d2, datetime.datetime) else None + tzinfo1: datetime.tzinfo | None = ( + d1.tzinfo if isinstance(d1, datetime.datetime) else None + ) + tzinfo2: datetime.tzinfo | None = ( + d2.tzinfo if isinstance(d2, datetime.datetime) else None + ) - if ( - tzinfo1 is None - and tzinfo2 is not None - or tzinfo2 is None - and tzinfo1 is not None + if (tzinfo1 is None and tzinfo2 is not None) or ( + tzinfo2 is None and tzinfo1 is not None ): raise ValueError( "Comparison between naive and aware datetimes is not supported" @@ -234,17 +205,8 @@ def precise_diff( # Trying to figure out the timezone names # If we can't find them, we assume different timezones if tzinfo1 and tzinfo2: - if hasattr(tzinfo1, "name"): - # Pendulum timezone - tz1 = tzinfo1.name - elif hasattr(tzinfo1, "zone"): - # pytz timezone - tz1 = tzinfo1.zone - - if hasattr(tzinfo2, "name"): - tz2 = tzinfo2.name - elif hasattr(tzinfo2, "zone"): - tz2 = tzinfo2.zone + tz1 = _get_tzinfo_name(tzinfo1) + tz2 = _get_tzinfo_name(tzinfo2) in_same_tz = tz1 == tz2 and tz1 is not None @@ -344,7 +306,7 @@ def precise_diff( ) -def _day_number(year, month, day): # type: (int, int, int) -> int +def _day_number(year: int, month: int, day: int) -> int: month = (month + 9) % 12 year = year - month // 10 @@ -356,3 +318,20 @@ def _day_number(year, month, day): # type: (int, int, int) -> int + (month * 306 + 5) // 10 + (day - 1) ) + + +def _get_tzinfo_name(tzinfo: datetime.tzinfo | None) -> str | None: + if tzinfo is None: + return None + + if hasattr(tzinfo, "key"): + # zoneinfo timezone + return cast("zoneinfo.ZoneInfo", tzinfo).key + elif hasattr(tzinfo, "name"): + # Pendulum timezone + return cast("Timezone", tzinfo).name + elif hasattr(tzinfo, "zone"): + # pytz timezone + return tzinfo.zone # type: ignore[no-any-return] + + return None diff --git a/src/pendulum/_pendulum.pyi b/src/pendulum/_pendulum.pyi new file mode 100644 index 00000000..74d7d830 --- /dev/null +++ b/src/pendulum/_pendulum.pyi @@ -0,0 +1,40 @@ +from __future__ import annotations + +from datetime import date +from datetime import datetime +from datetime import time +from typing import NamedTuple + +class Duration: + years: int = 0 + months: int = 0 + weeks: int = 0 + days: int = 0 + remaining_days: int = 0 + hours: int = 0 + minutes: int = 0 + seconds: int = 0 + remaining_seconds: int = 0 + microseconds: int = 0 + +class PreciseDiff(NamedTuple): + years: int + months: int + days: int + hours: int + minutes: int + seconds: int + microseconds: int + total_days: int + +def parse_iso8601( + text: str, +) -> datetime | date | time | Duration: ... +def days_in_year(year: int) -> int: ... +def is_leap(year: int) -> bool: ... +def is_long_year(year: int) -> bool: ... +def local_time( + unix_time: int, utc_offset: int, microseconds: int +) -> tuple[int, int, int, int, int, int, int]: ... +def precise_diff(d1: datetime | date, d2: datetime | date) -> PreciseDiff: ... +def week_day(year: int, month: int, day: int) -> int: ... diff --git a/pendulum/constants.py b/src/pendulum/constants.py similarity index 96% rename from pendulum/constants.py rename to src/pendulum/constants.py index 38697d73..51eb0590 100644 --- a/pendulum/constants.py +++ b/src/pendulum/constants.py @@ -1,11 +1,6 @@ # The day constants -SUNDAY = 0 -MONDAY = 1 -TUESDAY = 2 -WEDNESDAY = 3 -THURSDAY = 4 -FRIDAY = 5 -SATURDAY = 6 +from __future__ import annotations + # Number of X in Y. YEARS_PER_CENTURY = 100 diff --git a/pendulum/date.py b/src/pendulum/date.py similarity index 68% rename from pendulum/date.py rename to src/pendulum/date.py index f11164ee..50554f43 100644 --- a/pendulum/date.py +++ b/src/pendulum/date.py @@ -1,95 +1,92 @@ -from __future__ import absolute_import -from __future__ import division +# The following is only needed because of Python 3.7 +# mypy: no-warn-unused-ignores +from __future__ import annotations import calendar import math from datetime import date +from datetime import datetime from datetime import timedelta +from typing import TYPE_CHECKING +from typing import ClassVar +from typing import NoReturn +from typing import cast +from typing import overload import pendulum -from .constants import FRIDAY -from .constants import MONDAY -from .constants import MONTHS_PER_YEAR -from .constants import SATURDAY -from .constants import SUNDAY -from .constants import THURSDAY -from .constants import TUESDAY -from .constants import WEDNESDAY -from .constants import YEARS_PER_CENTURY -from .constants import YEARS_PER_DECADE -from .exceptions import PendulumException -from .helpers import add_duration -from .mixins.default import FormattableMixin -from .period import Period +from pendulum.constants import MONTHS_PER_YEAR +from pendulum.constants import YEARS_PER_CENTURY +from pendulum.constants import YEARS_PER_DECADE +from pendulum.day import WeekDay +from pendulum.exceptions import PendulumException +from pendulum.helpers import add_duration +from pendulum.interval import Interval +from pendulum.mixins.default import FormattableMixin -class Date(FormattableMixin, date): +if TYPE_CHECKING: + from typing_extensions import Self + from typing_extensions import SupportsIndex - # Names of days of the week - _days = { - SUNDAY: "Sunday", - MONDAY: "Monday", - TUESDAY: "Tuesday", - WEDNESDAY: "Wednesday", - THURSDAY: "Thursday", - FRIDAY: "Friday", - SATURDAY: "Saturday", - } - _MODIFIERS_VALID_UNITS = ["day", "week", "month", "year", "decade", "century"] +class Date(FormattableMixin, date): + _MODIFIERS_VALID_UNITS: ClassVar[list[str]] = [ + "day", + "week", + "month", + "year", + "decade", + "century", + ] # Getters/Setters - def set(self, year=None, month=None, day=None): + def set( + self, year: int | None = None, month: int | None = None, day: int | None = None + ) -> Self: return self.replace(year=year, month=month, day=day) @property - def day_of_week(self): + def day_of_week(self) -> WeekDay: """ Returns the day of the week (0-6). - - :rtype: int """ - return self.isoweekday() % 7 + return WeekDay(self.weekday()) @property - def day_of_year(self): + def day_of_year(self) -> int: """ Returns the day of the year (1-366). - - :rtype: int """ k = 1 if self.is_leap_year() else 2 return (275 * self.month) // 9 - k * ((self.month + 9) // 12) + self.day - 30 @property - def week_of_year(self): + def week_of_year(self) -> int: return self.isocalendar()[1] @property - def days_in_month(self): + def days_in_month(self) -> int: return calendar.monthrange(self.year, self.month)[1] @property - def week_of_month(self): - first_day_of_month = self.replace(day=1) - - return self.week_of_year - first_day_of_month.week_of_year + 1 + def week_of_month(self) -> int: + return math.ceil((self.day + self.first_of("month").isoweekday() - 1) / 7) @property - def age(self): + def age(self) -> int: return self.diff(abs=False).in_years() @property - def quarter(self): - return int(math.ceil(self.month / 3)) + def quarter(self) -> int: + return math.ceil(self.month / 3) # String Formatting - def to_date_string(self): + def to_date_string(self) -> str: """ Format the instance as date. @@ -97,7 +94,7 @@ def to_date_string(self): """ return self.strftime("%Y-%m-%d") - def to_formatted_date_string(self): + def to_formatted_date_string(self) -> str: """ Format the instance as a readable date. @@ -105,28 +102,14 @@ def to_formatted_date_string(self): """ return self.strftime("%b %d, %Y") - def __repr__(self): - return ( - "{klass}(" - "{year}, {month}, {day}" - ")".format( - klass=self.__class__.__name__, - year=self.year, - month=self.month, - day=self.day, - ) - ) + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.year}, {self.month}, {self.day})" # COMPARISONS - def closest(self, dt1, dt2): + def closest(self, dt1: date, dt2: date) -> Self: """ Get the closest date from the instance. - - :type dt1: Date or date - :type dt2: Date or date - - :rtype: Date """ dt1 = self.__class__(dt1.year, dt1.month, dt1.day) dt2 = self.__class__(dt2.year, dt2.month, dt2.day) @@ -136,14 +119,9 @@ def closest(self, dt1, dt2): return dt2 - def farthest(self, dt1, dt2): + def farthest(self, dt1: date, dt2: date) -> Self: """ Get the farthest date from the instance. - - :type dt1: Date or date - :type dt2: Date or date - - :rtype: Date """ dt1 = self.__class__(dt1.year, dt1.month, dt1.day) dt2 = self.__class__(dt2.year, dt2.month, dt2.day) @@ -153,89 +131,69 @@ def farthest(self, dt1, dt2): return dt2 - def is_future(self): + def is_future(self) -> bool: """ Determines if the instance is in the future, ie. greater than now. - - :rtype: bool """ return self > self.today() - def is_past(self): + def is_past(self) -> bool: """ Determines if the instance is in the past, ie. less than now. - - :rtype: bool """ return self < self.today() - def is_leap_year(self): + def is_leap_year(self) -> bool: """ Determines if the instance is a leap year. - - :rtype: bool """ return calendar.isleap(self.year) - def is_long_year(self): + def is_long_year(self) -> bool: """ Determines if the instance is a long year See link ``_ - - :rtype: bool """ return Date(self.year, 12, 28).isocalendar()[1] == 53 - def is_same_day(self, dt): + def is_same_day(self, dt: date) -> bool: """ Checks if the passed in date is the same day as the instance current day. - - :type dt: Date or date - - :rtype: bool """ return self == dt - def is_anniversary(self, dt=None): + def is_anniversary(self, dt: date | None = None) -> bool: """ - Check if its the anniversary. + Check if it's the anniversary. Compares the date/month values of the two dates. - - :rtype: bool """ if dt is None: - dt = Date.today() + dt = self.__class__.today() instance = self.__class__(dt.year, dt.month, dt.day) return (self.month, self.day) == (instance.month, instance.day) # the additional method for checking if today is the anniversary day - # the alias is provided to start using a new name and keep the backward compatibility - # the old name can be completely replaced with the new in one of the future versions + # the alias is provided to start using a new name and keep the backward + # compatibility the old name can be completely replaced with the new in + # one of the future versions is_birthday = is_anniversary - # ADDITIONS AND SUBSTRACTIONS + # ADDITIONS AND SUBTRACTIONS - def add(self, years=0, months=0, weeks=0, days=0): + def add( + self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0 + ) -> Self: """ Add duration to the instance. :param years: The number of years - :type years: int - :param months: The number of months - :type months: int - :param weeks: The number of weeks - :type weeks: int - :param days: The number of days - :type days: int - - :rtype: Date """ dt = add_duration( date(self.year, self.month, self.day), @@ -247,34 +205,24 @@ def add(self, years=0, months=0, weeks=0, days=0): return self.__class__(dt.year, dt.month, dt.day) - def subtract(self, years=0, months=0, weeks=0, days=0): + def subtract( + self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0 + ) -> Self: """ Remove duration from the instance. :param years: The number of years - :type years: int - :param months: The number of months - :type months: int - :param weeks: The number of weeks - :type weeks: int - :param days: The number of days - :type days: int - - :rtype: Date """ return self.add(years=-years, months=-months, weeks=-weeks, days=-days) - def _add_timedelta(self, delta): + def _add_timedelta(self, delta: timedelta) -> Self: """ Add timedelta duration to the instance. :param delta: The timedelta instance - :type delta: pendulum.Duration or datetime.timedelta - - :rtype: Date """ if isinstance(delta, pendulum.Duration): return self.add( @@ -286,14 +234,11 @@ def _add_timedelta(self, delta): return self.add(days=delta.days) - def _subtract_timedelta(self, delta): + def _subtract_timedelta(self, delta: timedelta) -> Self: """ Remove timedelta duration from the instance. :param delta: The timedelta instance - :type delta: pendulum.Duration or datetime.timedelta - - :rtype: Date """ if isinstance(delta, pendulum.Duration): return self.subtract( @@ -305,13 +250,22 @@ def _subtract_timedelta(self, delta): return self.subtract(days=delta.days) - def __add__(self, other): + def __add__(self, other: timedelta) -> Self: if not isinstance(other, timedelta): return NotImplemented return self._add_timedelta(other) - def __sub__(self, other): + @overload # type: ignore[override] # this is only needed because of Python 3.7 + def __sub__(self, __delta: timedelta) -> Self: ... + + @overload + def __sub__(self, __dt: datetime) -> NoReturn: ... + + @overload + def __sub__(self, __dt: Self) -> Interval[Date]: ... + + def __sub__(self, other: timedelta | date) -> Self | Interval[Date]: if isinstance(other, timedelta): return self._subtract_timedelta(other) @@ -324,23 +278,24 @@ def __sub__(self, other): # DIFFERENCES - def diff(self, dt=None, abs=True): + def diff(self, dt: date | None = None, abs: bool = True) -> Interval[Date]: """ - Returns the difference between two Date objects as a Period. - - :type dt: Date or None + Returns the difference between two Date objects as an Interval. + :param dt: The date to compare to (defaults to today) :param abs: Whether to return an absolute interval or not - :type abs: bool - - :rtype: Period """ if dt is None: dt = self.today() - return Period(self, Date(dt.year, dt.month, dt.day), absolute=abs) + return Interval(self, Date(dt.year, dt.month, dt.day), absolute=abs) - def diff_for_humans(self, other=None, absolute=False, locale=None): + def diff_for_humans( + self, + other: date | None = None, + absolute: bool = False, + locale: str | None = None, + ) -> str: """ Get the difference in a human readable format in the current locale. @@ -360,15 +315,9 @@ def diff_for_humans(self, other=None, absolute=False, locale=None): 1 day after 5 months after - :type other: Date - + :param other: The date to compare to (defaults to today) :param absolute: removes time difference modifiers ago, after, etc - :type absolute: bool - :param locale: The locale to use for localization - :type locale: str - - :rtype: str """ is_now = other is None @@ -381,7 +330,7 @@ def diff_for_humans(self, other=None, absolute=False, locale=None): # MODIFIERS - def start_of(self, unit): + def start_of(self, unit: str) -> Self: """ Returns a copy of the instance with the time reset with the following rules: @@ -394,16 +343,13 @@ def start_of(self, unit): * century: date to first day of century and time to 00:00:00 :param unit: The unit to reset to - :type unit: str - - :rtype: Date """ if unit not in self._MODIFIERS_VALID_UNITS: - raise ValueError('Invalid unit "{}" for start_of()'.format(unit)) + raise ValueError(f'Invalid unit "{unit}" for start_of()') - return getattr(self, "_start_of_{}".format(unit))() + return cast("Self", getattr(self, f"_start_of_{unit}")()) - def end_of(self, unit): + def end_of(self, unit: str) -> Self: """ Returns a copy of the instance with the time reset with the following rules: @@ -415,108 +361,83 @@ def end_of(self, unit): * century: date to last day of century :param unit: The unit to reset to - :type unit: str - - :rtype: Date """ if unit not in self._MODIFIERS_VALID_UNITS: - raise ValueError('Invalid unit "%s" for end_of()' % unit) + raise ValueError(f'Invalid unit "{unit}" for end_of()') - return getattr(self, "_end_of_%s" % unit)() + return cast("Self", getattr(self, f"_end_of_{unit}")()) - def _start_of_day(self): + def _start_of_day(self) -> Self: """ Compatibility method. - - :rtype: Date """ return self - def _end_of_day(self): + def _end_of_day(self) -> Self: """ Compatibility method - - :rtype: Date """ return self - def _start_of_month(self): + def _start_of_month(self) -> Self: """ Reset the date to the first day of the month. - - :rtype: Date """ return self.set(self.year, self.month, 1) - def _end_of_month(self): + def _end_of_month(self) -> Self: """ Reset the date to the last day of the month. - - :rtype: Date """ return self.set(self.year, self.month, self.days_in_month) - def _start_of_year(self): + def _start_of_year(self) -> Self: """ Reset the date to the first day of the year. - - :rtype: Date """ return self.set(self.year, 1, 1) - def _end_of_year(self): + def _end_of_year(self) -> Self: """ Reset the date to the last day of the year. - - :rtype: Date """ return self.set(self.year, 12, 31) - def _start_of_decade(self): + def _start_of_decade(self) -> Self: """ Reset the date to the first day of the decade. - - :rtype: Date """ year = self.year - self.year % YEARS_PER_DECADE return self.set(year, 1, 1) - def _end_of_decade(self): + def _end_of_decade(self) -> Self: """ Reset the date to the last day of the decade. - - :rtype: Date """ year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1 return self.set(year, 12, 31) - def _start_of_century(self): + def _start_of_century(self) -> Self: """ Reset the date to the first day of the century. - - :rtype: Date """ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1 return self.set(year, 1, 1) - def _end_of_century(self): + def _end_of_century(self) -> Self: """ Reset the date to the last day of the century. - - :rtype: Date """ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY return self.set(year, 12, 31) - def _start_of_week(self): + def _start_of_week(self) -> Self: """ Reset the date to the first day of the week. - - :rtype: Date """ dt = self @@ -525,11 +446,9 @@ def _start_of_week(self): return dt.start_of("day") - def _end_of_week(self): + def _end_of_week(self) -> Self: """ Reset the date to the last day of the week. - - :rtype: Date """ dt = self @@ -538,7 +457,7 @@ def _end_of_week(self): return dt.end_of("day") - def next(self, day_of_week=None): + def next(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the next occurrence of a given day of the week. If no day_of_week is provided, modify to the next occurrence @@ -546,14 +465,11 @@ def next(self, day_of_week=None): to indicate the desired day_of_week, ex. pendulum.MONDAY. :param day_of_week: The next day of week to reset to. - :type day_of_week: int or None - - :rtype: Date """ if day_of_week is None: day_of_week = self.day_of_week - if day_of_week < SUNDAY or day_of_week > SATURDAY: + if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY: raise ValueError("Invalid day of week") dt = self.add(days=1) @@ -562,7 +478,7 @@ def next(self, day_of_week=None): return dt - def previous(self, day_of_week=None): + def previous(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the previous occurrence of a given day of the week. If no day_of_week is provided, modify to the previous occurrence @@ -570,14 +486,11 @@ def previous(self, day_of_week=None): to indicate the desired day_of_week, ex. pendulum.MONDAY. :param day_of_week: The previous day of week to reset to. - :type day_of_week: int or None - - :rtype: Date """ if day_of_week is None: day_of_week = self.day_of_week - if day_of_week < SUNDAY or day_of_week > SATURDAY: + if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY: raise ValueError("Invalid day of week") dt = self.subtract(days=1) @@ -586,49 +499,43 @@ def previous(self, day_of_week=None): return dt - def first_of(self, unit, day_of_week=None): + def first_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self: """ Returns an instance set to the first occurrence of a given day of the week in the current unit. If no day_of_week is provided, modify to the first day of the unit. - Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. + Use the supplied consts to indicate the desired day_of_week, + ex. pendulum.MONDAY. Supported units are month, quarter and year. :param unit: The unit to use - :type unit: str - - :type day_of_week: int or None - - :rtype: Date + :param day_of_week: The day of week to reset to. """ if unit not in ["month", "quarter", "year"]: - raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + raise ValueError(f'Invalid unit "{unit}" for first_of()') - return getattr(self, "_first_of_{}".format(unit))(day_of_week) + return cast("Self", getattr(self, f"_first_of_{unit}")(day_of_week)) - def last_of(self, unit, day_of_week=None): + def last_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self: """ Returns an instance set to the last occurrence of a given day of the week in the current unit. If no day_of_week is provided, modify to the last day of the unit. - Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. + Use the supplied consts to indicate the desired day_of_week, + ex. pendulum.MONDAY. Supported units are month, quarter and year. :param unit: The unit to use - :type unit: str - - :type day_of_week: int or None - - :rtype: Date + :param day_of_week: The day of week to reset to. """ if unit not in ["month", "quarter", "year"]: - raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + raise ValueError(f'Invalid unit "{unit}" for first_of()') - return getattr(self, "_last_of_{}".format(unit))(day_of_week) + return cast("Self", getattr(self, f"_last_of_{unit}")(day_of_week)) - def nth_of(self, unit, nth, day_of_week): + def nth_of(self, unit: str, nth: int, day_of_week: WeekDay) -> Self: """ Returns a new instance set to the given occurrence of a given day of the week in the current unit. @@ -639,37 +546,29 @@ def nth_of(self, unit, nth, day_of_week): Supported units are month, quarter and year. :param unit: The unit to use - :type unit: str - - :type nth: int - - :type day_of_week: int or None - - :rtype: Date + :param nth: The occurrence to use + :param day_of_week: The day of week to set to. """ if unit not in ["month", "quarter", "year"]: - raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + raise ValueError(f'Invalid unit "{unit}" for first_of()') - dt = getattr(self, "_nth_of_{}".format(unit))(nth, day_of_week) - if dt is False: + dt = cast("Self", getattr(self, f"_nth_of_{unit}")(nth, day_of_week)) + if not dt: raise PendulumException( - "Unable to find occurence {} of {} in {}".format( - nth, self._days[day_of_week], unit - ) + f"Unable to find occurrence {nth}" + f" of {WeekDay(day_of_week).name.capitalize()} in {unit}" ) return dt - def _first_of_month(self, day_of_week): + def _first_of_month(self, day_of_week: WeekDay) -> Self: """ Modify to the first occurrence of a given day of the week in the current month. If no day_of_week is provided, modify to the first day of the month. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - :type day_of_week: int - - :rtype: Date + :param day_of_week: The day of week to set to. """ dt = self @@ -678,7 +577,7 @@ def _first_of_month(self, day_of_week): month = calendar.monthcalendar(dt.year, dt.month) - calendar_day = (day_of_week - 1) % 7 + calendar_day = day_of_week if month[0][calendar_day] > 0: day_of_month = month[0][calendar_day] @@ -687,16 +586,14 @@ def _first_of_month(self, day_of_week): return dt.set(day=day_of_month) - def _last_of_month(self, day_of_week=None): + def _last_of_month(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current month. If no day_of_week is provided, modify to the last day of the month. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - :type day_of_week: int or None - - :rtype: Date + :param day_of_week: The day of week to set to. """ dt = self @@ -705,7 +602,7 @@ def _last_of_month(self, day_of_week=None): month = calendar.monthcalendar(dt.year, dt.month) - calendar_day = (day_of_week - 1) % 7 + calendar_day = day_of_week if month[-1][calendar_day] > 0: day_of_month = month[-1][calendar_day] @@ -714,74 +611,54 @@ def _last_of_month(self, day_of_week=None): return dt.set(day=day_of_month) - def _nth_of_month(self, nth, day_of_week): + def _nth_of_month(self, nth: int, day_of_week: WeekDay) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current month. If the calculated occurrence is outside, the scope of the current month, then return False and no modifications are made. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type nth: int - - :type day_of_week: int or None - - :rtype: Date """ if nth == 1: return self.first_of("month", day_of_week) dt = self.first_of("month") check = dt.format("YYYY-MM") - for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): dt = dt.next(day_of_week) if dt.format("YYYY-MM") == check: return self.set(day=dt.day) - return False + return None - def _first_of_quarter(self, day_of_week=None): + def _first_of_quarter(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the first occurrence of a given day of the week in the current quarter. If no day_of_week is provided, modify to the first day of the quarter. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type day_of_week: int or None - - :rtype: Date """ return self.set(self.year, self.quarter * 3 - 2, 1).first_of( "month", day_of_week ) - def _last_of_quarter(self, day_of_week=None): + def _last_of_quarter(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current quarter. If no day_of_week is provided, modify to the last day of the quarter. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type day_of_week: int or None - - :rtype: Date """ return self.set(self.year, self.quarter * 3, 1).last_of("month", day_of_week) - def _nth_of_quarter(self, nth, day_of_week): + def _nth_of_quarter(self, nth: int, day_of_week: WeekDay) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current quarter. If the calculated occurrence is outside, the scope of the current quarter, then return False and no modifications are made. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type nth: int - - :type day_of_week: int or None - - :rtype: Date """ if nth == 1: return self.first_of("quarter", day_of_week) @@ -790,75 +667,57 @@ def _nth_of_quarter(self, nth, day_of_week): last_month = dt.month year = dt.year dt = dt.first_of("quarter") - for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): dt = dt.next(day_of_week) if last_month < dt.month or year != dt.year: - return False + return None return self.set(self.year, dt.month, dt.day) - def _first_of_year(self, day_of_week=None): + def _first_of_year(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the first occurrence of a given day of the week in the current year. If no day_of_week is provided, modify to the first day of the year. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type day_of_week: int or None - - :rtype: Date """ return self.set(month=1).first_of("month", day_of_week) - def _last_of_year(self, day_of_week=None): + def _last_of_year(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current year. If no day_of_week is provided, modify to the last day of the year. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type day_of_week: int or None - - :rtype: Date """ return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week) - def _nth_of_year(self, nth, day_of_week): + def _nth_of_year(self, nth: int, day_of_week: WeekDay) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current year. If the calculated occurrence is outside, the scope of the current year, then return False and no modifications are made. Use the supplied consts to indicate the desired day_of_week, ex. pendulum.MONDAY. - - :type nth: int - - :type day_of_week: int or None - - :rtype: Date """ if nth == 1: return self.first_of("year", day_of_week) dt = self.first_of("year") year = dt.year - for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): dt = dt.next(day_of_week) if year != dt.year: - return False + return None return self.set(self.year, dt.month, dt.day) - def average(self, dt=None): + def average(self, dt: date | None = None) -> Self: """ Modify the current instance to the average of a given instance (default now) and the current instance. - - :type dt: Date or date - - :rtype: Date """ if dt is None: dt = Date.today() @@ -868,22 +727,29 @@ def average(self, dt=None): # Native methods override @classmethod - def today(cls): - return pendulum.today().date() + def today(cls) -> Self: + dt = date.today() + + return cls(dt.year, dt.month, dt.day) @classmethod - def fromtimestamp(cls, t): - dt = super(Date, cls).fromtimestamp(t) + def fromtimestamp(cls, t: float) -> Self: + dt = super().fromtimestamp(t) return cls(dt.year, dt.month, dt.day) @classmethod - def fromordinal(cls, n): - dt = super(Date, cls).fromordinal(n) + def fromordinal(cls, n: int) -> Self: + dt = super().fromordinal(n) return cls(dt.year, dt.month, dt.day) - def replace(self, year=None, month=None, day=None): + def replace( + self, + year: SupportsIndex | None = None, + month: SupportsIndex | None = None, + day: SupportsIndex | None = None, + ) -> Self: year = year if year is not None else self.year month = month if month is not None else self.month day = day if day is not None else self.day diff --git a/pendulum/datetime.py b/src/pendulum/datetime.py similarity index 60% rename from pendulum/datetime.py rename to src/pendulum/datetime.py index feb140fe..da89b13d 100644 --- a/pendulum/datetime.py +++ b/src/pendulum/datetime.py @@ -1,70 +1,74 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division +from __future__ import annotations import calendar import datetime +import traceback -from typing import Optional -from typing import TypeVar -from typing import Union +from typing import TYPE_CHECKING +from typing import Any +from typing import ClassVar +from typing import cast +from typing import overload import pendulum -from .constants import ATOM -from .constants import COOKIE -from .constants import MINUTES_PER_HOUR -from .constants import MONTHS_PER_YEAR -from .constants import RFC822 -from .constants import RFC850 -from .constants import RFC1036 -from .constants import RFC1123 -from .constants import RFC2822 -from .constants import RSS -from .constants import SATURDAY -from .constants import SECONDS_PER_DAY -from .constants import SECONDS_PER_MINUTE -from .constants import SUNDAY -from .constants import W3C -from .constants import YEARS_PER_CENTURY -from .constants import YEARS_PER_DECADE -from .date import Date -from .exceptions import PendulumException -from .helpers import add_duration -from .helpers import timestamp -from .period import Period -from .time import Time -from .tz import UTC -from .tz.timezone import Timezone -from .utils._compat import _HAS_FOLD - - -_D = TypeVar("_D", bound="DateTime") +from pendulum.constants import ATOM +from pendulum.constants import COOKIE +from pendulum.constants import MINUTES_PER_HOUR +from pendulum.constants import MONTHS_PER_YEAR +from pendulum.constants import RFC822 +from pendulum.constants import RFC850 +from pendulum.constants import RFC1036 +from pendulum.constants import RFC1123 +from pendulum.constants import RFC2822 +from pendulum.constants import RSS +from pendulum.constants import SECONDS_PER_DAY +from pendulum.constants import SECONDS_PER_MINUTE +from pendulum.constants import W3C +from pendulum.constants import YEARS_PER_CENTURY +from pendulum.constants import YEARS_PER_DECADE +from pendulum.date import Date +from pendulum.day import WeekDay +from pendulum.exceptions import PendulumException +from pendulum.helpers import add_duration +from pendulum.interval import Interval +from pendulum.time import Time +from pendulum.tz import UTC +from pendulum.tz import local_timezone +from pendulum.tz.timezone import FixedTimezone +from pendulum.tz.timezone import Timezone + + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Literal + + from typing_extensions import Self + from typing_extensions import SupportsIndex class DateTime(datetime.datetime, Date): - - EPOCH = None # type: DateTime + EPOCH: ClassVar[DateTime] + min: ClassVar[DateTime] + max: ClassVar[DateTime] # Formats - _FORMATS = { + _FORMATS: ClassVar[dict[str, str | Callable[[datetime.datetime], str]]] = { "atom": ATOM, "cookie": COOKIE, - "iso8601": lambda dt: dt.isoformat(), + "iso8601": lambda dt: dt.isoformat("T"), "rfc822": RFC822, "rfc850": RFC850, "rfc1036": RFC1036, "rfc1123": RFC1123, "rfc2822": RFC2822, - "rfc3339": lambda dt: dt.isoformat(), + "rfc3339": lambda dt: dt.isoformat("T"), "rss": RSS, "w3c": W3C, } - _EPOCH = datetime.datetime(1970, 1, 1, tzinfo=UTC) - - _MODIFIERS_VALID_UNITS = [ + _MODIFIERS_VALID_UNITS: ClassVar[list[str]] = [ "second", "minute", "hour", @@ -76,63 +80,134 @@ class DateTime(datetime.datetime, Date): "century", ] - if not _HAS_FOLD: + _EPOCH: datetime.datetime = datetime.datetime(1970, 1, 1, tzinfo=UTC) - def __new__( - cls, - year, - month, - day, - hour=0, - minute=0, - second=0, - microsecond=0, - tzinfo=None, - fold=0, - ): - self = datetime.datetime.__new__( - cls, year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo - ) + @classmethod + def create( + cls, + year: SupportsIndex, + month: SupportsIndex, + day: SupportsIndex, + hour: SupportsIndex = 0, + minute: SupportsIndex = 0, + second: SupportsIndex = 0, + microsecond: SupportsIndex = 0, + tz: str | float | Timezone | FixedTimezone | None | datetime.tzinfo = UTC, + fold: int = 1, + raise_on_unknown_times: bool = False, + ) -> Self: + """ + Creates a new DateTime instance from a specific date and time. + """ + if tz is not None: + tz = pendulum._safe_timezone(tz) + + dt = datetime.datetime( + year, month, day, hour, minute, second, microsecond, fold=fold + ) + + if tz is not None: + dt = tz.convert(dt, raise_on_unknown_times=raise_on_unknown_times) + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=dt.tzinfo, + fold=dt.fold, + ) + + @classmethod + def instance( + cls, + dt: datetime.datetime, + tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC, + ) -> Self: + tz = dt.tzinfo or tz + + if tz is not None: + tz = pendulum._safe_timezone(tz, dt=dt) + + return cls.create( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tz=tz, + fold=dt.fold, + ) - self._fold = fold + @overload + @classmethod + def now(cls, tz: datetime.tzinfo | None = None) -> Self: ... - return self + @overload + @classmethod + def now(cls, tz: str | Timezone | FixedTimezone | None = None) -> Self: ... @classmethod - def now(cls, tz=None): # type: (Optional[Union[str, Timezone]]) -> DateTime + def now( + cls, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = None + ) -> Self: """ Get a DateTime instance for the current date and time. """ - return pendulum.now(tz) + if tz is None or tz == "local": + dt = datetime.datetime.now(local_timezone()) + elif tz is UTC or tz == "UTC": + dt = datetime.datetime.now(UTC) + else: + dt = datetime.datetime.now(UTC) + tz = pendulum._safe_timezone(tz) + dt = dt.astimezone(tz) + + return cls( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=dt.tzinfo, + fold=dt.fold, + ) @classmethod - def utcnow(cls): # type: () -> DateTime + def utcnow(cls) -> Self: """ Get a DateTime instance for the current date and time in UTC. """ - return pendulum.now(UTC) + return cls.now(UTC) @classmethod - def today(cls): # type: () -> DateTime - return pendulum.now() + def today(cls) -> Self: + return cls.now() @classmethod - def strptime(cls, time, fmt): # type: (str, str) -> DateTime - return pendulum.instance(datetime.datetime.strptime(time, fmt)) + def strptime(cls, time: str, fmt: str) -> Self: + return cls.instance(datetime.datetime.strptime(time, fmt)) # Getters/Setters def set( self, - year=None, - month=None, - day=None, - hour=None, - minute=None, - second=None, - microsecond=None, - tz=None, - ): + 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, + tz: str | float | Timezone | FixedTimezone | datetime.tzinfo | None = None, + ) -> Self: if year is None: year = self.year if month is None: @@ -150,52 +225,18 @@ def set( if tz is None: tz = self.tz - return pendulum.datetime( - year, month, day, hour, minute, second, microsecond, tz=tz + return self.__class__.create( + year, month, day, hour, minute, second, microsecond, tz=tz, fold=self.fold ) - if not _HAS_FOLD: - - @property - def fold(self): - return self._fold - - def timestamp(self): - if self.tzinfo is None: - s = timestamp(self) - - return s + self.microsecond / 1e6 - else: - kwargs = {"tzinfo": self.tzinfo} - - if _HAS_FOLD: - kwargs["fold"] = self.fold - - dt = datetime.datetime( - self.year, - self.month, - self.day, - self.hour, - self.minute, - self.second, - self.microsecond, - **kwargs - ) - return (dt - self._EPOCH).total_seconds() - @property - def float_timestamp(self): + def float_timestamp(self) -> float: return self.timestamp() @property - def int_timestamp(self): + def int_timestamp(self) -> int: # Workaround needed to avoid inaccuracy # for far into the future datetimes - kwargs = {"tzinfo": self.tzinfo} - - if _HAS_FOLD: - kwargs["fold"] = self.fold - dt = datetime.datetime( self.year, self.month, @@ -204,7 +245,8 @@ def int_timestamp(self): self.minute, self.second, self.microsecond, - **kwargs + tzinfo=self.tzinfo, + fold=self.fold, ) delta = dt - self._EPOCH @@ -212,26 +254,31 @@ def int_timestamp(self): return delta.days * SECONDS_PER_DAY + delta.seconds @property - def offset(self): + def offset(self) -> int | None: return self.get_offset() @property - def offset_hours(self): - return self.get_offset() / SECONDS_PER_MINUTE / MINUTES_PER_HOUR + def offset_hours(self) -> float | None: + offset = self.get_offset() + + if offset is None: + return None + + return offset / SECONDS_PER_MINUTE / MINUTES_PER_HOUR @property - def timezone(self): # type: () -> Optional[Timezone] - if not isinstance(self.tzinfo, Timezone): - return + def timezone(self) -> Timezone | FixedTimezone | None: + if not isinstance(self.tzinfo, (Timezone, FixedTimezone)): + return None return self.tzinfo @property - def tz(self): # type: () -> Optional[Timezone] + def tz(self) -> Timezone | FixedTimezone | None: return self.timezone @property - def timezone_name(self): # type: () -> Optional[str] + def timezone_name(self) -> str | None: tz = self.timezone if tz is None: @@ -240,28 +287,32 @@ def timezone_name(self): # type: () -> Optional[str] return tz.name @property - def age(self): + def age(self) -> int: return self.date().diff(self.now(self.tz).date(), abs=False).in_years() - def is_local(self): + def is_local(self) -> bool: return self.offset == self.in_timezone(pendulum.local_timezone()).offset - def is_utc(self): - return self.offset == UTC.offset + def is_utc(self) -> bool: + return self.offset == 0 - def is_dst(self): + def is_dst(self) -> bool: return self.dst() != datetime.timedelta() - def get_offset(self): - return int(self.utcoffset().total_seconds()) + def get_offset(self) -> int | None: + utcoffset = self.utcoffset() + if utcoffset is None: + return None + + return int(utcoffset.total_seconds()) - def date(self): + def date(self) -> Date: return Date(self.year, self.month, self.day) - def time(self): + def time(self) -> Time: return Time(self.hour, self.minute, self.second, self.microsecond) - def naive(self): # type: (_D) -> _D + def naive(self) -> Self: """ Return the DateTime without timezone information. """ @@ -275,54 +326,35 @@ def naive(self): # type: (_D) -> _D self.microsecond, ) - def on(self, year, month, day): + def on(self, year: int, month: int, day: int) -> Self: """ Returns a new instance with the current date set to a different date. - - :param year: The year - :type year: int - - :param month: The month - :type month: int - - :param day: The day - :type day: int - - :rtype: DateTime """ return self.set(year=int(year), month=int(month), day=int(day)) - def at(self, hour, minute=0, second=0, microsecond=0): + def at( + self, hour: int, minute: int = 0, second: int = 0, microsecond: int = 0 + ) -> Self: """ Returns a new instance with the current time to a different time. - - :param hour: The hour - :type hour: int - - :param minute: The minute - :type minute: int - - :param second: The second - :type second: int - - :param microsecond: The microsecond - :type microsecond: int - - :rtype: DateTime """ return self.set( hour=hour, minute=minute, second=second, microsecond=microsecond ) - def in_timezone(self, tz): # type: (Union[str, Timezone]) -> DateTime + def in_timezone(self, tz: str | Timezone | FixedTimezone) -> Self: """ Set the instance's timezone from a string or object. """ tz = pendulum._safe_timezone(tz) - return tz.convert(self, dst_rule=pendulum.POST_TRANSITION) + dt = self + if not self.timezone: + dt = dt.replace(fold=1) + + return tz.convert(dt) - def in_tz(self, tz): # type: (Union[str, Timezone]) -> DateTime + def in_tz(self, tz: str | Timezone | FixedTimezone) -> Self: """ Set the instance's timezone from a string or object. """ @@ -330,51 +362,39 @@ def in_tz(self, tz): # type: (Union[str, Timezone]) -> DateTime # STRING FORMATTING - def to_time_string(self): + def to_time_string(self) -> str: """ Format the instance as time. - - :rtype: str """ return self.format("HH:mm:ss") - def to_datetime_string(self): + def to_datetime_string(self) -> str: """ Format the instance as date and time. - - :rtype: str """ return self.format("YYYY-MM-DD HH:mm:ss") - def to_day_datetime_string(self): + def to_day_datetime_string(self) -> str: """ Format the instance as day, date and time (in english). - - :rtype: str """ return self.format("ddd, MMM D, YYYY h:mm A", locale="en") - def to_atom_string(self): + def to_atom_string(self) -> str: """ Format the instance as ATOM. - - :rtype: str """ return self._to_string("atom") - def to_cookie_string(self): + def to_cookie_string(self) -> str: """ Format the instance as COOKIE. - - :rtype: str """ return self._to_string("cookie", locale="en") - def to_iso8601_string(self): + def to_iso8601_string(self) -> str: """ Format the instance as ISO 8601. - - :rtype: str """ string = self._to_string("iso8601") @@ -383,100 +403,76 @@ def to_iso8601_string(self): return string - def to_rfc822_string(self): + def to_rfc822_string(self) -> str: """ Format the instance as RFC 822. - - :rtype: str """ return self._to_string("rfc822") - def to_rfc850_string(self): + def to_rfc850_string(self) -> str: """ Format the instance as RFC 850. - - :rtype: str """ return self._to_string("rfc850") - def to_rfc1036_string(self): + def to_rfc1036_string(self) -> str: """ Format the instance as RFC 1036. - - :rtype: str """ return self._to_string("rfc1036") - def to_rfc1123_string(self): + def to_rfc1123_string(self) -> str: """ Format the instance as RFC 1123. - - :rtype: str """ return self._to_string("rfc1123") - def to_rfc2822_string(self): + def to_rfc2822_string(self) -> str: """ Format the instance as RFC 2822. - - :rtype: str """ return self._to_string("rfc2822") - def to_rfc3339_string(self): + def to_rfc3339_string(self) -> str: """ Format the instance as RFC 3339. - - :rtype: str """ return self._to_string("rfc3339") - def to_rss_string(self): + def to_rss_string(self) -> str: """ Format the instance as RSS. - - :rtype: str """ return self._to_string("rss") - def to_w3c_string(self): + def to_w3c_string(self) -> str: """ Format the instance as W3C. - - :rtype: str """ return self._to_string("w3c") - def _to_string(self, fmt, locale=None): + def _to_string(self, fmt: str, locale: str | None = None) -> str: """ Format the instance to a common string format. - - :param fmt: The name of the string format - :type fmt: string - - :param locale: The locale to use - :type locale: str or None - - :rtype: str """ if fmt not in self._FORMATS: - raise ValueError("Format [{}] is not supported".format(fmt)) + raise ValueError(f"Format [{fmt}] is not supported") - fmt = self._FORMATS[fmt] - if callable(fmt): - return fmt(self) + fmt_value = self._FORMATS[fmt] + if callable(fmt_value): + return fmt_value(self) - return self.format(fmt, locale=locale) + return self.format(fmt_value, locale=locale) - def __str__(self): - return self.isoformat("T") + def __str__(self) -> str: + return self.isoformat(" ") - def __repr__(self): + def __repr__(self) -> str: us = "" if self.microsecond: - us = ", {}".format(self.microsecond) + us = f", {self.microsecond}" - repr_ = "{klass}(" "{year}, {month}, {day}, " "{hour}, {minute}, {second}{us}" + repr_ = "{klass}({year}, {month}, {day}, {hour}, {minute}, {second}{us}" if self.tzinfo is not None: repr_ += ", tzinfo={tzinfo}" @@ -492,125 +488,90 @@ def __repr__(self): minute=self.minute, second=self.second, us=us, - tzinfo=self.tzinfo, + tzinfo=repr(self.tzinfo), ) # Comparisons - def closest(self, dt1, dt2, *dts): + def closest(self, *dts: datetime.datetime) -> Self: # type: ignore[override] """ - Get the farthest date from the instance. - - :type dt1: datetime.datetime - :type dt2: datetime.datetime - :type dts: list[datetime.datetime,] - - :rtype: DateTime + Get the closest date to the instance. """ - dt1 = pendulum.instance(dt1) - dt2 = pendulum.instance(dt2) - dts = [dt1, dt2] + [pendulum.instance(x) for x in dts] - dts = [(abs(self - dt), dt) for dt in dts] + pdts = [self.instance(x) for x in dts] - return min(dts)[1] + return min((abs(self - dt), dt) for dt in pdts)[1] - def farthest(self, dt1, dt2, *dts): + def farthest(self, *dts: datetime.datetime) -> Self: # type: ignore[override] """ Get the farthest date from the instance. - - :type dt1: datetime.datetime - :type dt2: datetime.datetime - :type dts: list[datetime.datetime,] - - :rtype: DateTime """ - dt1 = pendulum.instance(dt1) - dt2 = pendulum.instance(dt2) - - dts = [dt1, dt2] + [pendulum.instance(x) for x in dts] - dts = [(abs(self - dt), dt) for dt in dts] + pdts = [self.instance(x) for x in dts] - return max(dts)[1] + return max((abs(self - dt), dt) for dt in pdts)[1] - def is_future(self): + def is_future(self) -> bool: """ Determines if the instance is in the future, ie. greater than now. - - :rtype: bool """ return self > self.now(self.timezone) - def is_past(self): + def is_past(self) -> bool: """ Determines if the instance is in the past, ie. less than now. - - :rtype: bool """ return self < self.now(self.timezone) - def is_long_year(self): + def is_long_year(self) -> bool: """ Determines if the instance is a long year See link `https://en.wikipedia.org/wiki/ISO_8601#Week_dates`_ - - :rtype: bool """ return ( - pendulum.datetime(self.year, 12, 28, 0, 0, 0, tz=self.tz).isocalendar()[1] + DateTime.create(self.year, 12, 28, 0, 0, 0, tz=self.tz).isocalendar()[1] == 53 ) - def is_same_day(self, dt): + def is_same_day(self, dt: datetime.datetime) -> bool: # type: ignore[override] """ Checks if the passed in date is the same day as the instance current day. - - :type dt: DateTime or datetime or str or int - - :rtype: bool """ - dt = pendulum.instance(dt) + dt = self.instance(dt) return self.to_date_string() == dt.to_date_string() - def is_anniversary(self, dt=None): + def is_anniversary( # type: ignore[override] + self, dt: datetime.datetime | None = None + ) -> bool: """ Check if its the anniversary. Compares the date/month values of the two dates. - - :rtype: bool """ if dt is None: dt = self.now(self.tz) - instance = pendulum.instance(dt) + instance = self.instance(dt) return (self.month, self.day) == (instance.month, instance.day) - # the additional method for checking if today is the anniversary day - # the alias is provided to start using a new name and keep the backward compatibility - # the old name can be completely replaced with the new in one of the future versions - is_birthday = is_anniversary - # ADDITIONS AND SUBSTRACTIONS def add( self, - years=0, - months=0, - weeks=0, - days=0, - hours=0, - minutes=0, - seconds=0, - microseconds=0, - ): # type: (_D, int, int, int, int, int, int, int, int) -> _D + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: float = 0, + microseconds: int = 0, + ) -> Self: """ Add a duration to the instance. If we're adding units of variable length (i.e., years, months), - move forward from curren time, - otherwise move forward from utc, for accuracy + move forward from current time, otherwise move forward from utc, for accuracy when moving across DST boundaries. """ units_of_variable_length = any([years, months, weeks, days]) @@ -641,8 +602,8 @@ def add( microseconds=microseconds, ) - if units_of_variable_length or self.tzinfo is None: - return pendulum.datetime( + if units_of_variable_length or self.tz is None: + return self.__class__.create( dt.year, dt.month, dt.day, @@ -653,7 +614,7 @@ def add( tz=self.tz, ) - dt = self.__class__( + dt = datetime.datetime( dt.year, dt.month, dt.day, @@ -680,43 +641,17 @@ def add( def subtract( self, - years=0, - months=0, - weeks=0, - days=0, - hours=0, - minutes=0, - seconds=0, - microseconds=0, - ): + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: float = 0, + microseconds: int = 0, + ) -> Self: """ Remove duration from the instance. - - :param years: The number of years - :type years: int - - :param months: The number of months - :type months: int - - :param weeks: The number of weeks - :type weeks: int - - :param days: The number of days - :type days: int - - :param hours: The number of hours - :type hours: int - - :param minutes: The number of minutes - :type minutes: int - - :param seconds: The number of seconds - :type seconds: int - - :param microseconds: The number of microseconds - :type microseconds: int - - :rtype: DateTime """ return self.add( years=-years, @@ -732,16 +667,11 @@ def subtract( # Adding a final underscore to the method name # to avoid errors for PyPy which already defines # a _add_timedelta method - def _add_timedelta_(self, delta): + def _add_timedelta_(self, delta: datetime.timedelta) -> Self: """ Add timedelta duration to the instance. - - :param delta: The timedelta instance - :type delta: pendulum.Duration or datetime.timedelta - - :rtype: DateTime """ - if isinstance(delta, pendulum.Period): + if isinstance(delta, pendulum.Interval): return self.add( years=delta.years, months=delta.months, @@ -753,20 +683,13 @@ def _add_timedelta_(self, delta): microseconds=delta.microseconds, ) elif isinstance(delta, pendulum.Duration): - return self.add( - years=delta.years, months=delta.months, seconds=delta._total - ) + return self.add(**delta._signature) # type: ignore[attr-defined] return self.add(seconds=delta.total_seconds()) - def _subtract_timedelta(self, delta): + def _subtract_timedelta(self, delta: datetime.timedelta) -> Self: """ Remove timedelta duration from the instance. - - :param delta: The timedelta instance - :type delta: pendulum.Duration or datetime.timedelta - - :rtype: DateTime """ if isinstance(delta, pendulum.Duration): return self.subtract( @@ -777,28 +700,23 @@ def _subtract_timedelta(self, delta): # DIFFERENCES - def diff(self, dt=None, abs=True): + def diff( # type: ignore[override] + self, dt: datetime.datetime | None = None, abs: bool = True + ) -> Interval[datetime.datetime]: """ - Returns the difference between two DateTime objects represented as a Duration. - - :type dt: DateTime or None - - :param abs: Whether to return an absolute interval or not - :type abs: bool - - :rtype: Period + Returns the difference between two DateTime objects represented as an Interval. """ if dt is None: dt = self.now(self.tz) - return Period(self, dt, absolute=abs) + return Interval(self, dt, absolute=abs) - def diff_for_humans( + def diff_for_humans( # type: ignore[override] self, - other=None, # type: Optional[DateTime] - absolute=False, # type: bool - locale=None, # type: Optional[str] - ): # type: (...) -> str + other: DateTime | None = None, + absolute: bool = False, + locale: str | None = None, + ) -> str: """ Get the difference in a human readable format in the current locale. @@ -828,7 +746,7 @@ def diff_for_humans( return pendulum.format_diff(diff, is_now, absolute, locale) # Modifiers - def start_of(self, unit): + def start_of(self, unit: str) -> Self: """ Returns a copy of the instance with the time reset with the following rules: @@ -842,18 +760,13 @@ def start_of(self, unit): * year: date to first day of the year and time to 00:00:00 * decade: date to first day of the decade and time to 00:00:00 * century: date to first day of century and time to 00:00:00 - - :param unit: The unit to reset to - :type unit: str - - :rtype: DateTime """ if unit not in self._MODIFIERS_VALID_UNITS: - raise ValueError('Invalid unit "{}" for start_of()'.format(unit)) + raise ValueError(f'Invalid unit "{unit}" for start_of()') - return getattr(self, "_start_of_{}".format(unit))() + return cast("Self", getattr(self, f"_start_of_{unit}")()) - def end_of(self, unit): + def end_of(self, unit: str) -> Self: """ Returns a copy of the instance with the time reset with the following rules: @@ -867,164 +780,125 @@ def end_of(self, unit): * year: date to last day of the year and time to 23:59:59.999999 * decade: date to last day of the decade and time to 23:59:59.999999 * century: date to last day of century and time to 23:59:59.999999 - - :param unit: The unit to reset to - :type unit: str - - :rtype: DateTime """ if unit not in self._MODIFIERS_VALID_UNITS: - raise ValueError('Invalid unit "%s" for end_of()' % unit) + raise ValueError(f'Invalid unit "{unit}" for end_of()') - return getattr(self, "_end_of_%s" % unit)() + return cast("Self", getattr(self, f"_end_of_{unit}")()) - def _start_of_second(self): + def _start_of_second(self) -> Self: """ Reset microseconds to 0. - - :rtype: DateTime """ return self.set(microsecond=0) - def _end_of_second(self): + def _end_of_second(self) -> Self: """ Set microseconds to 999999. - - :rtype: DateTime """ return self.set(microsecond=999999) - def _start_of_minute(self): + def _start_of_minute(self) -> Self: """ Reset seconds and microseconds to 0. - - :rtype: DateTime """ return self.set(second=0, microsecond=0) - def _end_of_minute(self): + def _end_of_minute(self) -> Self: """ Set seconds to 59 and microseconds to 999999. - - :rtype: DateTime """ return self.set(second=59, microsecond=999999) - def _start_of_hour(self): + def _start_of_hour(self) -> Self: """ Reset minutes, seconds and microseconds to 0. - - :rtype: DateTime """ return self.set(minute=0, second=0, microsecond=0) - def _end_of_hour(self): + def _end_of_hour(self) -> Self: """ Set minutes and seconds to 59 and microseconds to 999999. - - :rtype: DateTime """ return self.set(minute=59, second=59, microsecond=999999) - def _start_of_day(self): + def _start_of_day(self) -> Self: """ - Reset the time to 00:00:00 - - :rtype: DateTime + Reset the time to 00:00:00. """ return self.at(0, 0, 0, 0) - def _end_of_day(self): + def _end_of_day(self) -> Self: """ - Reset the time to 23:59:59.999999 - - :rtype: DateTime + Reset the time to 23:59:59.999999. """ return self.at(23, 59, 59, 999999) - def _start_of_month(self): + def _start_of_month(self) -> Self: """ Reset the date to the first day of the month and the time to 00:00:00. - - :rtype: DateTime """ return self.set(self.year, self.month, 1, 0, 0, 0, 0) - def _end_of_month(self): + def _end_of_month(self) -> Self: """ Reset the date to the last day of the month and the time to 23:59:59.999999. - - :rtype: DateTime """ return self.set(self.year, self.month, self.days_in_month, 23, 59, 59, 999999) - def _start_of_year(self): + def _start_of_year(self) -> Self: """ Reset the date to the first day of the year and the time to 00:00:00. - - :rtype: DateTime """ return self.set(self.year, 1, 1, 0, 0, 0, 0) - def _end_of_year(self): + def _end_of_year(self) -> Self: """ Reset the date to the last day of the year - and the time to 23:59:59.999999 - - :rtype: DateTime + and the time to 23:59:59.999999. """ return self.set(self.year, 12, 31, 23, 59, 59, 999999) - def _start_of_decade(self): + def _start_of_decade(self) -> Self: """ Reset the date to the first day of the decade and the time to 00:00:00. - - :rtype: DateTime """ year = self.year - self.year % YEARS_PER_DECADE return self.set(year, 1, 1, 0, 0, 0, 0) - def _end_of_decade(self): + def _end_of_decade(self) -> Self: """ Reset the date to the last day of the decade and the time to 23:59:59.999999. - - :rtype: DateTime """ year = self.year - self.year % YEARS_PER_DECADE + YEARS_PER_DECADE - 1 return self.set(year, 12, 31, 23, 59, 59, 999999) - def _start_of_century(self): + def _start_of_century(self) -> Self: """ Reset the date to the first day of the century and the time to 00:00:00. - - :rtype: DateTime """ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + 1 return self.set(year, 1, 1, 0, 0, 0, 0) - def _end_of_century(self): + def _end_of_century(self) -> Self: """ Reset the date to the last day of the century and the time to 23:59:59.999999. - - :rtype: DateTime """ year = self.year - 1 - (self.year - 1) % YEARS_PER_CENTURY + YEARS_PER_CENTURY return self.set(year, 12, 31, 23, 59, 59, 999999) - def _start_of_week(self): + def _start_of_week(self) -> Self: """ Reset the date to the first day of the week and the time to 00:00:00. - - :rtype: DateTime """ dt = self @@ -1033,12 +907,10 @@ def _start_of_week(self): return dt.start_of("day") - def _end_of_week(self): + def _end_of_week(self) -> Self: """ Reset the date to the last day of the week and the time to 23:59:59. - - :rtype: DateTime """ dt = self @@ -1047,31 +919,20 @@ def _end_of_week(self): return dt.end_of("day") - def next(self, day_of_week=None, keep_time=False): + def next(self, day_of_week: WeekDay | None = None, keep_time: bool = False) -> Self: """ Modify to the next occurrence of a given day of the week. If no day_of_week is provided, modify to the next occurrence of the current day of the week. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :param day_of_week: The next day of week to reset to. - :type day_of_week: int or None - - :param keep_time: Whether to keep the time information or not. - :type keep_time: bool - - :rtype: DateTime """ if day_of_week is None: day_of_week = self.day_of_week - if day_of_week < SUNDAY or day_of_week > SATURDAY: + if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY: raise ValueError("Invalid day of week") - if keep_time: - dt = self - else: - dt = self.start_of("day") + dt = self if keep_time else self.start_of("day") dt = dt.add(days=1) while dt.day_of_week != day_of_week: @@ -1079,31 +940,22 @@ def next(self, day_of_week=None, keep_time=False): return dt - def previous(self, day_of_week=None, keep_time=False): + def previous( + self, day_of_week: WeekDay | None = None, keep_time: bool = False + ) -> Self: """ Modify to the previous occurrence of a given day of the week. If no day_of_week is provided, modify to the previous occurrence of the current day of the week. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :param day_of_week: The previous day of week to reset to. - :type day_of_week: int or None - - :param keep_time: Whether to keep the time information or not. - :type keep_time: bool - - :rtype: DateTime """ if day_of_week is None: day_of_week = self.day_of_week - if day_of_week < SUNDAY or day_of_week > SATURDAY: + if day_of_week < WeekDay.MONDAY or day_of_week > WeekDay.SUNDAY: raise ValueError("Invalid day of week") - if keep_time: - dt = self - else: - dt = self.start_of("day") + dt = self if keep_time else self.start_of("day") dt = dt.subtract(days=1) while dt.day_of_week != day_of_week: @@ -1111,49 +963,37 @@ def previous(self, day_of_week=None, keep_time=False): return dt - def first_of(self, unit, day_of_week=None): + def first_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self: """ Returns an instance set to the first occurrence of a given day of the week in the current unit. If no day_of_week is provided, modify to the first day of the unit. - Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. + Use the supplied consts to indicate the desired day_of_week, + ex. DateTime.MONDAY. Supported units are month, quarter and year. - - :param unit: The unit to use - :type unit: str - - :type day_of_week: int or None - - :rtype: DateTime """ if unit not in ["month", "quarter", "year"]: - raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + raise ValueError(f'Invalid unit "{unit}" for first_of()') - return getattr(self, "_first_of_{}".format(unit))(day_of_week) + return cast("Self", getattr(self, f"_first_of_{unit}")(day_of_week)) - def last_of(self, unit, day_of_week=None): + def last_of(self, unit: str, day_of_week: WeekDay | None = None) -> Self: """ Returns an instance set to the last occurrence of a given day of the week in the current unit. If no day_of_week is provided, modify to the last day of the unit. - Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. + Use the supplied consts to indicate the desired day_of_week, + ex. DateTime.MONDAY. Supported units are month, quarter and year. - - :param unit: The unit to use - :type unit: str - - :type day_of_week: int or None - - :rtype: DateTime """ if unit not in ["month", "quarter", "year"]: - raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + raise ValueError(f'Invalid unit "{unit}" for first_of()') - return getattr(self, "_last_of_{}".format(unit))(day_of_week) + return cast("Self", getattr(self, f"_last_of_{unit}")(day_of_week)) - def nth_of(self, unit, nth, day_of_week): + def nth_of(self, unit: str, nth: int, day_of_week: WeekDay) -> Self: """ Returns a new instance set to the given occurrence of a given day of the week in the current unit. @@ -1162,39 +1002,25 @@ def nth_of(self, unit, nth, day_of_week): to indicate the desired day_of_week, ex. DateTime.MONDAY. Supported units are month, quarter and year. - - :param unit: The unit to use - :type unit: str - - :type nth: int - - :type day_of_week: int or None - - :rtype: DateTime """ if unit not in ["month", "quarter", "year"]: - raise ValueError('Invalid unit "{}" for first_of()'.format(unit)) + raise ValueError(f'Invalid unit "{unit}" for first_of()') - dt = getattr(self, "_nth_of_{}".format(unit))(nth, day_of_week) - if dt is False: + dt = cast("Self | None", getattr(self, f"_nth_of_{unit}")(nth, day_of_week)) + if not dt: raise PendulumException( - "Unable to find occurence {} of {} in {}".format( - nth, self._days[day_of_week], unit - ) + f"Unable to find occurrence {nth}" + f" of {WeekDay(day_of_week).name.capitalize()} in {unit}" ) return dt - def _first_of_month(self, day_of_week): + def _first_of_month(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the first occurrence of a given day of the week in the current month. If no day_of_week is provided, modify to the first day of the month. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :type day_of_week: int - - :rtype: DateTime """ dt = self.start_of("day") @@ -1203,7 +1029,7 @@ def _first_of_month(self, day_of_week): month = calendar.monthcalendar(dt.year, dt.month) - calendar_day = (day_of_week - 1) % 7 + calendar_day = day_of_week if month[0][calendar_day] > 0: day_of_month = month[0][calendar_day] @@ -1212,16 +1038,12 @@ def _first_of_month(self, day_of_week): return dt.set(day=day_of_month) - def _last_of_month(self, day_of_week=None): + def _last_of_month(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current month. If no day_of_week is provided, modify to the last day of the month. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :type day_of_week: int or None - - :rtype: DateTime """ dt = self.start_of("day") @@ -1230,7 +1052,7 @@ def _last_of_month(self, day_of_week=None): month = calendar.monthcalendar(dt.year, dt.month) - calendar_day = (day_of_week - 1) % 7 + calendar_day = day_of_week if month[-1][calendar_day] > 0: day_of_month = month[-1][calendar_day] @@ -1239,74 +1061,58 @@ def _last_of_month(self, day_of_week=None): return dt.set(day=day_of_month) - def _nth_of_month(self, nth, day_of_week): + def _nth_of_month( + self, nth: int, day_of_week: WeekDay | None = None + ) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current month. If the calculated occurrence is outside, the scope of the current month, then return False and no modifications are made. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :type nth: int - - :type day_of_week: int or None - - :rtype: DateTime """ if nth == 1: return self.first_of("month", day_of_week) dt = self.first_of("month") check = dt.format("%Y-%M") - for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): dt = dt.next(day_of_week) if dt.format("%Y-%M") == check: return self.set(day=dt.day).start_of("day") - return False + return None - def _first_of_quarter(self, day_of_week=None): + def _first_of_quarter(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the first occurrence of a given day of the week in the current quarter. If no day_of_week is provided, modify to the first day of the quarter. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :type day_of_week: int or None - - :rtype: DateTime """ return self.on(self.year, self.quarter * 3 - 2, 1).first_of( "month", day_of_week ) - def _last_of_quarter(self, day_of_week=None): + def _last_of_quarter(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current quarter. If no day_of_week is provided, modify to the last day of the quarter. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :type day_of_week: int or None - - :rtype: DateTime """ return self.on(self.year, self.quarter * 3, 1).last_of("month", day_of_week) - def _nth_of_quarter(self, nth, day_of_week): + def _nth_of_quarter( + self, nth: int, day_of_week: WeekDay | None = None + ) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current quarter. If the calculated occurrence is outside, the scope of the current quarter, then return False and no modifications are made. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :type nth: int - - :type day_of_week: int or None - - :rtype: DateTime """ if nth == 1: return self.first_of("quarter", day_of_week) @@ -1315,75 +1121,59 @@ def _nth_of_quarter(self, nth, day_of_week): last_month = dt.month year = dt.year dt = dt.first_of("quarter") - for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): dt = dt.next(day_of_week) if last_month < dt.month or year != dt.year: - return False + return None return self.on(self.year, dt.month, dt.day).start_of("day") - def _first_of_year(self, day_of_week=None): + def _first_of_year(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the first occurrence of a given day of the week in the current year. If no day_of_week is provided, modify to the first day of the year. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :type day_of_week: int or None - - :rtype: DateTime """ return self.set(month=1).first_of("month", day_of_week) - def _last_of_year(self, day_of_week=None): + def _last_of_year(self, day_of_week: WeekDay | None = None) -> Self: """ Modify to the last occurrence of a given day of the week in the current year. If no day_of_week is provided, modify to the last day of the year. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :type day_of_week: int or None - - :rtype: DateTime """ return self.set(month=MONTHS_PER_YEAR).last_of("month", day_of_week) - def _nth_of_year(self, nth, day_of_week): + def _nth_of_year(self, nth: int, day_of_week: WeekDay | None = None) -> Self | None: """ Modify to the given occurrence of a given day of the week in the current year. If the calculated occurrence is outside, the scope of the current year, then return False and no modifications are made. Use the supplied consts to indicate the desired day_of_week, ex. DateTime.MONDAY. - - :type nth: int - - :type day_of_week: int or None - - :rtype: DateTime """ if nth == 1: return self.first_of("year", day_of_week) dt = self.first_of("year") year = dt.year - for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + for _ in range(nth - (1 if dt.day_of_week == day_of_week else 0)): dt = dt.next(day_of_week) if year != dt.year: - return False + return None return self.on(self.year, dt.month, dt.day).start_of("day") - def average(self, dt=None): + def average( # type: ignore[override] + self, dt: datetime.datetime | None = None + ) -> Self: """ Modify the current instance to the average of a given instance (default now) and the current instance. - - :type dt: DateTime or datetime - - :rtype: DateTime """ if dt is None: dt = self.now(self.tz) @@ -1393,7 +1183,15 @@ def average(self, dt=None): microseconds=(diff.in_seconds() * 1000000 + diff.microseconds) // 2 ) - def __sub__(self, other): + @overload # type: ignore[override] + def __sub__(self, other: datetime.timedelta) -> Self: ... + + @overload + def __sub__(self, other: DateTime) -> Interval[datetime.datetime]: ... + + def __sub__( + self, other: datetime.datetime | datetime.timedelta + ) -> Self | Interval[datetime.datetime]: if isinstance(other, datetime.timedelta): return self._subtract_timedelta(other) @@ -1412,11 +1210,11 @@ def __sub__(self, other): other.microsecond, ) else: - other = pendulum.instance(other) + other = self.instance(other) return other.diff(self, False) - def __rsub__(self, other): + def __rsub__(self, other: datetime.datetime) -> Interval[datetime.datetime]: if not isinstance(other, datetime.datetime): return NotImplemented @@ -1432,52 +1230,75 @@ def __rsub__(self, other): other.microsecond, ) else: - other = pendulum.instance(other) + other = self.instance(other) return self.diff(other, False) - def __add__(self, other): + def __add__(self, other: datetime.timedelta) -> Self: if not isinstance(other, datetime.timedelta): return NotImplemented + caller = traceback.extract_stack(limit=2)[0].name + if caller == "astimezone": + return super().__add__(other) + return self._add_timedelta_(other) - def __radd__(self, other): + def __radd__(self, other: datetime.timedelta) -> Self: return self.__add__(other) # Native methods override @classmethod - def fromtimestamp(cls, t, tz=None): - return pendulum.instance(datetime.datetime.fromtimestamp(t, tz=tz), tz=tz) + def fromtimestamp(cls, t: float, tz: datetime.tzinfo | None = None) -> Self: + tzinfo = pendulum._safe_timezone(tz) + + return cls.instance(datetime.datetime.fromtimestamp(t, tz=tzinfo), tz=tzinfo) @classmethod - def utcfromtimestamp(cls, t): - return pendulum.instance(datetime.datetime.utcfromtimestamp(t), tz=None) + def utcfromtimestamp(cls, t: float) -> Self: + return cls.instance(datetime.datetime.utcfromtimestamp(t), tz=None) @classmethod - def fromordinal(cls, n): - return pendulum.instance(datetime.datetime.fromordinal(n), tz=None) + def fromordinal(cls, n: int) -> Self: + return cls.instance(datetime.datetime.fromordinal(n), tz=None) @classmethod - def combine(cls, date, time): - return pendulum.instance(datetime.datetime.combine(date, time), tz=None) + def combine( + cls, + date: datetime.date, + time: datetime.time, + tzinfo: datetime.tzinfo | None = None, + ) -> Self: + return cls.instance(datetime.datetime.combine(date, time), tz=tzinfo) + + def astimezone(self, tz: datetime.tzinfo | None = None) -> Self: + dt = super().astimezone(tz) - def astimezone(self, tz=None): - return pendulum.instance(super(DateTime, self).astimezone(tz)) + return self.__class__( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + fold=dt.fold, + tzinfo=dt.tzinfo, + ) def replace( self, - year=None, - month=None, - day=None, - hour=None, - minute=None, - second=None, - microsecond=None, - tzinfo=True, - fold=None, - ): + year: SupportsIndex | None = None, + month: SupportsIndex | None = None, + day: SupportsIndex | None = None, + hour: SupportsIndex | None = None, + minute: SupportsIndex | None = None, + second: SupportsIndex | None = None, + microsecond: SupportsIndex | None = None, + tzinfo: bool | datetime.tzinfo | Literal[True] | None = True, + fold: int | None = None, + ) -> Self: if year is None: year = self.year if month is None: @@ -1497,13 +1318,10 @@ def replace( if fold is None: fold = self.fold - transition_rule = pendulum.POST_TRANSITION - if fold is not None: - transition_rule = pendulum.PRE_TRANSITION - if fold: - transition_rule = pendulum.POST_TRANSITION + if tzinfo is not None: + tzinfo = pendulum._safe_timezone(tzinfo) - return pendulum.datetime( + return self.__class__.create( year, month, day, @@ -1512,13 +1330,15 @@ def replace( second, microsecond, tz=tzinfo, - dst_rule=transition_rule, + fold=fold, ) - def __getnewargs__(self): + def __getnewargs__(self) -> tuple[Self]: return (self,) - def _getstate(self, protocol=3): + def _getstate( + self, protocol: SupportsIndex = 3 + ) -> tuple[int, int, int, int, int, int, int, datetime.tzinfo | None]: return ( self.year, self.month, @@ -1530,20 +1350,38 @@ def _getstate(self, protocol=3): self.tzinfo, ) - def __reduce__(self): + def __reduce__( + self, + ) -> tuple[ + type[Self], + tuple[int, int, int, int, int, int, int, datetime.tzinfo | None], + ]: return self.__reduce_ex__(2) - def __reduce_ex__(self, protocol): + def __reduce_ex__( + self, protocol: SupportsIndex + ) -> tuple[ + type[Self], + tuple[int, int, int, int, int, int, int, datetime.tzinfo | None], + ]: return self.__class__, self._getstate(protocol) - def _cmp(self, other, **kwargs): + def __deepcopy__(self, _: dict[int, Self]) -> Self: + return self.__class__( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + tzinfo=self.tz, + fold=self.fold, + ) + + def _cmp(self, other: datetime.datetime, **kwargs: Any) -> int: # Fix for pypy which compares using this method # which would lead to infinite recursion if we didn't override - kwargs = {"tzinfo": self.tz} - - if _HAS_FOLD: - kwargs["fold"] = self.fold - dt = datetime.datetime( self.year, self.month, @@ -1552,7 +1390,8 @@ def _cmp(self, other, **kwargs): self.minute, self.second, self.microsecond, - **kwargs + tzinfo=self.tz, + fold=self.fold, ) return 0 if dt == other else 1 if dt > other else -1 @@ -1560,4 +1399,4 @@ def _cmp(self, other, **kwargs): DateTime.min = DateTime(1, 1, 1, 0, 0, tzinfo=UTC) DateTime.max = DateTime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=UTC) -DateTime.EPOCH = DateTime(1970, 1, 1) +DateTime.EPOCH = DateTime(1970, 1, 1, tzinfo=UTC) diff --git a/src/pendulum/day.py b/src/pendulum/day.py new file mode 100644 index 00000000..7bfffca1 --- /dev/null +++ b/src/pendulum/day.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from enum import IntEnum + + +class WeekDay(IntEnum): + MONDAY = 0 + TUESDAY = 1 + WEDNESDAY = 2 + THURSDAY = 3 + FRIDAY = 4 + SATURDAY = 5 + SUNDAY = 6 diff --git a/pendulum/duration.py b/src/pendulum/duration.py similarity index 62% rename from pendulum/duration.py rename to src/pendulum/duration.py index 45df13da..d6cc0657 100644 --- a/pendulum/duration.py +++ b/src/pendulum/duration.py @@ -1,20 +1,24 @@ -from __future__ import absolute_import -from __future__ import division +from __future__ import annotations from datetime import timedelta +from typing import TYPE_CHECKING +from typing import cast +from typing import overload import pendulum +from pendulum.constants import SECONDS_PER_DAY +from pendulum.constants import SECONDS_PER_HOUR +from pendulum.constants import SECONDS_PER_MINUTE +from pendulum.constants import US_PER_SECOND from pendulum.utils._compat import PYPY -from pendulum.utils._compat import decode -from .constants import SECONDS_PER_DAY -from .constants import SECONDS_PER_HOUR -from .constants import SECONDS_PER_MINUTE -from .constants import US_PER_SECOND +if TYPE_CHECKING: + from typing_extensions import Self -def _divide_and_round(a, b): + +def _divide_and_round(a: float, b: float) -> int: """divide a by b and round result to the nearest integer When the ratio is exactly half-way between two integers, @@ -23,12 +27,17 @@ def _divide_and_round(a, b): # Based on the reference implementation for divmod_near # in Objects/longobject.c. q, r = divmod(a, b) + + # The output of divmod() is either a float or an int, + # but we always want it to be an int. + q = int(q) + # round up if either r / b > 0.5, or r / b == 0.5 and q is odd. # The expression r / b > 0.5 is equivalent to 2 * r > b if b is # positive, 2 * r < b if b negative. r *= 2 greater_than_half = r > b if b > 0 else r < b - if greater_than_half or r == b and q % 2 == 1: + if greater_than_half or (r == b and q % 2 == 1): q += 1 return q @@ -41,6 +50,15 @@ class Duration(timedelta): Provides several improvements over the base class. """ + _total: float = 0 + _years: int = 0 + _months: int = 0 + _weeks: int = 0 + _days: int = 0 + _remaining_days: int = 0 + _seconds: int = 0 + _microseconds: int = 0 + _y = None _m = None _w = None @@ -52,16 +70,16 @@ class Duration(timedelta): def __new__( cls, - days=0, - seconds=0, - microseconds=0, - milliseconds=0, - minutes=0, - hours=0, - weeks=0, - years=0, - months=0, - ): + days: float = 0, + seconds: float = 0, + microseconds: float = 0, + milliseconds: float = 0, + minutes: float = 0, + hours: float = 0, + weeks: float = 0, + years: float = 0, + months: float = 0, + ) -> Self: if not isinstance(years, int) or not isinstance(months, int): raise ValueError("Float year and months are not supported") @@ -94,23 +112,34 @@ def __new__( self._months = months self._years = years + self._signature = { # type: ignore[attr-defined] + "years": years, + "months": months, + "weeks": weeks, + "days": days, + "hours": hours, + "minutes": minutes, + "seconds": seconds, + "microseconds": microseconds + milliseconds * 1000, + } + return self - def total_minutes(self): + def total_minutes(self) -> float: return self.total_seconds() / SECONDS_PER_MINUTE - def total_hours(self): + def total_hours(self) -> float: return self.total_seconds() / SECONDS_PER_HOUR - def total_days(self): + def total_days(self) -> float: return self.total_seconds() / SECONDS_PER_DAY - def total_weeks(self): + def total_weeks(self) -> float: return self.total_days() / 7 if PYPY: - def total_seconds(self): + def total_seconds(self) -> float: days = 0 if hasattr(self, "_years"): @@ -130,29 +159,29 @@ def total_seconds(self): ) / US_PER_SECOND @property - def years(self): + def years(self) -> int: return self._years @property - def months(self): + def months(self) -> int: return self._months @property - def weeks(self): + def weeks(self) -> int: return self._weeks if PYPY: @property - def days(self): + def days(self) -> int: return self._years * 365 + self._months * 30 + self._days @property - def remaining_days(self): + def remaining_days(self) -> int: return self._remaining_days @property - def hours(self): + def hours(self) -> int: if self._h is None: seconds = self._seconds self._h = 0 @@ -162,7 +191,7 @@ def hours(self): return self._h @property - def minutes(self): + def minutes(self) -> int: if self._i is None: seconds = self._seconds self._i = 0 @@ -172,11 +201,11 @@ def minutes(self): return self._i @property - def seconds(self): + def seconds(self) -> int: return self._seconds @property - def remaining_seconds(self): + def remaining_seconds(self) -> int: if self._s is None: self._s = self._seconds self._s = abs(self._s) % 60 * self._sign(self._s) @@ -184,46 +213,41 @@ def remaining_seconds(self): return self._s @property - def microseconds(self): + def microseconds(self) -> int: return self._microseconds @property - def invert(self): + def invert(self) -> bool: if self._invert is None: self._invert = self.total_seconds() < 0 return self._invert - def in_weeks(self): + def in_weeks(self) -> int: return int(self.total_weeks()) - def in_days(self): + def in_days(self) -> int: return int(self.total_days()) - def in_hours(self): + def in_hours(self) -> int: return int(self.total_hours()) - def in_minutes(self): + def in_minutes(self) -> int: return int(self.total_minutes()) - def in_seconds(self): + def in_seconds(self) -> int: return int(self.total_seconds()) - def in_words(self, locale=None, separator=" "): + def in_words(self, locale: str | None = None, separator: str = " ") -> str: """ Get the current interval in words in the current locale. Ex: 6 jours 23 heures 58 minutes :param locale: The locale to use. Defaults to current locale. - :type locale: str - :param separator: The separator to use between each unit - :type separator: str - - :rtype: str """ - periods = [ + intervals = [ ("year", self.years), ("month", self.months), ("week", self.weeks), @@ -236,77 +260,76 @@ def in_words(self, locale=None, separator=" "): if locale is None: locale = pendulum.get_locale() - locale = pendulum.locale(locale) + loaded_locale = pendulum.locale(locale) + parts = [] - for period in periods: - unit, count = period - if abs(count) > 0: - translation = locale.translation( - "units.{}.{}".format(unit, locale.plural(abs(count))) + for interval in intervals: + unit, interval_count = interval + if abs(interval_count) > 0: + translation = loaded_locale.translation( + f"units.{unit}.{loaded_locale.plural(abs(interval_count))}" ) - parts.append(translation.format(count)) + parts.append(translation.format(interval_count)) if not parts: - if abs(self.microseconds) > 0: - unit = "units.second.{}".format(locale.plural(1)) - count = "{:.2f}".format(abs(self.microseconds) / 1e6) + count: int | str = 0 + if self.microseconds != 0: + unit = f"units.second.{loaded_locale.plural(0)}" + count = f"{abs(self.microseconds) / 1e6:.2f}" else: - unit = "units.microsecond.{}".format(locale.plural(0)) - count = 0 - translation = locale.translation(unit) + unit = f"units.microsecond.{loaded_locale.plural(0)}" + translation = loaded_locale.translation(unit) parts.append(translation.format(count)) - return decode(separator.join(parts)) + return separator.join(parts) - def _sign(self, value): + def _sign(self, value: float) -> int: if value < 0: return -1 return 1 - def as_timedelta(self): + def as_timedelta(self) -> timedelta: """ Return the interval as a native timedelta. - - :rtype: timedelta """ return timedelta(seconds=self.total_seconds()) - def __str__(self): + def __str__(self) -> str: return self.in_words() - def __repr__(self): - rep = "{}(".format(self.__class__.__name__) + def __repr__(self) -> str: + rep = f"{self.__class__.__name__}(" if self._years: - rep += "years={}, ".format(self._years) + rep += f"years={self._years}, " if self._months: - rep += "months={}, ".format(self._months) + rep += f"months={self._months}, " if self._weeks: - rep += "weeks={}, ".format(self._weeks) + rep += f"weeks={self._weeks}, " if self._days: - rep += "days={}, ".format(self._remaining_days) + rep += f"days={self._remaining_days}, " if self.hours: - rep += "hours={}, ".format(self.hours) + rep += f"hours={self.hours}, " if self.minutes: - rep += "minutes={}, ".format(self.minutes) + rep += f"minutes={self.minutes}, " if self.remaining_seconds: - rep += "seconds={}, ".format(self.remaining_seconds) + rep += f"seconds={self.remaining_seconds}, " if self.microseconds: - rep += "microseconds={}, ".format(self.microseconds) + rep += f"microseconds={self.microseconds}, " rep += ")" return rep.replace(", )", ")") - def __add__(self, other): + def __add__(self, other: timedelta) -> Self: if isinstance(other, timedelta): return self.__class__(seconds=self.total_seconds() + other.total_seconds()) @@ -314,13 +337,13 @@ def __add__(self, other): __radd__ = __add__ - def __sub__(self, other): + def __sub__(self, other: timedelta) -> Self: if isinstance(other, timedelta): return self.__class__(seconds=self.total_seconds() - other.total_seconds()) return NotImplemented - def __neg__(self): + def __neg__(self) -> Self: return self.__class__( years=-self._years, months=-self._months, @@ -330,10 +353,10 @@ def __neg__(self): microseconds=-self._microseconds, ) - def _to_microseconds(self): + def _to_microseconds(self) -> int: return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds - def __mul__(self, other): + def __mul__(self, other: int | float) -> Self: if isinstance(other, int): return self.__class__( years=self._years * other, @@ -351,13 +374,22 @@ def __mul__(self, other): __rmul__ = __mul__ - def __floordiv__(self, other): + @overload + def __floordiv__(self, other: timedelta) -> int: ... + + @overload + def __floordiv__(self, other: int) -> Self: ... + + def __floordiv__(self, other: int | timedelta) -> int | Duration: if not isinstance(other, (int, timedelta)): return NotImplemented usec = self._to_microseconds() if isinstance(other, timedelta): - return usec // other._to_microseconds() + return cast( + "int", + usec // other._to_microseconds(), # type: ignore[attr-defined] + ) if isinstance(other, int): return self.__class__( @@ -368,13 +400,22 @@ def __floordiv__(self, other): months=self._months // other, ) - def __truediv__(self, other): + @overload + def __truediv__(self, other: timedelta) -> float: ... + + @overload + def __truediv__(self, other: float) -> Self: ... + + def __truediv__(self, other: int | float | timedelta) -> Self | float: if not isinstance(other, (int, float, timedelta)): return NotImplemented usec = self._to_microseconds() if isinstance(other, timedelta): - return usec / other._to_microseconds() + return cast( + "float", + usec / other._to_microseconds(), # type: ignore[attr-defined] + ) if isinstance(other, int): return self.__class__( @@ -398,22 +439,37 @@ def __truediv__(self, other): __div__ = __floordiv__ - def __mod__(self, other): + def __mod__(self, other: timedelta) -> Self: if isinstance(other, timedelta): - r = self._to_microseconds() % other._to_microseconds() + r = self._to_microseconds() % other._to_microseconds() # type: ignore[attr-defined] return self.__class__(0, 0, r) return NotImplemented - def __divmod__(self, other): + def __divmod__(self, other: timedelta) -> tuple[int, Duration]: if isinstance(other, timedelta): - q, r = divmod(self._to_microseconds(), other._to_microseconds()) + q, r = divmod( + self._to_microseconds(), + other._to_microseconds(), # type: ignore[attr-defined] + ) return q, self.__class__(0, 0, r) return NotImplemented + def __deepcopy__(self, _: dict[int, Self]) -> Self: + return self.__class__( + days=self.remaining_days, + seconds=self.remaining_seconds, + microseconds=self.microseconds, + minutes=self.minutes, + hours=self.hours, + years=self.years, + months=self.months, + weeks=self.weeks, + ) + Duration.min = Duration(days=-999999999) Duration.max = Duration( @@ -429,16 +485,16 @@ class AbsoluteDuration(Duration): def __new__( cls, - days=0, - seconds=0, - microseconds=0, - milliseconds=0, - minutes=0, - hours=0, - weeks=0, - years=0, - months=0, - ): + days: float = 0, + seconds: float = 0, + microseconds: float = 0, + milliseconds: float = 0, + minutes: float = 0, + hours: float = 0, + weeks: float = 0, + years: float = 0, + months: float = 0, + ) -> AbsoluteDuration: if not isinstance(years, int) or not isinstance(months, int): raise ValueError("Float year and months are not supported") @@ -457,22 +513,19 @@ def __new__( total = abs(self._total) self._microseconds = round(total % 1 * 1e6) - self._seconds = int(total) % SECONDS_PER_DAY - - days = int(total) // SECONDS_PER_DAY + days, self._seconds = divmod(int(total), SECONDS_PER_DAY) self._days = abs(days + years * 365 + months * 30) - self._remaining_days = days % 7 - self._weeks = days // 7 + self._weeks, self._remaining_days = divmod(days, 7) self._months = abs(months) self._years = abs(years) return self - def total_seconds(self): + def total_seconds(self) -> float: return abs(self._total) @property - def invert(self): + def invert(self) -> bool: if self._invert is None: self._invert = self._total < 0 diff --git a/src/pendulum/exceptions.py b/src/pendulum/exceptions.py new file mode 100644 index 00000000..16872119 --- /dev/null +++ b/src/pendulum/exceptions.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from pendulum.parsing.exceptions import ParserError + + +class PendulumException(Exception): + pass + + +__all__ = [ + "ParserError", + "PendulumException", +] diff --git a/src/pendulum/formatting/__init__.py b/src/pendulum/formatting/__init__.py new file mode 100644 index 00000000..0c6e725d --- /dev/null +++ b/src/pendulum/formatting/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from pendulum.formatting.formatter import Formatter + + +__all__ = ["Formatter"] diff --git a/pendulum/formatting/difference_formatter.py b/src/pendulum/formatting/difference_formatter.py similarity index 54% rename from pendulum/formatting/difference_formatter.py rename to src/pendulum/formatting/difference_formatter.py index 9be4b962..2440ee6f 100644 --- a/pendulum/formatting/difference_formatter.py +++ b/src/pendulum/formatting/difference_formatter.py @@ -1,73 +1,87 @@ -import typing +from __future__ import annotations -import pendulum +import typing as t -from pendulum.utils._compat import decode +from pendulum.locales.locale import Locale -from ..locales.locale import Locale +if t.TYPE_CHECKING: + from pendulum import Duration -class DifferenceFormatter(object): +DAYS_THRESHOLD_FOR_HALF_WEEK = 3 +DAYS_THRESHOLD_FOR_HALF_MONTH = 15 +MONTHS_THRESHOLD_FOR_HALF_YEAR = 6 + +HOURS_IN_NEARLY_A_DAY = 22 +DAYS_IN_NEARLY_A_MONTH = 27 +MONTHS_IN_NEARLY_A_YEAR = 11 + +DAYS_OF_WEEK = 7 +SECONDS_OF_MINUTE = 60 +FEW_SECONDS_MAX = 10 + +KEY_FUTURE = ".future" +KEY_PAST = ".past" +KEY_AFTER = ".after" +KEY_BEFORE = ".before" + + +class DifferenceFormatter: """ Handles formatting differences in text. """ - def __init__(self, locale="en"): + def __init__(self, locale: str = "en") -> None: self._locale = Locale.load(locale) def format( - self, diff, is_now=True, absolute=False, locale=None - ): # type: (pendulum.Period, bool, bool, typing.Optional[str]) -> str + self, + diff: Duration, + is_now: bool = True, + absolute: bool = False, + locale: str | Locale | None = None, + ) -> str: """ Formats a difference. :param diff: The difference to format - :type diff: pendulum.period.Period - :param is_now: Whether the difference includes now - :type is_now: bool - :param absolute: Whether it's an absolute difference or not - :type absolute: bool - :param locale: The locale to use - :type locale: str or None - - :rtype: str """ - if locale is None: - locale = self._locale - else: - locale = Locale.load(locale) - - count = diff.remaining_seconds + locale = self._locale if locale is None else Locale.load(locale) if diff.years > 0: unit = "year" count = diff.years - if diff.months > 6: + if diff.months > MONTHS_THRESHOLD_FOR_HALF_YEAR: count += 1 - elif diff.months == 11 and (diff.weeks * 7 + diff.remaining_days) > 15: + elif (diff.months == MONTHS_IN_NEARLY_A_YEAR) and ( + (diff.weeks * DAYS_OF_WEEK + diff.remaining_days) + > DAYS_THRESHOLD_FOR_HALF_MONTH + ): unit = "year" count = 1 elif diff.months > 0: unit = "month" count = diff.months - if (diff.weeks * 7 + diff.remaining_days) >= 27: + if ( + diff.weeks * DAYS_OF_WEEK + diff.remaining_days + ) >= DAYS_IN_NEARLY_A_MONTH: count += 1 elif diff.weeks > 0: unit = "week" count = diff.weeks - if diff.remaining_days > 3: + if diff.remaining_days > DAYS_THRESHOLD_FOR_HALF_WEEK: count += 1 elif diff.remaining_days > 0: unit = "day" count = diff.remaining_days - if diff.hours >= 22: + if diff.hours >= HOURS_IN_NEARLY_A_DAY: count += 1 elif diff.hours > 0: unit = "hour" @@ -75,7 +89,7 @@ def format( elif diff.minutes > 0: unit = "minute" count = diff.minutes - elif 10 < diff.remaining_seconds <= 59: + elif FEW_SECONDS_MAX < diff.remaining_seconds < SECONDS_OF_MINUTE: unit = "second" count = diff.remaining_seconds else: @@ -83,7 +97,7 @@ def format( time = locale.get("custom.units.few_second") if time is not None: if absolute: - return time + return t.cast("str", time) key = "custom" is_future = diff.invert @@ -94,32 +108,29 @@ def format( key += ".ago" else: if is_future: - key += ".after" + key += KEY_AFTER else: - key += ".before" + key += KEY_BEFORE - return locale.get(key).format(time) + return t.cast("str", locale.get(key).format(time)) else: unit = "second" count = diff.remaining_seconds - if count == 0: count = 1 - if absolute: - key = "translations.units.{}".format(unit) + key = f"translations.units.{unit}" else: is_future = diff.invert - if is_now: # Relative to now, so we can use # the CLDR data - key = "translations.relative.{}".format(unit) + key = f"translations.relative.{unit}" if is_future: - key += ".future" + key += KEY_FUTURE else: - key += ".past" + key += KEY_PAST else: # Absolute comparison # So we have to use the custom locale data @@ -127,27 +138,26 @@ def format( # Checking for special pluralization rules key = "custom.units_relative" if is_future: - key += ".{}.future".format(unit) + key += f".{unit}{KEY_FUTURE}" else: - key += ".{}.past".format(unit) + key += f".{unit}{KEY_PAST}" trans = locale.get(key) if not trans: # No special rule - time = locale.get( - "translations.units.{}.{}".format(unit, locale.plural(count)) - ).format(count) + key = f"translations.units.{unit}.{locale.plural(count)}" + time = locale.get(key).format(count) else: time = trans[locale.plural(count)].format(count) key = "custom" if is_future: - key += ".after" + key += KEY_AFTER else: - key += ".before" + key += KEY_BEFORE - return locale.get(key).format(decode(time)) + return t.cast("str", locale.get(key).format(time)) - key += ".{}".format(locale.plural(count)) + key += f".{locale.plural(count)}" - return decode(locale.get(key).format(count)) + return t.cast("str", locale.get(key).format(count)) diff --git a/pendulum/formatting/formatter.py b/src/pendulum/formatting/formatter.py similarity index 68% rename from pendulum/formatting/formatter.py rename to src/pendulum/formatting/formatter.py index 9b64fcd6..b09977d0 100644 --- a/pendulum/formatting/formatter.py +++ b/src/pendulum/formatting/formatter.py @@ -1,16 +1,25 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations import datetime import re -import typing + +from re import Match +from typing import TYPE_CHECKING +from typing import Any +from typing import ClassVar +from typing import cast import pendulum from pendulum.locales.locale import Locale -from pendulum.utils._compat import decode +if TYPE_CHECKING: + from collections.abc import Callable + from collections.abc import Sequence + + from pendulum import Timezone + _MATCH_1 = r"\d" _MATCH_2 = r"\d\d" _MATCH_3 = r"\d{3}" @@ -30,19 +39,18 @@ _MATCH_TIMESTAMP = r"[+-]?\d+(\.\d{1,6})?" _MATCH_WORD = ( "(?i)[0-9]*" - "['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+" + "['a-z\u00a0-\u05ff\u0700-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]+" r"|[\u0600-\u06FF/]+(\s*?[\u0600-\u06FF]+){1,2}" ) _MATCH_TIMEZONE = "[A-Za-z0-9-+]+(/[A-Za-z0-9-+_]+)?" class Formatter: - - _TOKENS = ( + _TOKENS: str = ( r"\[([^\[]*)\]|\\(.)|" "(" "Mo|MM?M?M?" - "|Do|DDDo|DD?D?D?|ddd?d?|do?" + "|Do|DDDo|DD?D?D?|ddd?d?|do?|eo?" "|E{1,4}" "|w[o|w]?|W[o|W]?|Qo?" "|YYYY|YY|Y" @@ -56,23 +64,27 @@ class Formatter: ")" ) - _FORMAT_RE = re.compile(_TOKENS) + _FORMAT_RE: re.Pattern[str] = re.compile(_TOKENS) - _FROM_FORMAT_RE = re.compile(r"(? str + self, dt: pendulum.DateTime, fmt: str, locale: str | Locale | None = None + ) -> str: """ Formats a DateTime instance with a given format and locale. :param dt: The instance to format - :type dt: pendulum.DateTime - :param fmt: The format to use - :type fmt: str - :param locale: The locale to use - :type locale: str or Locale or None - - :rtype: str """ - if not locale: - locale = pendulum.get_locale() - - locale = Locale.load(locale) + loaded_locale: Locale = Locale.load(locale or pendulum.get_locale()) result = self._FORMAT_RE.sub( lambda m: m.group(1) if m.group(1) else m.group(2) if m.group(2) - else self._format_token(dt, m.group(3), locale), + else self._format_token(dt, m.group(3), loaded_locale), fmt, ) - return decode(result) + return result - def _format_token( - self, dt, token, locale - ): # type: (pendulum.DateTime, str, Locale) -> str + def _format_token(self, dt: pendulum.DateTime, token: str, locale: Locale) -> str: """ Formats a DateTime instance with a given token and locale. :param dt: The instance to format - :type dt: pendulum.DateTime - :param token: The token to use - :type token: str - :param locale: The locale to use - :type locale: Locale - - :rtype: str """ if token in self._DATE_FORMATS: - fmt = locale.get("custom.date_formats.{}".format(token)) + fmt = locale.get(f"custom.date_formats.{token}") if fmt is None: fmt = self._DEFAULT_DATE_FORMATS[token] @@ -301,47 +295,46 @@ def _format_token( offset = dt.utcoffset() or datetime.timedelta() minutes = offset.total_seconds() / 60 - if minutes >= 0: - sign = "+" - else: - sign = "-" + sign = "+" if minutes >= 0 else "-" hour, minute = divmod(abs(int(minutes)), 60) - return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute) + return f"{sign}{hour:02d}{separator}{minute:02d}" + + return token def _format_localizable_token( - self, dt, token, locale - ): # type: (pendulum.DateTime, str, Locale) -> str + self, dt: pendulum.DateTime, token: str, locale: Locale + ) -> str: """ Formats a DateTime instance with a given localizable token and locale. :param dt: The instance to format - :type dt: pendulum.DateTime - :param token: The token to use - :type token: str - :param locale: The locale to use - :type locale: Locale - - :rtype: str """ if token == "MMM": - return locale.get("translations.months.abbreviated")[dt.month] + return cast("str", locale.get("translations.months.abbreviated")[dt.month]) elif token == "MMMM": - return locale.get("translations.months.wide")[dt.month] + return cast("str", locale.get("translations.months.wide")[dt.month]) elif token == "dd": - return locale.get("translations.days.short")[dt.day_of_week] + return cast("str", locale.get("translations.days.short")[dt.day_of_week]) elif token == "ddd": - return locale.get("translations.days.abbreviated")[dt.day_of_week] + return cast( + "str", + locale.get("translations.days.abbreviated")[dt.day_of_week], + ) elif token == "dddd": - return locale.get("translations.days.wide")[dt.day_of_week] + return cast("str", locale.get("translations.days.wide")[dt.day_of_week]) + elif token == "e": + first_day = cast("int", locale.get("translations.week_data.first_day")) + + return str((dt.day_of_week % 7 - first_day) % 7) elif token == "Do": return locale.ordinalize(dt.day) elif token == "do": - return locale.ordinalize(dt.day_of_week) + return locale.ordinalize((dt.day_of_week + 1) % 7) elif token == "Mo": return locale.ordinalize(dt.month) elif token == "Qo": @@ -350,6 +343,10 @@ def _format_localizable_token( return locale.ordinalize(dt.week_of_year) elif token == "DDDo": return locale.ordinalize(dt.day_of_year) + elif token == "eo": + first_day = cast("int", locale.get("translations.week_data.first_day")) + + return locale.ordinalize((dt.day_of_week % 7 - first_day) % 7 + 1) elif token == "A": key = "translations.day_periods" if dt.hour >= 12: @@ -357,17 +354,17 @@ def _format_localizable_token( else: key += ".am" - return locale.get(key) + return cast("str", locale.get(key)) else: return token def parse( self, - time, # type: str - fmt, # type: str - now, # type: pendulum.DateTime - locale=None, # type: typing.Optional[str] - ): # type: (...) -> typing.Dict[str, typing.Any] + time: str, + fmt: str, + now: pendulum.DateTime, + locale: str | None = None, + ) -> dict[str, Any]: """ Parses a time string matching a given format as a tuple. @@ -380,14 +377,13 @@ def parse( """ escaped_fmt = re.escape(fmt) - tokens = self._FROM_FORMAT_RE.findall(escaped_fmt) - if not tokens: - return time + if not self._FROM_FORMAT_RE.search(escaped_fmt): + raise ValueError("The given time string does not match the given format") if not locale: locale = pendulum.get_locale() - locale = Locale.load(locale) + loaded_locale: Locale = Locale.load(locale) parsed = { "year": None, @@ -406,19 +402,22 @@ def parse( } pattern = self._FROM_FORMAT_RE.sub( - lambda m: self._replace_tokens(m.group(0), locale), escaped_fmt + lambda m: self._replace_tokens(m.group(0), loaded_locale), escaped_fmt ) - if not re.search("^" + pattern + "$", time): - raise ValueError("String does not match format {}".format(fmt)) + if not re.fullmatch(pattern, time): + raise ValueError(f"String does not match format {fmt}") - re.sub(pattern, lambda m: self._get_parsed_values(m, parsed, locale, now), time) + def _get_parsed_values(m: Match[str]) -> Any: + return self._get_parsed_values(m, parsed, loaded_locale, now) + + re.sub(pattern, _get_parsed_values, time) return self._check_parsed(parsed, now) def _check_parsed( - self, parsed, now - ): # type: (typing.Dict[str, typing.Any], pendulum.DateTime) -> typing.Dict[str, typing.Any] + self, parsed: dict[str, Any], now: pendulum.DateTime + ) -> dict[str, Any]: """ Checks validity of parsed elements. @@ -426,7 +425,7 @@ def _check_parsed( :return: The validated elements. """ - validated = { + validated: dict[str, int | Timezone | None] = { "year": parsed["year"], "month": parsed["month"], "day": parsed["day"], @@ -442,7 +441,7 @@ def _check_parsed( if parsed["timestamp"] is not None: str_us = str(parsed["timestamp"]) if "." in str_us: - microseconds = int("{}".format(str_us.split(".")[1].ljust(6, "0"))) + microseconds = int(f"{str_us.split('.')[1].ljust(6, '0')}") else: microseconds = 0 @@ -461,7 +460,7 @@ def _check_parsed( if parsed["quarter"] is not None: if validated["year"] is not None: - dt = pendulum.datetime(validated["year"], 1, 1) + dt = pendulum.datetime(cast("int", validated["year"]), 1, 1) else: dt = now @@ -478,8 +477,9 @@ def _check_parsed( validated["year"] = now.year if parsed["day_of_year"] is not None: - dt = pendulum.parse( - "{}-{:>03d}".format(validated["year"], parsed["day_of_year"]) + dt = cast( + "pendulum.DateTime", + pendulum.parse(f"{validated['year']}-{parsed['day_of_year']:>03d}"), ) validated["month"] = dt.month @@ -487,9 +487,9 @@ def _check_parsed( if parsed["day_of_week"] is not None: dt = pendulum.datetime( - validated["year"], - validated["month"] or now.month, - validated["day"] or now.day, + cast("int", validated["year"]), + cast("int", validated["month"]) or now.month, + cast("int", validated["day"]) or now.day, ) dt = dt.start_of("week").subtract(days=1) dt = dt.next(parsed["day_of_week"]) @@ -514,9 +514,9 @@ def _check_parsed( raise ValueError("Invalid date") pm = parsed["meridiem"] == "pm" - validated["hour"] %= 12 + validated["hour"] %= 12 # type: ignore[operator] if pm: - validated["hour"] += 12 + validated["hour"] += 12 # type: ignore[operator] if validated["month"] is None: if parsed["year"] is not None: @@ -539,8 +539,12 @@ def _check_parsed( return validated def _get_parsed_values( - self, m, parsed, locale, now - ): # type: (typing.Match[str], typing.Dict[str, typing.Any], Locale, pendulum.DateTime) -> None + self, + m: Match[str], + parsed: dict[str, Any], + locale: Locale, + now: pendulum.DateTime, + ) -> None: for token, index in m.re.groupindex.items(): if token in self._LOCALIZABLE_TOKENS: self._get_parsed_locale_value(token, m.group(index), parsed, locale) @@ -548,16 +552,23 @@ def _get_parsed_values( self._get_parsed_value(token, m.group(index), parsed, now) def _get_parsed_value( - self, token, value, parsed, now - ): # type: (str, str, typing.Dict[str, typing.Any], pendulum.DateTime) -> None + self, + token: str, + value: str, + parsed: dict[str, Any], + now: pendulum.DateTime, + ) -> None: parsed_token = self._PARSE_TOKENS[token](value) if "Y" in token: if token == "YY": - parsed_token = now.year // 100 * 100 + parsed_token + if parsed_token <= 68: + parsed_token += 2000 + else: + parsed_token += 1900 parsed["year"] = parsed_token - elif "Q" == token: + elif token == "Q": parsed["quarter"] = parsed_token elif token in ["MM", "M"]: parsed["month"] = parsed_token @@ -583,11 +594,11 @@ def _get_parsed_value( elif token in ["X", "x"]: parsed["timestamp"] = parsed_token elif token in ["ZZ", "Z"]: - negative = True if value.startswith("-") else False + negative = bool(value.startswith("-")) tz = value[1:] if ":" not in tz: if len(tz) == 2: - tz = "{}00".format(tz) + tz = f"{tz}00" off_hour = tz[0:2] off_minute = tz[2:4] @@ -602,14 +613,14 @@ def _get_parsed_value( parsed["tz"] = pendulum.timezone(offset) elif token == "z": # Full timezone - if value not in pendulum.timezones: + if value not in pendulum.timezones(): raise ValueError("Invalid date") parsed["tz"] = pendulum.timezone(value) def _get_parsed_locale_value( - self, token, value, parsed, locale - ): # type: (str, str, typing.Dict[str, typing.Any], Locale) -> None + self, token: str, value: str, parsed: dict[str, Any], locale: Locale + ) -> None: if token == "MMMM": unit = "month" match = "months.wide" @@ -617,7 +628,7 @@ def _get_parsed_locale_value( unit = "month" match = "months.abbreviated" elif token == "Do": - parsed["day"] = int(re.match(r"(\d+)", value).group(1)) + parsed["day"] = int(cast("Match[str]", re.match(r"(\d+)", value)).group(1)) return elif token == "dddd": @@ -637,7 +648,7 @@ def _get_parsed_locale_value( if token == "a": value = value.lower() - valid_values = list(map(lambda x: x.lower(), valid_values)) + valid_values = [x.lower() for x in valid_values] if value not in valid_values: raise ValueError("Invalid date") @@ -646,13 +657,13 @@ def _get_parsed_locale_value( return else: - raise ValueError('Invalid token "{}"'.format(token)) + raise ValueError(f'Invalid token "{token}"') parsed[unit] = locale.match_translation(match, value) if value is None: raise ValueError("Invalid date") - def _replace_tokens(self, token, locale): # type: (str, Locale) -> str + def _replace_tokens(self, token: str, locale: Locale) -> str: if token.startswith("[") and token.endswith("]"): return token[1:-1] elif token.startswith("\\"): @@ -661,7 +672,7 @@ def _replace_tokens(self, token, locale): # type: (str, Locale) -> str return token elif token not in self._REGEX_TOKENS and token not in self._LOCALIZABLE_TOKENS: - raise ValueError("Unsupported token: {}".format(token)) + raise ValueError(f"Unsupported token: {token}") if token in self._LOCALIZABLE_TOKENS: values = self._LOCALIZABLE_TOKENS[token] @@ -669,17 +680,19 @@ def _replace_tokens(self, token, locale): # type: (str, Locale) -> str candidates = values(locale) else: candidates = tuple( - locale.translation(self._LOCALIZABLE_TOKENS[token]).values() + locale.translation( + cast("str", self._LOCALIZABLE_TOKENS[token]) + ).values() ) else: - candidates = self._REGEX_TOKENS[token] + candidates = cast("Sequence[str]", self._REGEX_TOKENS[token]) if not candidates: - raise ValueError("Unsupported token: {}".format(token)) + raise ValueError(f"Unsupported token: {token}") if not isinstance(candidates, tuple): - candidates = (candidates,) + candidates = (cast("str", candidates),) - pattern = "(?P<{}>{})".format(token, "|".join([decode(p) for p in candidates])) + pattern = f"(?P<{token}>{'|'.join(candidates)})" return pattern diff --git a/src/pendulum/helpers.py b/src/pendulum/helpers.py new file mode 100644 index 00000000..db42b31e --- /dev/null +++ b/src/pendulum/helpers.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +import os +import struct + +from datetime import date +from datetime import datetime +from datetime import timedelta +from functools import cache +from math import copysign +from typing import TYPE_CHECKING +from typing import Any +from typing import TypeVar +from typing import overload + +import pendulum + +from pendulum.constants import DAYS_PER_MONTHS +from pendulum.day import WeekDay +from pendulum.locales.locale import Locale + + +if TYPE_CHECKING: + # Prevent import cycles + from pendulum.duration import Duration + + # LazyLoaded + from pendulum.formatting.difference_formatter import DifferenceFormatter + +with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" + +_DT = TypeVar("_DT", bound=datetime) +_D = TypeVar("_D", bound=date) + +try: + if not with_extensions or struct.calcsize("P") == 4: + raise ImportError() + + from pendulum._pendulum import PreciseDiff + from pendulum._pendulum import days_in_year + from pendulum._pendulum import is_leap + from pendulum._pendulum import is_long_year + from pendulum._pendulum import local_time + from pendulum._pendulum import precise_diff + from pendulum._pendulum import week_day +except ImportError: + from pendulum._helpers import PreciseDiff # type: ignore[assignment] + from pendulum._helpers import days_in_year + from pendulum._helpers import is_leap + from pendulum._helpers import is_long_year + from pendulum._helpers import local_time + from pendulum._helpers import precise_diff # type: ignore[assignment] + from pendulum._helpers import week_day + +difference_formatter: DifferenceFormatter + + +@overload +def add_duration( + dt: _DT, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: float = 0, + microseconds: int = 0, +) -> _DT: ... + + +@overload +def add_duration( + dt: _D, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, +) -> _D: + pass + + +def add_duration( + dt: date | datetime, + years: int = 0, + months: int = 0, + weeks: int = 0, + days: int = 0, + hours: int = 0, + minutes: int = 0, + seconds: float = 0, + microseconds: int = 0, +) -> date | datetime: + """ + Adds a duration to a date/datetime instance. + """ + days += weeks * 7 + + if ( + isinstance(dt, date) + and not isinstance(dt, datetime) + and any([hours, minutes, seconds, microseconds]) + ): + raise RuntimeError("Time elements cannot be added to a date instance.") + + # Normalizing + if abs(microseconds) > 999999: + s = _sign(microseconds) + div, mod = divmod(microseconds * s, 1000000) + microseconds = mod * s + seconds += div * s + + if abs(seconds) > 59: + s = _sign(seconds) + div, mod = divmod(seconds * s, 60) # type: ignore[assignment] + seconds = mod * s + minutes += div * s + + if abs(minutes) > 59: + s = _sign(minutes) + div, mod = divmod(minutes * s, 60) + minutes = mod * s + hours += div * s + + if abs(hours) > 23: + s = _sign(hours) + div, mod = divmod(hours * s, 24) + hours = mod * s + days += div * s + + if abs(months) > 11: + s = _sign(months) + div, mod = divmod(months * s, 12) + months = mod * s + years += div * s + + year = dt.year + years + month = dt.month + + if months: + month += months + if month > 12: + year += 1 + month -= 12 + elif month < 1: + year -= 1 + month += 12 + + day = min(DAYS_PER_MONTHS[int(is_leap(year))][month], dt.day) + + dt = dt.replace(year=year, month=month, day=day) + + return dt + timedelta( + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + microseconds=microseconds, + ) + + +def format_diff( + diff: Duration, + is_now: bool = True, + absolute: bool = False, + locale: str | None = None, +) -> str: + if locale is None: + locale = get_locale() + + return _difference_formatter().format(diff, is_now, absolute, locale) + + +def _sign(x: float) -> int: + return int(copysign(1, x)) + + +# Global helpers + + +def locale(name: str) -> Locale: + return Locale.load(name) + + +def set_locale(name: str) -> None: + locale(name) + + pendulum._LOCALE = name + + +def get_locale() -> str: + return pendulum._LOCALE + + +def week_starts_at(wday: WeekDay) -> None: + if wday < WeekDay.MONDAY or wday > WeekDay.SUNDAY: + raise ValueError("Invalid day of week") + + pendulum._WEEK_STARTS_AT = wday + + +def week_ends_at(wday: WeekDay) -> None: + if wday < WeekDay.MONDAY or wday > WeekDay.SUNDAY: + raise ValueError("Invalid day of week") + + pendulum._WEEK_ENDS_AT = wday + + +@cache +def _difference_formatter() -> DifferenceFormatter: + from pendulum.formatting.difference_formatter import DifferenceFormatter + + return DifferenceFormatter() + + +def __getattr__(name: str) -> Any: + if name == "difference_formatter": + return _difference_formatter() + raise AttributeError(name) + + +__all__ = [ + "PreciseDiff", + "add_duration", + "days_in_year", + "format_diff", + "get_locale", + "is_leap", + "is_long_year", + "local_time", + "locale", + "precise_diff", + "set_locale", + "week_day", + "week_ends_at", + "week_starts_at", +] diff --git a/src/pendulum/interval.py b/src/pendulum/interval.py new file mode 100644 index 00000000..c5e0713a --- /dev/null +++ b/src/pendulum/interval.py @@ -0,0 +1,420 @@ +from __future__ import annotations + +import copy +import operator + +from datetime import date +from datetime import datetime +from datetime import timedelta +from typing import TYPE_CHECKING +from typing import Any +from typing import Generic +from typing import TypeVar +from typing import cast +from typing import overload + +import pendulum + +from pendulum.constants import MONTHS_PER_YEAR +from pendulum.duration import Duration +from pendulum.helpers import precise_diff + + +if TYPE_CHECKING: + from collections.abc import Iterator + + from typing_extensions import Self + from typing_extensions import SupportsIndex + + from pendulum.helpers import PreciseDiff + from pendulum.locales.locale import Locale + + +_T = TypeVar("_T", bound=date) + + +class Interval(Duration, Generic[_T]): + """ + An interval of time between two datetimes. + """ + + def __new__(cls, start: _T, end: _T, absolute: bool = False) -> Self: + if (isinstance(start, datetime) and not isinstance(end, datetime)) or ( + not isinstance(start, datetime) and isinstance(end, datetime) + ): + raise ValueError( + "Both start and end of an Interval must have the same type" + ) + + if ( + isinstance(start, datetime) + and isinstance(end, datetime) + and ( + (start.tzinfo is None and end.tzinfo is not None) + or (start.tzinfo is not None and end.tzinfo is None) + ) + ): + raise TypeError("can't compare offset-naive and offset-aware datetimes") + + if absolute and start > end: + end, start = start, end + + _start = start + _end = end + if isinstance(start, pendulum.DateTime): + _start = cast( + "_T", + datetime( + start.year, + start.month, + start.day, + start.hour, + start.minute, + start.second, + start.microsecond, + tzinfo=start.tzinfo, + fold=start.fold, + ), + ) + elif isinstance(start, pendulum.Date): + _start = cast("_T", date(start.year, start.month, start.day)) + + if isinstance(end, pendulum.DateTime): + _end = cast( + "_T", + datetime( + end.year, + end.month, + end.day, + end.hour, + end.minute, + end.second, + end.microsecond, + tzinfo=end.tzinfo, + fold=end.fold, + ), + ) + elif isinstance(end, pendulum.Date): + _end = cast("_T", date(end.year, end.month, end.day)) + + # Fixing issues with datetime.__sub__() + # not handling offsets if the tzinfo is the same + if ( + isinstance(_start, datetime) + and isinstance(_end, datetime) + and _start.tzinfo is _end.tzinfo + ): + if _start.tzinfo is not None: + offset = cast("timedelta", cast("datetime", start).utcoffset()) + _start = cast("_T", (_start - offset).replace(tzinfo=None)) + + if isinstance(end, datetime) and _end.tzinfo is not None: + offset = cast("timedelta", end.utcoffset()) + _end = cast("_T", (_end - offset).replace(tzinfo=None)) + + delta: timedelta = _end - _start + + return super().__new__(cls, seconds=delta.total_seconds()) + + def __init__(self, start: _T, end: _T, absolute: bool = False) -> None: + super().__init__() + + _start: _T + if not isinstance(start, pendulum.Date): + if isinstance(start, datetime): + start = cast("_T", pendulum.instance(start)) + else: + start = cast("_T", pendulum.date(start.year, start.month, start.day)) + + _start = start + else: + if isinstance(start, pendulum.DateTime): + _start = cast( + "_T", + datetime( + start.year, + start.month, + start.day, + start.hour, + start.minute, + start.second, + start.microsecond, + tzinfo=start.tzinfo, + ), + ) + else: + _start = cast("_T", date(start.year, start.month, start.day)) + + _end: _T + if not isinstance(end, pendulum.Date): + if isinstance(end, datetime): + end = cast("_T", pendulum.instance(end)) + else: + end = cast("_T", pendulum.date(end.year, end.month, end.day)) + + _end = end + else: + if isinstance(end, pendulum.DateTime): + _end = cast( + "_T", + datetime( + end.year, + end.month, + end.day, + end.hour, + end.minute, + end.second, + end.microsecond, + tzinfo=end.tzinfo, + ), + ) + else: + _end = cast("_T", date(end.year, end.month, end.day)) + + self._invert = False + if start > end: + self._invert = True + + if absolute: + end, start = start, end + _end, _start = _start, _end + + self._absolute = absolute + self._start: _T = start + self._end: _T = end + self._delta: PreciseDiff = precise_diff(_start, _end) + + @property + def years(self) -> int: + return self._delta.years + + @property + def months(self) -> int: + return self._delta.months + + @property + def weeks(self) -> int: + return abs(self._delta.days) // 7 * self._sign(self._delta.days) + + @property + def days(self) -> int: + return self._days + + @property + def remaining_days(self) -> int: + return abs(self._delta.days) % 7 * self._sign(self._days) + + @property + def hours(self) -> int: + return self._delta.hours + + @property + def minutes(self) -> int: + return self._delta.minutes + + @property + def start(self) -> _T: + return self._start + + @property + def end(self) -> _T: + return self._end + + def in_years(self) -> int: + """ + Gives the duration of the Interval in full years. + """ + return self.years + + def in_months(self) -> int: + """ + Gives the duration of the Interval in full months. + """ + return self.years * MONTHS_PER_YEAR + self.months + + def in_weeks(self) -> int: + days = self.in_days() + sign = 1 + + if days < 0: + sign = -1 + + return sign * (abs(days) // 7) + + def in_days(self) -> int: + return self._delta.total_days + + def in_words(self, locale: str | None = None, separator: str = " ") -> str: + """ + Get the current interval in words in the current locale. + + Ex: 6 jours 23 heures 58 minutes + + :param locale: The locale to use. Defaults to current locale. + :param separator: The separator to use between each unit + """ + from pendulum.locales.locale import Locale + + intervals = [ + ("year", self.years), + ("month", self.months), + ("week", self.weeks), + ("day", self.remaining_days), + ("hour", self.hours), + ("minute", self.minutes), + ("second", self.remaining_seconds), + ] + loaded_locale: Locale = Locale.load(locale or pendulum.get_locale()) + parts = [] + for interval in intervals: + unit, interval_count = interval + if abs(interval_count) > 0: + translation = loaded_locale.translation( + f"units.{unit}.{loaded_locale.plural(abs(interval_count))}" + ) + parts.append(translation.format(interval_count)) + + if not parts: + count: str | int = 0 + if abs(self.microseconds) > 0: + unit = f"units.second.{loaded_locale.plural(1)}" + count = f"{abs(self.microseconds) / 1e6:.2f}" + else: + unit = f"units.microsecond.{loaded_locale.plural(0)}" + + translation = loaded_locale.translation(unit) + parts.append(translation.format(count)) + + return separator.join(parts) + + def range(self, unit: str, amount: int = 1) -> Iterator[_T]: + method = "add" + op = operator.le + if not self._absolute and self.invert: + method = "subtract" + op = operator.ge + + start, end = self.start, self.end + + i = amount + while op(start, end): + yield start + + start = getattr(self.start, method)(**{unit: i}) + + i += amount + + def as_duration(self) -> Duration: + """ + Return the Interval as a Duration. + """ + return Duration(seconds=self.total_seconds()) + + def __iter__(self) -> Iterator[_T]: + return self.range("days") + + def __contains__(self, item: _T) -> bool: + return self.start <= item <= self.end + + def __add__(self, other: timedelta) -> Duration: # type: ignore[override] + return self.as_duration().__add__(other) + + __radd__ = __add__ # type: ignore[assignment] + + def __sub__(self, other: timedelta) -> Duration: # type: ignore[override] + return self.as_duration().__sub__(other) + + def __neg__(self) -> Self: + return self.__class__(self.end, self.start, self._absolute) + + def __mul__(self, other: int | float) -> Duration: # type: ignore[override] + return self.as_duration().__mul__(other) + + __rmul__ = __mul__ # type: ignore[assignment] + + @overload # type: ignore[override] + def __floordiv__(self, other: timedelta) -> int: ... + + @overload + def __floordiv__(self, other: int) -> Duration: ... + + def __floordiv__(self, other: int | timedelta) -> int | Duration: + return self.as_duration().__floordiv__(other) + + __div__ = __floordiv__ # type: ignore[assignment] + + @overload # type: ignore[override] + def __truediv__(self, other: timedelta) -> float: ... + + @overload + def __truediv__(self, other: float) -> Duration: ... + + def __truediv__(self, other: float | timedelta) -> Duration | float: + return self.as_duration().__truediv__(other) + + def __mod__(self, other: timedelta) -> Duration: # type: ignore[override] + return self.as_duration().__mod__(other) + + def __divmod__(self, other: timedelta) -> tuple[int, Duration]: + return self.as_duration().__divmod__(other) + + def __abs__(self) -> Self: + return self.__class__(self.start, self.end, absolute=True) + + def __repr__(self) -> str: + return f" {self._end}]>" + + def __str__(self) -> str: + return self.__repr__() + + def _cmp(self, other: timedelta) -> int: + # Only needed for PyPy + assert isinstance(other, timedelta) + + if isinstance(other, Interval): + other = other.as_timedelta() + + td = self.as_timedelta() + + return 0 if td == other else 1 if td > other else -1 + + def _getstate(self, protocol: SupportsIndex = 3) -> tuple[_T, _T, bool]: + start, end = self.start, self.end + + if self._invert and self._absolute: + end, start = start, end + + return start, end, self._absolute + + def __reduce__( + self, + ) -> tuple[type[Self], tuple[_T, _T, bool]]: + return self.__reduce_ex__(2) + + def __reduce_ex__( + self, protocol: SupportsIndex + ) -> tuple[type[Self], tuple[_T, _T, bool]]: + return self.__class__, self._getstate(protocol) + + def __hash__(self) -> int: + return hash((self.start, self.end, self._absolute)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Interval): + return (self.start, self.end, self._absolute) == ( + other.start, + other.end, + other._absolute, + ) + else: + return self.as_duration() == other + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def __deepcopy__(self, memo: dict[int, Any]) -> Self: + return self.__class__( + copy.deepcopy(self.start, memo), + copy.deepcopy(self.end, memo), + self._absolute, + ) diff --git a/pendulum/_extensions/__init__.py b/src/pendulum/locales/__init__.py similarity index 100% rename from pendulum/_extensions/__init__.py rename to src/pendulum/locales/__init__.py diff --git a/pendulum/locales/__init__.py b/src/pendulum/locales/bg/__init__.py similarity index 100% rename from pendulum/locales/__init__.py rename to src/pendulum/locales/bg/__init__.py diff --git a/src/pendulum/locales/bg/custom.py b/src/pendulum/locales/bg/custom.py new file mode 100644 index 00000000..38dc70f1 --- /dev/null +++ b/src/pendulum/locales/bg/custom.py @@ -0,0 +1,22 @@ +""" +bg custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "ago": "преди {}", + "from_now": "след {}", + "after": "след {0}", + "before": "преди {0}", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "L": "DD.MM.YYYY", + "LL": "D MMMM YYYY г.", + "LLL": "D MMMM YYYY г., HH:mm", + "LLLL": "dddd, D MMMM YYYY г., HH:mm", + }, +} diff --git a/src/pendulum/locales/bg/locale.py b/src/pendulum/locales/bg/locale.py new file mode 100644 index 00000000..856f2f3b --- /dev/null +++ b/src/pendulum/locales/bg/locale.py @@ -0,0 +1,221 @@ +from .custom import translations as custom_translations + + +""" +bg locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + 'plural': lambda n: 'one' if (n == n and ((n == 1))) else 'other', + 'ordinal': lambda n: 'other', + 'translations': { + 'days': { + 'abbreviated': { + 0: 'пн', + 1: 'вт', + 2: 'ср', + 3: 'чт', + 4: 'пт', + 5: 'сб', + 6: 'нд', + }, + 'narrow': { + 0: 'п', + 1: 'в', + 2: 'с', + 3: 'ч', + 4: 'п', + 5: 'с', + 6: 'н', + }, + 'short': { + 0: 'пн', + 1: 'вт', + 2: 'ср', + 3: 'чт', + 4: 'пт', + 5: 'сб', + 6: 'нд', + }, + 'wide': { + 0: 'понеделник', + 1: 'вторник', + 2: 'сряда', + 3: 'четвъртък', + 4: 'петък', + 5: 'събота', + 6: 'неделя', + }, + }, + 'months': { + 'abbreviated': { + 1: 'яну', + 2: 'фев', + 3: 'март', + 4: 'апр', + 5: 'май', + 6: 'юни', + 7: 'юли', + 8: 'авг', + 9: 'сеп', + 10: 'окт', + 11: 'ное', + 12: 'дек', + }, + 'narrow': { + 1: 'я', + 2: 'ф', + 3: 'м', + 4: 'а', + 5: 'м', + 6: 'ю', + 7: 'ю', + 8: 'а', + 9: 'с', + 10: 'о', + 11: 'н', + 12: 'д', + }, + 'wide': { + 1: 'януари', + 2: 'февруари', + 3: 'март', + 4: 'април', + 5: 'май', + 6: 'юни', + 7: 'юли', + 8: 'август', + 9: 'септември', + 10: 'октомври', + 11: 'ноември', + 12: 'декември', + }, + }, + 'units': { + 'year': { + 'one': '{0} година', + 'other': '{0} години', + }, + 'month': { + 'one': '{0} месец', + 'other': '{0} месеца', + }, + 'week': { + 'one': '{0} седмица', + 'other': '{0} седмици', + }, + 'day': { + 'one': '{0} ден', + 'other': '{0} дни', + }, + 'hour': { + 'one': '{0} час', + 'other': '{0} часа', + }, + 'minute': { + 'one': '{0} минута', + 'other': '{0} минути', + }, + 'second': { + 'one': '{0} секунда', + 'other': '{0} секунди', + }, + 'microsecond': { + 'one': '{0} микросекунда', + 'other': '{0} микросекунди', + }, + }, + 'relative': { + 'year': { + 'future': { + 'other': 'след {0} години', + 'one': 'след {0} година', + }, + 'past': { + 'other': 'преди {0} години', + 'one': 'преди {0} година', + }, + }, + 'month': { + 'future': { + 'other': 'след {0} месеца', + 'one': 'след {0} месец', + }, + 'past': { + 'other': 'преди {0} месеца', + 'one': 'преди {0} месец', + }, + }, + 'week': { + 'future': { + 'other': 'след {0} седмици', + 'one': 'след {0} седмица', + }, + 'past': { + 'other': 'преди {0} седмици', + 'one': 'преди {0} седмица', + }, + }, + 'day': { + 'future': { + 'other': 'след {0} дни', + 'one': 'след {0} ден', + }, + 'past': { + 'other': 'преди {0} дни', + 'one': 'преди {0} ден', + }, + }, + 'hour': { + 'future': { + 'other': 'след {0} часа', + 'one': 'след {0} час', + }, + 'past': { + 'other': 'преди {0} часа', + 'one': 'преди {0} час', + }, + }, + 'minute': { + 'future': { + 'other': 'след {0} минути', + 'one': 'след {0} минута', + }, + 'past': { + 'other': 'преди {0} минути', + 'one': 'преди {0} минута', + }, + }, + 'second': { + 'future': { + 'other': 'след {0} секунди', + 'one': 'след {0} секунда', + }, + 'past': { + 'other': 'преди {0} секунди', + 'one': 'преди {0} секунда', + }, + }, + }, + 'day_periods': { + 'midnight': 'полунощ', + 'am': 'пр.об.', + 'pm': 'сл.об.', + 'morning1': 'сутринта', + 'morning2': 'на обяд', + 'afternoon1': 'следобед', + 'evening1': 'вечерта', + 'night1': 'през нощта', + }, + 'week_data': { + 'min_days': 1, + 'first_day': 0, + 'weekend_start': 5, + 'weekend_end': 6, + }, + }, + 'custom': custom_translations +} diff --git a/pendulum/locales/da/__init__.py b/src/pendulum/locales/cs/__init__.py similarity index 100% rename from pendulum/locales/da/__init__.py rename to src/pendulum/locales/cs/__init__.py diff --git a/src/pendulum/locales/cs/custom.py b/src/pendulum/locales/cs/custom.py new file mode 100644 index 00000000..c909f324 --- /dev/null +++ b/src/pendulum/locales/cs/custom.py @@ -0,0 +1,25 @@ +""" +cs custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "pár vteřin"}, + # Relative time + "ago": "{} zpět", + "from_now": "za {}", + "after": "{0} po", + "before": "{0} zpět", + # Ordinals + "ordinal": {"one": ".", "two": ".", "few": ".", "other": "."}, + # Date formats + "date_formats": { + "LTS": "h:mm:ss", + "LT": "h:mm", + "L": "DD. M. YYYY", + "LL": "D. MMMM, YYYY", + "LLL": "D. MMMM, YYYY h:mm", + "LLLL": "dddd, D. MMMM, YYYY h:mm", + }, +} diff --git a/src/pendulum/locales/cs/locale.py b/src/pendulum/locales/cs/locale.py new file mode 100644 index 00000000..b44d0f76 --- /dev/null +++ b/src/pendulum/locales/cs/locale.py @@ -0,0 +1,274 @@ +from __future__ import annotations + +from pendulum.locales.cs.custom import translations as custom_translations + + +""" +cs locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ((n == n and (n >= 2 and n <= 4)) and (0 == 0 and (0 == 0))) + else "many" + if (not (0 == 0 and (0 == 0))) + else "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "po", + 1: "út", + 2: "st", + 3: "čt", + 4: "pá", + 5: "so", + 6: "ne", + }, + "narrow": { + 0: "P", + 1: "Ú", + 2: "S", + 3: "Č", + 4: "P", + 5: "S", + 6: "N", + }, + "short": { + 0: "po", + 1: "út", + 2: "st", + 3: "čt", + 4: "pá", + 5: "so", + 6: "ne", + }, + "wide": { + 0: "pondělí", + 1: "úterý", + 2: "středa", + 3: "čtvrtek", + 4: "pátek", + 5: "sobota", + 6: "neděle", + }, + }, + "months": { + "abbreviated": { + 1: "led", + 2: "úno", + 3: "bře", + 4: "dub", + 5: "kvě", + 6: "čvn", + 7: "čvc", + 8: "srp", + 9: "zář", + 10: "říj", + 11: "lis", + 12: "pro", + }, + "narrow": { + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "10", + 11: "11", + 12: "12", + }, + "wide": { + 1: "ledna", + 2: "února", + 3: "března", + 4: "dubna", + 5: "května", + 6: "června", + 7: "července", + 8: "srpna", + 9: "září", + 10: "října", + 11: "listopadu", + 12: "prosince", + }, + }, + "units": { + "year": { + "one": "{0} rok", + "few": "{0} roky", + "many": "{0} roku", + "other": "{0} let", + }, + "month": { + "one": "{0} měsíc", + "few": "{0} měsíce", + "many": "{0} měsíce", + "other": "{0} měsíců", + }, + "week": { + "one": "{0} týden", + "few": "{0} týdny", + "many": "{0} týdne", + "other": "{0} týdnů", + }, + "day": { + "one": "{0} den", + "few": "{0} dny", + "many": "{0} dne", + "other": "{0} dní", + }, + "hour": { + "one": "{0} hodina", + "few": "{0} hodiny", + "many": "{0} hodiny", + "other": "{0} hodin", + }, + "minute": { + "one": "{0} minuta", + "few": "{0} minuty", + "many": "{0} minuty", + "other": "{0} minut", + }, + "second": { + "one": "{0} sekunda", + "few": "{0} sekundy", + "many": "{0} sekundy", + "other": "{0} sekund", + }, + "microsecond": { + "one": "{0} mikrosekunda", + "few": "{0} mikrosekundy", + "many": "{0} mikrosekundy", + "other": "{0} mikrosekund", + }, + }, + "relative": { + "year": { + "future": { + "other": "za {0} let", + "one": "za {0} rok", + "few": "za {0} roky", + "many": "za {0} roku", + }, + "past": { + "other": "před {0} lety", + "one": "před {0} rokem", + "few": "před {0} lety", + "many": "před {0} roku", + }, + }, + "month": { + "future": { + "other": "za {0} měsíců", + "one": "za {0} měsíc", + "few": "za {0} měsíce", + "many": "za {0} měsíce", + }, + "past": { + "other": "před {0} měsíci", + "one": "před {0} měsícem", + "few": "před {0} měsíci", + "many": "před {0} měsíce", + }, + }, + "week": { + "future": { + "other": "za {0} týdnů", + "one": "za {0} týden", + "few": "za {0} týdny", + "many": "za {0} týdne", + }, + "past": { + "other": "před {0} týdny", + "one": "před {0} týdnem", + "few": "před {0} týdny", + "many": "před {0} týdne", + }, + }, + "day": { + "future": { + "other": "za {0} dní", + "one": "za {0} den", + "few": "za {0} dny", + "many": "za {0} dne", + }, + "past": { + "other": "před {0} dny", + "one": "před {0} dnem", + "few": "před {0} dny", + "many": "před {0} dne", + }, + }, + "hour": { + "future": { + "other": "za {0} hodin", + "one": "za {0} hodinu", + "few": "za {0} hodiny", + "many": "za {0} hodiny", + }, + "past": { + "other": "před {0} hodinami", + "one": "před {0} hodinou", + "few": "před {0} hodinami", + "many": "před {0} hodiny", + }, + }, + "minute": { + "future": { + "other": "za {0} minut", + "one": "za {0} minutu", + "few": "za {0} minuty", + "many": "za {0} minuty", + }, + "past": { + "other": "před {0} minutami", + "one": "před {0} minutou", + "few": "před {0} minutami", + "many": "před {0} minuty", + }, + }, + "second": { + "future": { + "other": "za {0} sekund", + "one": "za {0} sekundu", + "few": "za {0} sekundy", + "many": "za {0} sekundy", + }, + "past": { + "other": "před {0} sekundami", + "one": "před {0} sekundou", + "few": "před {0} sekundami", + "many": "před {0} sekundy", + }, + }, + }, + "day_periods": { + "midnight": "půlnoc", + "am": "dop.", + "noon": "poledne", + "pm": "odp.", + "morning1": "ráno", + "morning2": "dopoledne", + "afternoon1": "odpoledne", + "evening1": "večer", + "night1": "v noci", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/de/__init__.py b/src/pendulum/locales/da/__init__.py similarity index 100% rename from pendulum/locales/de/__init__.py rename to src/pendulum/locales/da/__init__.py diff --git a/pendulum/locales/da/custom.py b/src/pendulum/locales/da/custom.py similarity index 84% rename from pendulum/locales/da/custom.py rename to src/pendulum/locales/da/custom.py index eaf36552..57e68f4a 100644 --- a/pendulum/locales/da/custom.py +++ b/src/pendulum/locales/da/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ da custom locale file. """ +from __future__ import annotations + translations = { # Relative time diff --git a/pendulum/locales/da/locale.py b/src/pendulum/locales/da/locale.py similarity index 81% rename from pendulum/locales/da/locale.py rename to src/pendulum/locales/da/locale.py index addee68a..2385de53 100644 --- a/pendulum/locales/da/locale.py +++ b/src/pendulum/locales/da/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.da.custom import translations as custom_translations """ @@ -14,32 +13,32 @@ locale = { "plural": lambda n: "one" if ( - (n == n and ((n == 1))) - or ((not (0 == 0 and ((0 == 0)))) and (n == n and ((n == 0) or (n == 1)))) + (n == n and (n == 1)) + or ((not (0 == 0 and (0 == 0))) and (n == n and ((n == 0) or (n == 1)))) ) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "søn.", - 1: "man.", - 2: "tir.", - 3: "ons.", - 4: "tor.", - 5: "fre.", - 6: "lør.", + 0: "man.", + 1: "tir.", + 2: "ons.", + 3: "tor.", + 4: "fre.", + 5: "lør.", + 6: "søn.", }, - "narrow": {0: "S", 1: "M", 2: "T", 3: "O", 4: "T", 5: "F", 6: "L"}, - "short": {0: "sø", 1: "ma", 2: "ti", 3: "on", 4: "to", 5: "fr", 6: "lø"}, + "narrow": {0: "M", 1: "T", 2: "O", 3: "T", 4: "F", 5: "L", 6: "S"}, + "short": {0: "ma", 1: "ti", 2: "on", 3: "to", 4: "fr", 5: "lø", 6: "sø"}, "wide": { - 0: "søndag", - 1: "mandag", - 2: "tirsdag", - 3: "onsdag", - 4: "torsdag", - 5: "fredag", - 6: "lørdag", + 0: "mandag", + 1: "tirsdag", + 2: "onsdag", + 3: "torsdag", + 4: "fredag", + 5: "lørdag", + 6: "søndag", }, }, "months": { @@ -145,6 +144,12 @@ "evening1": "om aftenen", "night1": "om natten", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/locales/en/__init__.py b/src/pendulum/locales/de/__init__.py similarity index 100% rename from pendulum/locales/en/__init__.py rename to src/pendulum/locales/de/__init__.py diff --git a/pendulum/locales/de/custom.py b/src/pendulum/locales/de/custom.py similarity index 94% rename from pendulum/locales/de/custom.py rename to src/pendulum/locales/de/custom.py index 45fd591c..8ef06cc4 100644 --- a/pendulum/locales/de/custom.py +++ b/src/pendulum/locales/de/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ de custom locale file. """ +from __future__ import annotations + translations = { # Relative time diff --git a/pendulum/locales/de/locale.py b/src/pendulum/locales/de/locale.py similarity index 79% rename from pendulum/locales/de/locale.py rename to src/pendulum/locales/de/locale.py index 9a9ec9f3..7781c3f9 100644 --- a/pendulum/locales/de/locale.py +++ b/src/pendulum/locales/de/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.de.custom import translations as custom_translations """ @@ -13,38 +12,38 @@ locale = { "plural": lambda n: "one" - if ((n == n and ((n == 1))) and (0 == 0 and ((0 == 0)))) + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "So.", - 1: "Mo.", - 2: "Di.", - 3: "Mi.", - 4: "Do.", - 5: "Fr.", - 6: "Sa.", + 0: "Mo.", + 1: "Di.", + 2: "Mi.", + 3: "Do.", + 4: "Fr.", + 5: "Sa.", + 6: "So.", }, - "narrow": {0: "S", 1: "M", 2: "D", 3: "M", 4: "D", 5: "F", 6: "S"}, + "narrow": {0: "M", 1: "D", 2: "M", 3: "D", 4: "F", 5: "S", 6: "S"}, "short": { - 0: "So.", - 1: "Mo.", - 2: "Di.", - 3: "Mi.", - 4: "Do.", - 5: "Fr.", - 6: "Sa.", + 0: "Mo.", + 1: "Di.", + 2: "Mi.", + 3: "Do.", + 4: "Fr.", + 5: "Sa.", + 6: "So.", }, "wide": { - 0: "Sonntag", - 1: "Montag", - 2: "Dienstag", - 3: "Mittwoch", - 4: "Donnerstag", - 5: "Freitag", - 6: "Samstag", + 0: "Montag", + 1: "Dienstag", + 2: "Mittwoch", + 3: "Donnerstag", + 4: "Freitag", + 5: "Samstag", + 6: "Sonntag", }, }, "months": { @@ -142,6 +141,12 @@ "evening1": "abends", "night1": "nachts", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/locales/es/__init__.py b/src/pendulum/locales/en/__init__.py similarity index 100% rename from pendulum/locales/es/__init__.py rename to src/pendulum/locales/en/__init__.py diff --git a/pendulum/locales/en/custom.py b/src/pendulum/locales/en/custom.py similarity index 89% rename from pendulum/locales/en/custom.py rename to src/pendulum/locales/en/custom.py index 9e631a2b..65cf467c 100644 --- a/pendulum/locales/en/custom.py +++ b/src/pendulum/locales/en/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ en custom locale file. """ +from __future__ import annotations + translations = { "units": {"few_second": "a few seconds"}, diff --git a/pendulum/locales/en/locale.py b/src/pendulum/locales/en/locale.py similarity index 75% rename from pendulum/locales/en/locale.py rename to src/pendulum/locales/en/locale.py index 917a4ce2..4f05c2f3 100644 --- a/pendulum/locales/en/locale.py +++ b/src/pendulum/locales/en/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.en.custom import translations as custom_translations """ @@ -13,45 +12,45 @@ locale = { "plural": lambda n: "one" - if ((n == n and ((n == 1))) and (0 == 0 and ((0 == 0)))) + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) else "other", "ordinal": lambda n: "few" if ( - ((n % 10) == (n % 10) and (((n % 10) == 3))) - and (not ((n % 100) == (n % 100) and (((n % 100) == 13)))) + ((n % 10) == (n % 10) and ((n % 10) == 3)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 13))) ) else "one" if ( - ((n % 10) == (n % 10) and (((n % 10) == 1))) - and (not ((n % 100) == (n % 100) and (((n % 100) == 11)))) + ((n % 10) == (n % 10) and ((n % 10) == 1)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 11))) ) else "two" if ( - ((n % 10) == (n % 10) and (((n % 10) == 2))) - and (not ((n % 100) == (n % 100) and (((n % 100) == 12)))) + ((n % 10) == (n % 10) and ((n % 10) == 2)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 12))) ) else "other", "translations": { "days": { "abbreviated": { - 0: "Sun", - 1: "Mon", - 2: "Tue", - 3: "Wed", - 4: "Thu", - 5: "Fri", - 6: "Sat", + 0: "Mon", + 1: "Tue", + 2: "Wed", + 3: "Thu", + 4: "Fri", + 5: "Sat", + 6: "Sun", }, - "narrow": {0: "S", 1: "M", 2: "T", 3: "W", 4: "T", 5: "F", 6: "S"}, - "short": {0: "Su", 1: "Mo", 2: "Tu", 3: "We", 4: "Th", 5: "Fr", 6: "Sa"}, + "narrow": {0: "M", 1: "T", 2: "W", 3: "T", 4: "F", 5: "S", 6: "S"}, + "short": {0: "Mo", 1: "Tu", 2: "We", 3: "Th", 4: "Fr", 5: "Sa", 6: "Su"}, "wide": { - 0: "Sunday", - 1: "Monday", - 2: "Tuesday", - 3: "Wednesday", - 4: "Thursday", - 5: "Friday", - 6: "Saturday", + 0: "Monday", + 1: "Tuesday", + 2: "Wednesday", + 3: "Thursday", + 4: "Friday", + 5: "Saturday", + 6: "Sunday", }, }, "months": { @@ -148,6 +147,12 @@ "evening1": "in the evening", "night1": "at night", }, + "week_data": { + "min_days": 1, + "first_day": 6, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/locales/fa/__init__.py b/src/pendulum/locales/en_gb/__init__.py similarity index 100% rename from pendulum/locales/fa/__init__.py rename to src/pendulum/locales/en_gb/__init__.py diff --git a/src/pendulum/locales/en_gb/custom.py b/src/pendulum/locales/en_gb/custom.py new file mode 100644 index 00000000..2c77a698 --- /dev/null +++ b/src/pendulum/locales/en_gb/custom.py @@ -0,0 +1,25 @@ +""" +en-gb custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "a few seconds"}, + # Relative time + "ago": "{} ago", + "from_now": "in {}", + "after": "{0} after", + "before": "{0} before", + # Ordinals + "ordinal": {"one": "st", "two": "nd", "few": "rd", "other": "th"}, + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "L": "DD/MM/YYYY", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY HH:mm", + "LLLL": "dddd, D MMMM YYYY HH:mm", + }, +} diff --git a/src/pendulum/locales/en_gb/locale.py b/src/pendulum/locales/en_gb/locale.py new file mode 100644 index 00000000..c25f7d5c --- /dev/null +++ b/src/pendulum/locales/en_gb/locale.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +from pendulum.locales.en_gb.custom import translations as custom_translations + + +""" +en-gb locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "few" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 3)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 13))) + ) + else "one" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 1)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 11))) + ) + else "two" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 2)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 12))) + ) + else "other", + "translations": { + "days": { + "abbreviated": { + 0: "Mon", + 1: "Tue", + 2: "Wed", + 3: "Thu", + 4: "Fri", + 5: "Sat", + 6: "Sun", + }, + "narrow": { + 0: "M", + 1: "T", + 2: "W", + 3: "T", + 4: "F", + 5: "S", + 6: "S", + }, + "short": { + 0: "Mo", + 1: "Tu", + 2: "We", + 3: "Th", + 4: "Fr", + 5: "Sa", + 6: "Su", + }, + "wide": { + 0: "Monday", + 1: "Tuesday", + 2: "Wednesday", + 3: "Thursday", + 4: "Friday", + 5: "Saturday", + 6: "Sunday", + }, + }, + "months": { + "abbreviated": { + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "May", + 6: "Jun", + 7: "Jul", + 8: "Aug", + 9: "Sept", + 10: "Oct", + 11: "Nov", + 12: "Dec", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "January", + 2: "February", + 3: "March", + 4: "April", + 5: "May", + 6: "June", + 7: "July", + 8: "August", + 9: "September", + 10: "October", + 11: "November", + 12: "December", + }, + }, + "units": { + "year": { + "one": "{0} year", + "other": "{0} years", + }, + "month": { + "one": "{0} month", + "other": "{0} months", + }, + "week": { + "one": "{0} week", + "other": "{0} weeks", + }, + "day": { + "one": "{0} day", + "other": "{0} days", + }, + "hour": { + "one": "{0} hour", + "other": "{0} hours", + }, + "minute": { + "one": "{0} minute", + "other": "{0} minutes", + }, + "second": { + "one": "{0} second", + "other": "{0} seconds", + }, + "microsecond": { + "one": "{0} microsecond", + "other": "{0} microseconds", + }, + }, + "relative": { + "year": { + "future": { + "other": "in {0} years", + "one": "in {0} year", + }, + "past": { + "other": "{0} years ago", + "one": "{0} year ago", + }, + }, + "month": { + "future": { + "other": "in {0} months", + "one": "in {0} month", + }, + "past": { + "other": "{0} months ago", + "one": "{0} month ago", + }, + }, + "week": { + "future": { + "other": "in {0} weeks", + "one": "in {0} week", + }, + "past": { + "other": "{0} weeks ago", + "one": "{0} week ago", + }, + }, + "day": { + "future": { + "other": "in {0} days", + "one": "in {0} day", + }, + "past": { + "other": "{0} days ago", + "one": "{0} day ago", + }, + }, + "hour": { + "future": { + "other": "in {0} hours", + "one": "in {0} hour", + }, + "past": { + "other": "{0} hours ago", + "one": "{0} hour ago", + }, + }, + "minute": { + "future": { + "other": "in {0} minutes", + "one": "in {0} minute", + }, + "past": { + "other": "{0} minutes ago", + "one": "{0} minute ago", + }, + }, + "second": { + "future": { + "other": "in {0} seconds", + "one": "in {0} second", + }, + "past": { + "other": "{0} seconds ago", + "one": "{0} second ago", + }, + }, + }, + "day_periods": { + "midnight": "midnight", + "am": "am", + "noon": "noon", + "pm": "pm", + "morning1": "in the morning", + "afternoon1": "in the afternoon", + "evening1": "in the evening", + "night1": "at night", + }, + "week_data": { + "min_days": 4, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/fo/__init__.py b/src/pendulum/locales/en_us/__init__.py similarity index 100% rename from pendulum/locales/fo/__init__.py rename to src/pendulum/locales/en_us/__init__.py diff --git a/src/pendulum/locales/en_us/custom.py b/src/pendulum/locales/en_us/custom.py new file mode 100644 index 00000000..72d2005c --- /dev/null +++ b/src/pendulum/locales/en_us/custom.py @@ -0,0 +1,25 @@ +""" +en-us custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "a few seconds"}, + # Relative time + "ago": "{} ago", + "from_now": "in {}", + "after": "{0} after", + "before": "{0} before", + # Ordinals + "ordinal": {"one": "st", "two": "nd", "few": "rd", "other": "th"}, + # Date formats + "date_formats": { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + }, +} diff --git a/src/pendulum/locales/en_us/locale.py b/src/pendulum/locales/en_us/locale.py new file mode 100644 index 00000000..7f40f3f6 --- /dev/null +++ b/src/pendulum/locales/en_us/locale.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +from pendulum.locales.en_us.custom import translations as custom_translations + + +""" +en-us locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "few" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 3)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 13))) + ) + else "one" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 1)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 11))) + ) + else "two" + if ( + ((n % 10) == (n % 10) and ((n % 10) == 2)) + and (not ((n % 100) == (n % 100) and ((n % 100) == 12))) + ) + else "other", + "translations": { + "days": { + "abbreviated": { + 0: "Mon", + 1: "Tue", + 2: "Wed", + 3: "Thu", + 4: "Fri", + 5: "Sat", + 6: "Sun", + }, + "narrow": { + 0: "M", + 1: "T", + 2: "W", + 3: "T", + 4: "F", + 5: "S", + 6: "S", + }, + "short": { + 0: "Mo", + 1: "Tu", + 2: "We", + 3: "Th", + 4: "Fr", + 5: "Sa", + 6: "Su", + }, + "wide": { + 0: "Monday", + 1: "Tuesday", + 2: "Wednesday", + 3: "Thursday", + 4: "Friday", + 5: "Saturday", + 6: "Sunday", + }, + }, + "months": { + "abbreviated": { + 1: "Jan", + 2: "Feb", + 3: "Mar", + 4: "Apr", + 5: "May", + 6: "Jun", + 7: "Jul", + 8: "Aug", + 9: "Sep", + 10: "Oct", + 11: "Nov", + 12: "Dec", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "January", + 2: "February", + 3: "March", + 4: "April", + 5: "May", + 6: "June", + 7: "July", + 8: "August", + 9: "September", + 10: "October", + 11: "November", + 12: "December", + }, + }, + "units": { + "year": { + "one": "{0} year", + "other": "{0} years", + }, + "month": { + "one": "{0} month", + "other": "{0} months", + }, + "week": { + "one": "{0} week", + "other": "{0} weeks", + }, + "day": { + "one": "{0} day", + "other": "{0} days", + }, + "hour": { + "one": "{0} hour", + "other": "{0} hours", + }, + "minute": { + "one": "{0} minute", + "other": "{0} minutes", + }, + "second": { + "one": "{0} second", + "other": "{0} seconds", + }, + "microsecond": { + "one": "{0} microsecond", + "other": "{0} microseconds", + }, + }, + "relative": { + "year": { + "future": { + "other": "in {0} years", + "one": "in {0} year", + }, + "past": { + "other": "{0} years ago", + "one": "{0} year ago", + }, + }, + "month": { + "future": { + "other": "in {0} months", + "one": "in {0} month", + }, + "past": { + "other": "{0} months ago", + "one": "{0} month ago", + }, + }, + "week": { + "future": { + "other": "in {0} weeks", + "one": "in {0} week", + }, + "past": { + "other": "{0} weeks ago", + "one": "{0} week ago", + }, + }, + "day": { + "future": { + "other": "in {0} days", + "one": "in {0} day", + }, + "past": { + "other": "{0} days ago", + "one": "{0} day ago", + }, + }, + "hour": { + "future": { + "other": "in {0} hours", + "one": "in {0} hour", + }, + "past": { + "other": "{0} hours ago", + "one": "{0} hour ago", + }, + }, + "minute": { + "future": { + "other": "in {0} minutes", + "one": "in {0} minute", + }, + "past": { + "other": "{0} minutes ago", + "one": "{0} minute ago", + }, + }, + "second": { + "future": { + "other": "in {0} seconds", + "one": "in {0} second", + }, + "past": { + "other": "{0} seconds ago", + "one": "{0} second ago", + }, + }, + }, + "day_periods": { + "midnight": "midnight", + "am": "AM", + "noon": "noon", + "pm": "PM", + "morning1": "in the morning", + "afternoon1": "in the afternoon", + "evening1": "in the evening", + "night1": "at night", + }, + "week_data": { + "min_days": 1, + "first_day": 6, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/id/__init__.py b/src/pendulum/locales/es/__init__.py similarity index 100% rename from pendulum/locales/id/__init__.py rename to src/pendulum/locales/es/__init__.py diff --git a/pendulum/locales/es/custom.py b/src/pendulum/locales/es/custom.py similarity index 89% rename from pendulum/locales/es/custom.py rename to src/pendulum/locales/es/custom.py index 18585e07..4acb411a 100644 --- a/pendulum/locales/es/custom.py +++ b/src/pendulum/locales/es/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ es custom locale file. """ +from __future__ import annotations + translations = { "units": {"few_second": "unos segundos"}, diff --git a/pendulum/locales/es/locale.py b/src/pendulum/locales/es/locale.py similarity index 81% rename from pendulum/locales/es/locale.py rename to src/pendulum/locales/es/locale.py index 2f6266b6..4ab2784e 100644 --- a/pendulum/locales/es/locale.py +++ b/src/pendulum/locales/es/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.es.custom import translations as custom_translations """ @@ -12,29 +11,29 @@ locale = { - "plural": lambda n: "one" if (n == n and ((n == 1))) else "other", + "plural": lambda n: "one" if (n == n and (n == 1)) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "dom.", - 1: "lun.", - 2: "mar.", - 3: "mié.", - 4: "jue.", - 5: "vie.", - 6: "sáb.", + 0: "lun.", + 1: "mar.", + 2: "mié.", + 3: "jue.", + 4: "vie.", + 5: "sáb.", + 6: "dom.", }, - "narrow": {0: "D", 1: "L", 2: "M", 3: "X", 4: "J", 5: "V", 6: "S"}, - "short": {0: "DO", 1: "LU", 2: "MA", 3: "MI", 4: "JU", 5: "VI", 6: "SA"}, + "narrow": {0: "L", 1: "M", 2: "X", 3: "J", 4: "V", 5: "S", 6: "D"}, + "short": {0: "LU", 1: "MA", 2: "MI", 3: "JU", 4: "VI", 5: "SA", 6: "DO"}, "wide": { - 0: "domingo", - 1: "lunes", - 2: "martes", - 3: "miércoles", - 4: "jueves", - 5: "viernes", - 6: "sábado", + 0: "lunes", + 1: "martes", + 2: "miércoles", + 3: "jueves", + 4: "viernes", + 5: "sábado", + 6: "domingo", }, }, "months": { @@ -139,6 +138,12 @@ "evening1": "de la tarde", "night1": "de la noche", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/locales/it/__init__.py b/src/pendulum/locales/fa/__init__.py similarity index 100% rename from pendulum/locales/it/__init__.py rename to src/pendulum/locales/fa/__init__.py diff --git a/pendulum/locales/fa/custom.py b/src/pendulum/locales/fa/custom.py similarity index 84% rename from pendulum/locales/fa/custom.py rename to src/pendulum/locales/fa/custom.py index 9cc84d3a..e4b4a60a 100644 --- a/pendulum/locales/fa/custom.py +++ b/src/pendulum/locales/fa/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ fa custom locale file. """ +from __future__ import annotations + translations = { # Relative time diff --git a/pendulum/locales/fa/locale.py b/src/pendulum/locales/fa/locale.py similarity index 80% rename from pendulum/locales/fa/locale.py rename to src/pendulum/locales/fa/locale.py index 4c5719d3..1c3f6c54 100644 --- a/pendulum/locales/fa/locale.py +++ b/src/pendulum/locales/fa/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.fa.custom import translations as custom_translations """ @@ -13,30 +12,30 @@ locale = { "plural": lambda n: "one" - if ((n == n and ((n == 0))) or (n == n and ((n == 1)))) + if ((n == n and (n == 0)) or (n == n and (n == 1))) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "یکشنبه", - 1: "دوشنبه", - 2: "سه\u200cشنبه", - 3: "چهارشنبه", - 4: "پنجشنبه", - 5: "جمعه", - 6: "شنبه", + 0: "دوشنبه", + 1: "سه\u200cشنبه", + 2: "چهارشنبه", + 3: "پنجشنبه", + 4: "جمعه", + 5: "شنبه", + 6: "یکشنبه", }, - "narrow": {0: "ی", 1: "د", 2: "س", 3: "چ", 4: "پ", 5: "ج", 6: "ش"}, - "short": {0: "۱ش", 1: "۲ش", 2: "۳ش", 3: "۴ش", 4: "۵ش", 5: "ج", 6: "ش"}, + "narrow": {0: "د", 1: "س", 2: "چ", 3: "پ", 4: "ج", 5: "ش", 6: "ی"}, + "short": {0: "۲ش", 1: "۳ش", 2: "۴ش", 3: "۵ش", 4: "ج", 5: "ش", 6: "۱ش"}, "wide": { - 0: "یکشنبه", - 1: "دوشنبه", - 2: "سه\u200cشنبه", - 3: "چهارشنبه", - 4: "پنجشنبه", - 5: "جمعه", - 6: "شنبه", + 0: "دوشنبه", + 1: "سه\u200cشنبه", + 2: "چهارشنبه", + 3: "پنجشنبه", + 4: "جمعه", + 5: "شنبه", + 6: "یکشنبه", }, }, "months": { @@ -133,6 +132,12 @@ "evening1": "عصر", "night1": "شب", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/locales/ko/__init__.py b/src/pendulum/locales/fo/__init__.py similarity index 100% rename from pendulum/locales/ko/__init__.py rename to src/pendulum/locales/fo/__init__.py diff --git a/pendulum/locales/fo/custom.py b/src/pendulum/locales/fo/custom.py similarity index 86% rename from pendulum/locales/fo/custom.py rename to src/pendulum/locales/fo/custom.py index 31f7f45e..3f0fd1c8 100644 --- a/pendulum/locales/fo/custom.py +++ b/src/pendulum/locales/fo/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ fo custom locale file. """ +from __future__ import annotations + translations = { # Relative time diff --git a/pendulum/locales/fo/locale.py b/src/pendulum/locales/fo/locale.py similarity index 77% rename from pendulum/locales/fo/locale.py rename to src/pendulum/locales/fo/locale.py index 8c87580c..28ec0c01 100644 --- a/pendulum/locales/fo/locale.py +++ b/src/pendulum/locales/fo/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.fo.custom import translations as custom_translations """ @@ -12,37 +11,37 @@ locale = { - "plural": lambda n: "one" if (n == n and ((n == 1))) else "other", + "plural": lambda n: "one" if (n == n and (n == 1)) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "sun.", - 1: "mán.", - 2: "týs.", - 3: "mik.", - 4: "hós.", - 5: "frí.", - 6: "ley.", + 0: "mán.", + 1: "týs.", + 2: "mik.", + 3: "hós.", + 4: "frí.", + 5: "ley.", + 6: "sun.", }, - "narrow": {0: "S", 1: "M", 2: "T", 3: "M", 4: "H", 5: "F", 6: "L"}, + "narrow": {0: "M", 1: "T", 2: "M", 3: "H", 4: "F", 5: "L", 6: "S"}, "short": { - 0: "su.", - 1: "má.", - 2: "tý.", - 3: "mi.", - 4: "hó.", - 5: "fr.", - 6: "le.", + 0: "má.", + 1: "tý.", + 2: "mi.", + 3: "hó.", + 4: "fr.", + 5: "le.", + 6: "su.", }, "wide": { - 0: "sunnudagur", - 1: "mánadagur", - 2: "týsdagur", - 3: "mikudagur", - 4: "hósdagur", - 5: "fríggjadagur", - 6: "leygardagur", + 0: "mánadagur", + 1: "týsdagur", + 2: "mikudagur", + 3: "hósdagur", + 4: "fríggjadagur", + 5: "leygardagur", + 6: "sunnudagur", }, }, "months": { @@ -130,6 +129,12 @@ }, }, "day_periods": {"am": "AM", "pm": "PM"}, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/locales/lt/__init__.py b/src/pendulum/locales/fr/__init__.py similarity index 100% rename from pendulum/locales/lt/__init__.py rename to src/pendulum/locales/fr/__init__.py diff --git a/pendulum/locales/fr/custom.py b/src/pendulum/locales/fr/custom.py similarity index 88% rename from pendulum/locales/fr/custom.py rename to src/pendulum/locales/fr/custom.py index 14d480f7..913656cb 100644 --- a/pendulum/locales/fr/custom.py +++ b/src/pendulum/locales/fr/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ fr custom locale file. """ +from __future__ import annotations + translations = { "units": {"few_second": "quelques secondes"}, diff --git a/pendulum/locales/fr/locale.py b/src/pendulum/locales/fr/locale.py similarity index 81% rename from pendulum/locales/fr/locale.py rename to src/pendulum/locales/fr/locale.py index c884ce9a..30ee8cd3 100644 --- a/pendulum/locales/fr/locale.py +++ b/src/pendulum/locales/fr/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.fr.custom import translations as custom_translations """ @@ -13,28 +12,28 @@ locale = { "plural": lambda n: "one" if (n == n and ((n == 0) or (n == 1))) else "other", - "ordinal": lambda n: "one" if (n == n and ((n == 1))) else "other", + "ordinal": lambda n: "one" if (n == n and (n == 1)) else "other", "translations": { "days": { "abbreviated": { - 0: "dim.", - 1: "lun.", - 2: "mar.", - 3: "mer.", - 4: "jeu.", - 5: "ven.", - 6: "sam.", + 0: "lun.", + 1: "mar.", + 2: "mer.", + 3: "jeu.", + 4: "ven.", + 5: "sam.", + 6: "dim.", }, - "narrow": {0: "D", 1: "L", 2: "M", 3: "M", 4: "J", 5: "V", 6: "S"}, - "short": {0: "di", 1: "lu", 2: "ma", 3: "me", 4: "je", 5: "ve", 6: "sa"}, + "narrow": {0: "L", 1: "M", 2: "M", 3: "J", 4: "V", 5: "S", 6: "D"}, + "short": {0: "lu", 1: "ma", 2: "me", 3: "je", 4: "ve", 5: "sa", 6: "di"}, "wide": { - 0: "dimanche", - 1: "lundi", - 2: "mardi", - 3: "mercredi", - 4: "jeudi", - 5: "vendredi", - 6: "samedi", + 0: "lundi", + 1: "mardi", + 2: "mercredi", + 3: "jeudi", + 4: "vendredi", + 5: "samedi", + 6: "dimanche", }, }, "months": { @@ -131,6 +130,12 @@ "evening1": "du soir", "night1": "de nuit", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/locales/nb/__init__.py b/src/pendulum/locales/he/__init__.py similarity index 100% rename from pendulum/locales/nb/__init__.py rename to src/pendulum/locales/he/__init__.py diff --git a/src/pendulum/locales/he/custom.py b/src/pendulum/locales/he/custom.py new file mode 100644 index 00000000..c8e1f70a --- /dev/null +++ b/src/pendulum/locales/he/custom.py @@ -0,0 +1,25 @@ +""" +he custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "כמה שניות"}, + # Relative time + "ago": "לפני {0}", + "from_now": "תוך {0}", + "after": "בעוד {0}", + "before": "{0} קודם", + # Ordinals + "ordinal": {"other": "º"}, + # Date formats + "date_formats": { + "LTS": "H:mm:ss", + "LT": "H:mm", + "LLLL": "dddd, D [ב] MMMM [ב] YYYY H:mm", + "LLL": "D [ב] MMMM [ב] YYYY H:mm", + "LL": "D [ב] MMMM [ב] YYYY", + "L": "DD/MM/YYYY", + }, +} diff --git a/src/pendulum/locales/he/locale.py b/src/pendulum/locales/he/locale.py new file mode 100644 index 00000000..42d55aab --- /dev/null +++ b/src/pendulum/locales/he/locale.py @@ -0,0 +1,277 @@ +from __future__ import annotations + +from pendulum.locales.he.custom import translations as custom_translations + + +""" +he locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "many" + if ( + ((0 == 0 and (0 == 0)) and (not (n == n and (n >= 0 and n <= 10)))) + and ((n % 10) == (n % 10) and ((n % 10) == 0)) + ) + else "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "two" + if ((n == n and (n == 2)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "יום ב׳", + 1: "יום ג׳", + 2: "יום ד׳", + 3: "יום ה׳", + 4: "יום ו׳", + 5: "שבת", + 6: "יום א׳", + }, + "narrow": { + 0: "ב׳", + 1: "ג׳", + 2: "ד׳", + 3: "ה׳", + 4: "ו׳", + 5: "ש׳", + 6: "א׳", + }, + "short": { + 0: "ב׳", + 1: "ג׳", + 2: "ד׳", + 3: "ה׳", + 4: "ו׳", + 5: "ש׳", + 6: "א׳", + }, + "wide": { + 0: "יום שני", + 1: "יום שלישי", + 2: "יום רביעי", + 3: "יום חמישי", + 4: "יום שישי", + 5: "יום שבת", + 6: "יום ראשון", + }, + }, + "months": { + "abbreviated": { + 1: "ינו׳", + 2: "פבר׳", + 3: "מרץ", + 4: "אפר׳", + 5: "מאי", + 6: "יוני", + 7: "יולי", + 8: "אוג׳", + 9: "ספט׳", + 10: "אוק׳", + 11: "נוב׳", + 12: "דצמ׳", + }, + "narrow": { + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "10", + 11: "11", + 12: "12", + }, + "wide": { + 1: "ינואר", + 2: "פברואר", + 3: "מרץ", + 4: "אפריל", + 5: "מאי", + 6: "יוני", + 7: "יולי", + 8: "אוגוסט", + 9: "ספטמבר", + 10: "אוקטובר", + 11: "נובמבר", + 12: "דצמבר", + }, + }, + "units": { + "year": { + "one": "שנה", + "two": "שנתיים", + "many": "{0} שנים", + "other": "{0} שנים", + }, + "month": { + "one": "חודש", + "two": "חודשיים", + "many": "{0} חודשים", + "other": "{0} חודשים", + }, + "week": { + "one": "שבוע", + "two": "שבועיים", + "many": "{0} שבועות", + "other": "{0} שבועות", + }, + "day": { + "one": "יום {0}", + "two": "יומיים", + "many": "{0} יום", + "other": "{0} ימים", + }, + "hour": { + "one": "שעה", + "two": "שעתיים", + "many": "{0} שעות", + "other": "{0} שעות", + }, + "minute": { + "one": "דקה", + "two": "שתי דקות", + "many": "{0} דקות", + "other": "{0} דקות", + }, + "second": { + "one": "שניה", + "two": "שתי שניות", + "many": "\u200f{0} שניות", + "other": "{0} שניות", + }, + "microsecond": { + "one": "{0} מיליונית שנייה", + "two": "{0} מיליוניות שנייה", + "many": "{0} מיליוניות שנייה", + "other": "{0} מיליוניות שנייה", + }, + }, + "relative": { + "year": { + "future": { + "other": "בעוד {0} שנים", + "one": "בעוד שנה", + "two": "בעוד שנתיים", + "many": "בעוד {0} שנה", + }, + "past": { + "other": "לפני {0} שנים", + "one": "לפני שנה", + "two": "לפני שנתיים", + "many": "לפני {0} שנה", + }, + }, + "month": { + "future": { + "other": "בעוד {0} חודשים", + "one": "בעוד חודש", + "two": "בעוד חודשיים", + "many": "בעוד {0} חודשים", + }, + "past": { + "other": "לפני {0} חודשים", + "one": "לפני חודש", + "two": "לפני חודשיים", + "many": "לפני {0} חודשים", + }, + }, + "week": { + "future": { + "other": "בעוד {0} שבועות", + "one": "בעוד שבוע", + "two": "בעוד שבועיים", + "many": "בעוד {0} שבועות", + }, + "past": { + "other": "לפני {0} שבועות", + "one": "לפני שבוע", + "two": "לפני שבועיים", + "many": "לפני {0} שבועות", + }, + }, + "day": { + "future": { + "other": "בעוד {0} ימים", + "one": "בעוד יום {0}", + "two": "בעוד יומיים", + "many": "בעוד {0} ימים", + }, + "past": { + "other": "לפני {0} ימים", + "one": "לפני יום {0}", + "two": "לפני יומיים", + "many": "לפני {0} ימים", + }, + }, + "hour": { + "future": { + "other": "בעוד {0} שעות", + "one": "בעוד שעה", + "two": "בעוד שעתיים", + "many": "בעוד {0} שעות", + }, + "past": { + "other": "לפני {0} שעות", + "one": "לפני שעה", + "two": "לפני שעתיים", + "many": "לפני {0} שעות", + }, + }, + "minute": { + "future": { + "other": "בעוד {0} דקות", + "one": "בעוד דקה", + "two": "בעוד שתי דקות", + "many": "בעוד {0} דקות", + }, + "past": { + "other": "לפני {0} דקות", + "one": "לפני דקה", + "two": "לפני שתי דקות", + "many": "לפני {0} דקות", + }, + }, + "second": { + "future": { + "other": "בעוד {0} שניות", + "one": "בעוד שנייה", + "two": "בעוד שתי שניות", + "many": "בעוד {0} שניות", + }, + "past": { + "other": "לפני {0} שניות", + "one": "לפני שנייה", + "two": "לפני שתי שניות", + "many": "לפני {0} שניות", + }, + }, + }, + "day_periods": { + "midnight": "חצות", + "am": "לפנה״צ", + "pm": "אחה״צ", + "morning1": "בבוקר", + "afternoon1": "בצהריים", + "afternoon2": "אחר הצהריים", + "evening1": "בערב", + "night1": "בלילה", + "night2": "לפנות בוקר", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/nl/__init__.py b/src/pendulum/locales/hi/__init__.py similarity index 100% rename from pendulum/locales/nl/__init__.py rename to src/pendulum/locales/hi/__init__.py diff --git a/src/pendulum/locales/hi/custom.py b/src/pendulum/locales/hi/custom.py new file mode 100644 index 00000000..f8b06ff7 --- /dev/null +++ b/src/pendulum/locales/hi/custom.py @@ -0,0 +1,21 @@ +""" +hi custom locale file. +""" + +translations = { + "units": {"few_second": "कुछ सेकंड"}, + # Relative time + "ago": "{} पहले", + "from_now": "{} में", + "after": "{0} बाद", + "before": "{0} पहले", + # Date formats + "date_formats": { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + }, +} diff --git a/src/pendulum/locales/hi/locale.py b/src/pendulum/locales/hi/locale.py new file mode 100644 index 00000000..e5383dba --- /dev/null +++ b/src/pendulum/locales/hi/locale.py @@ -0,0 +1,221 @@ +from pendulum.locales.hi.custom import translations as custom_translations + + +""" +hi locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + 'plural': lambda n: 'one' if ((n == n and ((n == 0))) or (n == n and ((n == 1)))) else 'other', + 'ordinal': lambda n: 'few' if (n == n and ((n == 4))) else 'many' if (n == n and ((n == 6))) else 'one' if (n == n and ((n == 1))) else 'two' if (n == n and ((n == 2) or (n == 3))) else 'other', + 'translations': { + 'days': { + 'abbreviated': { + 0: 'सोम', + 1: 'मंगल', + 2: 'बुध', + 3: 'गुरु', + 4: 'शुक्र', + 5: 'शनि', + 6: 'रवि', + }, + 'narrow': { + 0: 'सो', + 1: 'मं', + 2: 'बु', + 3: 'गु', + 4: 'शु', + 5: 'श', + 6: 'र', + }, + 'short': { + 0: 'सो', + 1: 'मं', + 2: 'बु', + 3: 'गु', + 4: 'शु', + 5: 'श', + 6: 'र', + }, + 'wide': { + 0: 'सोमवार', + 1: 'मंगलवार', + 2: 'बुधवार', + 3: 'गुरुवार', + 4: 'शुक्रवार', + 5: 'शनिवार', + 6: 'रविवार', + }, + }, + 'months': { + 'abbreviated': { + 1: 'जन॰', + 2: 'फ़र॰', + 3: 'मार्च', + 4: 'अप्रैल', + 5: 'मई', + 6: 'जून', + 7: 'जुल॰', + 8: 'अग॰', + 9: 'सित॰', + 10: 'अक्तू॰', + 11: 'नव॰', + 12: 'दिस॰', + }, + 'narrow': { + 1: 'ज', + 2: 'फ़', + 3: 'मा', + 4: 'अ', + 5: 'म', + 6: 'जू', + 7: 'जु', + 8: 'अ', + 9: 'सि', + 10: 'अ', + 11: 'न', + 12: 'दि', + }, + 'wide': { + 1: 'जनवरी', + 2: 'फ़रवरी', + 3: 'मार्च', + 4: 'अप्रैल', + 5: 'मई', + 6: 'जून', + 7: 'जुलाई', + 8: 'अगस्त', + 9: 'सितंबर', + 10: 'अक्तूबर', + 11: 'नवंबर', + 12: 'दिसंबर', + }, + }, + 'units': { + 'year': { + 'one': '{0} वर्ष', + 'other': '{0} वर्ष', + }, + 'month': { + 'one': '{0} महीना', + 'other': '{0} महीने', + }, + 'week': { + 'one': '{0} सप्ताह', + 'other': '{0} सप्ताह', + }, + 'day': { + 'one': '{0} दिन', + 'other': '{0} दिन', + }, + 'hour': { + 'one': '{0} घंटा', + 'other': '{0} घंटे', + }, + 'minute': { + 'one': '{0} मिनट', + 'other': '{0} मिनट', + }, + 'second': { + 'one': '{0} सेकंड', + 'other': '{0} सेकंड', + }, + 'microsecond': { + 'one': '{0} माइक्रोसेकंड', + 'other': '{0} माइक्रोसेकंड', + }, + }, + 'relative': { + 'year': { + 'future': { + 'other': '{0} वर्ष में', + 'one': '{0} वर्ष में', + }, + 'past': { + 'other': '{0} वर्ष पहले', + 'one': '{0} वर्ष पहले', + }, + }, + 'month': { + 'future': { + 'other': '{0} माह में', + 'one': '{0} माह में', + }, + 'past': { + 'other': '{0} माह पहले', + 'one': '{0} माह पहले', + }, + }, + 'week': { + 'future': { + 'other': '{0} सप्ताह में', + 'one': '{0} सप्ताह में', + }, + 'past': { + 'other': '{0} सप्ताह पहले', + 'one': '{0} सप्ताह पहले', + }, + }, + 'day': { + 'future': { + 'other': '{0} दिन में', + 'one': '{0} दिन में', + }, + 'past': { + 'other': '{0} दिन पहले', + 'one': '{0} दिन पहले', + }, + }, + 'hour': { + 'future': { + 'other': '{0} घंटे में', + 'one': '{0} घंटे में', + }, + 'past': { + 'other': '{0} घंटे पहले', + 'one': '{0} घंटे पहले', + }, + }, + 'minute': { + 'future': { + 'other': '{0} मिनट में', + 'one': '{0} मिनट में', + }, + 'past': { + 'other': '{0} मिनट पहले', + 'one': '{0} मिनट पहले', + }, + }, + 'second': { + 'future': { + 'other': '{0} सेकंड में', + 'one': '{0} सेकंड में', + }, + 'past': { + 'other': '{0} सेकंड पहले', + 'one': '{0} सेकंड पहले', + }, + }, + }, + 'day_periods': { + "midnight": "मध्यरात्रि", + "am": "AM", + "noon": "दोपहर", + "pm": "PM", + "morning1": "सुबह में", + "afternoon1": "दोपहर में", + "evening1": "शाम में", + "night1": "रात में", + }, + 'week_data': { + 'min_days': 1, + 'first_day': 0, + 'weekend_start': 5, + 'weekend_end': 6, + }, + }, + 'custom': custom_translations +} diff --git a/pendulum/locales/nn/__init__.py b/src/pendulum/locales/id/__init__.py similarity index 100% rename from pendulum/locales/nn/__init__.py rename to src/pendulum/locales/id/__init__.py diff --git a/pendulum/locales/id/custom.py b/src/pendulum/locales/id/custom.py similarity index 87% rename from pendulum/locales/id/custom.py rename to src/pendulum/locales/id/custom.py index 8abd4747..3d4460cb 100644 --- a/pendulum/locales/id/custom.py +++ b/src/pendulum/locales/id/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ id custom locale file. """ +from __future__ import annotations + translations = { "units": {"few_second": "beberapa detik"}, diff --git a/pendulum/locales/id/locale.py b/src/pendulum/locales/id/locale.py similarity index 78% rename from pendulum/locales/id/locale.py rename to src/pendulum/locales/id/locale.py index 44ee697d..20733571 100644 --- a/pendulum/locales/id/locale.py +++ b/src/pendulum/locales/id/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.id.custom import translations as custom_translations """ @@ -17,32 +16,32 @@ "translations": { "days": { "abbreviated": { - 0: "Min", - 1: "Sen", - 2: "Sel", - 3: "Rab", - 4: "Kam", - 5: "Jum", - 6: "Sab", + 0: "Sen", + 1: "Sel", + 2: "Rab", + 3: "Kam", + 4: "Jum", + 5: "Sab", + 6: "Min", }, - "narrow": {0: "M", 1: "S", 2: "S", 3: "R", 4: "K", 5: "J", 6: "S"}, + "narrow": {0: "S", 1: "S", 2: "R", 3: "K", 4: "J", 5: "S", 6: "M"}, "short": { - 0: "Min", - 1: "Sen", - 2: "Sel", - 3: "Rab", - 4: "Kam", - 5: "Jum", - 6: "Sab", + 0: "Sen", + 1: "Sel", + 2: "Rab", + 3: "Kam", + 4: "Jum", + 5: "Sab", + 6: "Min", }, "wide": { - 0: "Minggu", - 1: "Senin", - 2: "Selasa", - 3: "Rabu", - 4: "Kamis", - 5: "Jumat", - 6: "Sabtu", + 0: "Senin", + 1: "Selasa", + 2: "Rabu", + 3: "Kamis", + 4: "Jumat", + 5: "Sabtu", + 6: "Minggu", }, }, "months": { @@ -139,6 +138,12 @@ "evening1": "sore", "night1": "malam", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/locales/pl/__init__.py b/src/pendulum/locales/it/__init__.py similarity index 100% rename from pendulum/locales/pl/__init__.py rename to src/pendulum/locales/it/__init__.py diff --git a/pendulum/locales/it/custom.py b/src/pendulum/locales/it/custom.py similarity index 88% rename from pendulum/locales/it/custom.py rename to src/pendulum/locales/it/custom.py index 6f3963e1..b1a77a09 100644 --- a/pendulum/locales/it/custom.py +++ b/src/pendulum/locales/it/custom.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- """ it custom locale file. """ - -from __future__ import unicode_literals +from __future__ import annotations translations = { diff --git a/pendulum/locales/it/locale.py b/src/pendulum/locales/it/locale.py similarity index 79% rename from pendulum/locales/it/locale.py rename to src/pendulum/locales/it/locale.py index 920a778b..ae5dc393 100644 --- a/pendulum/locales/it/locale.py +++ b/src/pendulum/locales/it/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.it.custom import translations as custom_translations """ @@ -13,7 +12,7 @@ locale = { "plural": lambda n: "one" - if ((n == n and ((n == 1))) and (0 == 0 and ((0 == 0)))) + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) else "other", "ordinal": lambda n: "many" if (n == n and ((n == 11) or (n == 8) or (n == 80) or (n == 800))) @@ -21,32 +20,32 @@ "translations": { "days": { "abbreviated": { - 0: "dom", - 1: "lun", - 2: "mar", - 3: "mer", - 4: "gio", - 5: "ven", - 6: "sab", + 0: "lun", + 1: "mar", + 2: "mer", + 3: "gio", + 4: "ven", + 5: "sab", + 6: "dom", }, - "narrow": {0: "D", 1: "L", 2: "M", 3: "M", 4: "G", 5: "V", 6: "S"}, + "narrow": {0: "L", 1: "M", 2: "M", 3: "G", 4: "V", 5: "S", 6: "D"}, "short": { - 0: "dom", - 1: "lun", - 2: "mar", - 3: "mer", - 4: "gio", - 5: "ven", - 6: "sab", + 0: "lun", + 1: "mar", + 2: "mer", + 3: "gio", + 4: "ven", + 5: "sab", + 6: "dom", }, "wide": { - 0: "domenica", - 1: "lunedì", - 2: "martedì", - 3: "mercoledì", - 4: "giovedì", - 5: "venerdì", - 6: "sabato", + 0: "lunedì", + 1: "martedì", + 2: "mercoledì", + 3: "giovedì", + 4: "venerdì", + 5: "sabato", + 6: "domenica", }, }, "months": { @@ -143,6 +142,12 @@ "evening1": "di sera", "night1": "di notte", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/locales/pt_br/__init__.py b/src/pendulum/locales/ja/__init__.py similarity index 100% rename from pendulum/locales/pt_br/__init__.py rename to src/pendulum/locales/ja/__init__.py diff --git a/src/pendulum/locales/ja/custom.py b/src/pendulum/locales/ja/custom.py new file mode 100644 index 00000000..4cb5b956 --- /dev/null +++ b/src/pendulum/locales/ja/custom.py @@ -0,0 +1,23 @@ +""" +ja custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "数秒"}, + # Relative time + "ago": "{} 前に", + "from_now": "今から {}", + "after": "{0} 後", + "before": "{0} 前", + # Date formats + "date_formats": { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + }, +} diff --git a/src/pendulum/locales/ja/locale.py b/src/pendulum/locales/ja/locale.py new file mode 100644 index 00000000..a1d3bd92 --- /dev/null +++ b/src/pendulum/locales/ja/locale.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +from pendulum.locales.ja.custom import translations as custom_translations + + +""" +ja locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "月", + 1: "火", + 2: "水", + 3: "木", + 4: "金", + 5: "土", + 6: "日", + }, + "narrow": { + 0: "月", + 1: "火", + 2: "水", + 3: "木", + 4: "金", + 5: "土", + 6: "日", + }, + "short": { + 0: "月", + 1: "火", + 2: "水", + 3: "木", + 4: "金", + 5: "土", + 6: "日", + }, + "wide": { + 0: "月曜日", + 1: "火曜日", + 2: "水曜日", + 3: "木曜日", + 4: "金曜日", + 5: "土曜日", + 6: "日曜日", + }, + }, + "months": { + "abbreviated": { + 1: "1月", + 2: "2月", + 3: "3月", + 4: "4月", + 5: "5月", + 6: "6月", + 7: "7月", + 8: "8月", + 9: "9月", + 10: "10月", + 11: "11月", + 12: "12月", + }, + "narrow": { + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "10", + 11: "11", + 12: "12", + }, + "wide": { + 1: "1月", + 2: "2月", + 3: "3月", + 4: "4月", + 5: "5月", + 6: "6月", + 7: "7月", + 8: "8月", + 9: "9月", + 10: "10月", + 11: "11月", + 12: "12月", + }, + }, + "units": { + "year": { + "other": "{0} 年", + }, + "month": { + "other": "{0} か月", + }, + "week": { + "other": "{0} 週間", + }, + "day": { + "other": "{0} 日", + }, + "hour": { + "other": "{0} 時間", + }, + "minute": { + "other": "{0} 分", + }, + "second": { + "other": "{0} 秒", + }, + "microsecond": { + "other": "{0} マイクロ秒", + }, + }, + "relative": { + "year": { + "future": { + "other": "{0} 年後", + }, + "past": { + "other": "{0} 年前", + }, + }, + "month": { + "future": { + "other": "{0} か月後", + }, + "past": { + "other": "{0} か月前", + }, + }, + "week": { + "future": { + "other": "{0} 週間後", + }, + "past": { + "other": "{0} 週間前", + }, + }, + "day": { + "future": { + "other": "{0} 日後", + }, + "past": { + "other": "{0} 日前", + }, + }, + "hour": { + "future": { + "other": "{0} 時間後", + }, + "past": { + "other": "{0} 時間前", + }, + }, + "minute": { + "future": { + "other": "{0} 分後", + }, + "past": { + "other": "{0} 分前", + }, + }, + "second": { + "future": { + "other": "{0} 秒後", + }, + "past": { + "other": "{0} 秒前", + }, + }, + }, + "day_periods": { + "midnight": "真夜中", + "am": "午前", + "noon": "正午", + "pm": "午後", + "morning1": "朝", + "afternoon1": "昼", + "evening1": "夕方", + "night1": "夜", + "night2": "夜中", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/pendulum/locales/ru/__init__.py b/src/pendulum/locales/ko/__init__.py similarity index 100% rename from pendulum/locales/ru/__init__.py rename to src/pendulum/locales/ko/__init__.py diff --git a/pendulum/locales/ko/custom.py b/src/pendulum/locales/ko/custom.py similarity index 75% rename from pendulum/locales/ko/custom.py rename to src/pendulum/locales/ko/custom.py index 0dd6a11a..c3546b7e 100644 --- a/pendulum/locales/ko/custom.py +++ b/src/pendulum/locales/ko/custom.py @@ -1,15 +1,13 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ ko custom locale file. """ +from __future__ import annotations + translations = { # Relative time - "after": "{0} 뒤", - "before": "{0} 앞", + "after": "{0} 후", + "before": "{0} 전", # Date formats "date_formats": { "LTS": "A h시 m분 s초", diff --git a/pendulum/locales/ko/locale.py b/src/pendulum/locales/ko/locale.py similarity index 78% rename from pendulum/locales/ko/locale.py rename to src/pendulum/locales/ko/locale.py index dfdb35a6..b285dc06 100644 --- a/pendulum/locales/ko/locale.py +++ b/src/pendulum/locales/ko/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.ko.custom import translations as custom_translations """ @@ -16,17 +15,17 @@ "ordinal": lambda n: "other", "translations": { "days": { - "abbreviated": {0: "일", 1: "월", 2: "화", 3: "수", 4: "목", 5: "금", 6: "토"}, - "narrow": {0: "일", 1: "월", 2: "화", 3: "수", 4: "목", 5: "금", 6: "토"}, - "short": {0: "일", 1: "월", 2: "화", 3: "수", 4: "목", 5: "금", 6: "토"}, + "abbreviated": {0: "월", 1: "화", 2: "수", 3: "목", 4: "금", 5: "토", 6: "일"}, + "narrow": {0: "월", 1: "화", 2: "수", 3: "목", 4: "금", 5: "토", 6: "일"}, + "short": {0: "월", 1: "화", 2: "수", 3: "목", 4: "금", 5: "토", 6: "일"}, "wide": { - 0: "일요일", - 1: "월요일", - 2: "화요일", - 3: "수요일", - 4: "목요일", - 5: "금요일", - 6: "토요일", + 0: "월요일", + 1: "화요일", + 2: "수요일", + 3: "목요일", + 4: "금요일", + 5: "토요일", + 6: "일요일", }, }, "months": { @@ -103,6 +102,12 @@ "evening1": "저녁", "night1": "밤", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/src/pendulum/locales/locale.py b/src/pendulum/locales/locale.py new file mode 100644 index 00000000..951ba8be --- /dev/null +++ b/src/pendulum/locales/locale.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import re + +from pathlib import Path +from typing import Any +from typing import ClassVar +from typing import Dict +from typing import cast + + +class Locale: + """ + Represent a specific locale. + """ + + _cache: ClassVar[dict[str, Locale]] = {} + + def __init__(self, locale: str, data: Any) -> None: + self._locale: str = locale + self._data: Any = data + self._key_cache: dict[str, str] = {} + + @classmethod + def load(cls, locale: str | Locale) -> Locale: + from importlib import import_module, resources + + if isinstance(locale, Locale): + return locale + + locale = cls.normalize_locale(locale) + if locale in cls._cache: + return cls._cache[locale] + + # Checking locale existence + actual_locale = locale + locale_path = cast(Path, resources.files(__package__).joinpath(actual_locale)) + while not locale_path.exists(): + if actual_locale == locale: + raise ValueError(f"Locale [{locale}] does not exist.") + + actual_locale = actual_locale.split("_")[0] + + m = import_module(f"pendulum.locales.{actual_locale}.locale") + + cls._cache[locale] = cls(locale, m.locale) + + return cls._cache[locale] + + @classmethod + def normalize_locale(cls, locale: str) -> str: + m = re.fullmatch("([a-z]{2})[-_]([a-z]{2})", locale, re.I) + if m: + return f"{m.group(1).lower()}_{m.group(2).lower()}" + else: + return locale.lower() + + def get(self, key: str, default: Any | None = None) -> Any: + if key in self._key_cache: + return self._key_cache[key] + + parts = key.split(".") + try: + result = self._data[parts[0]] + for part in parts[1:]: + result = result[part] + except KeyError: + result = default + + self._key_cache[key] = result + + return self._key_cache[key] + + def translation(self, key: str) -> Any: + return self.get(f"translations.{key}") + + def plural(self, number: int) -> str: + return cast(str, self._data["plural"](number)) + + def ordinal(self, number: int) -> str: + return cast(str, self._data["ordinal"](number)) + + def ordinalize(self, number: int) -> str: + ordinal = self.get(f"custom.ordinal.{self.ordinal(number)}") + + if not ordinal: + return f"{number}" + + return f"{number}{ordinal}" + + def match_translation(self, key: str, value: Any) -> dict[str, str] | None: + translations = self.translation(key) + if value not in translations.values(): + return None + + return cast(Dict[str, str], {v: k for k, v in translations.items()}[value]) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}('{self._locale}')" diff --git a/pendulum/locales/zh/__init__.py b/src/pendulum/locales/lt/__init__.py similarity index 100% rename from pendulum/locales/zh/__init__.py rename to src/pendulum/locales/lt/__init__.py diff --git a/pendulum/locales/lt/custom.py b/src/pendulum/locales/lt/custom.py similarity index 98% rename from pendulum/locales/lt/custom.py rename to src/pendulum/locales/lt/custom.py index 11c99805..d7f17d3d 100644 --- a/pendulum/locales/lt/custom.py +++ b/src/pendulum/locales/lt/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ lt custom locale file. """ +from __future__ import annotations + translations = { # Relative time diff --git a/pendulum/locales/lt/locale.py b/src/pendulum/locales/lt/locale.py similarity index 85% rename from pendulum/locales/lt/locale.py rename to src/pendulum/locales/lt/locale.py index 12f13359..6e9a4606 100644 --- a/pendulum/locales/lt/locale.py +++ b/src/pendulum/locales/lt/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.lt.custom import translations as custom_translations """ @@ -14,39 +13,39 @@ locale = { "plural": lambda n: "few" if ( - ((n % 10) == (n % 10) and (((n % 10) >= 2 and (n % 10) <= 9))) - and (not ((n % 100) == (n % 100) and (((n % 100) >= 11 and (n % 100) <= 19)))) + ((n % 10) == (n % 10) and ((n % 10) >= 2 and (n % 10) <= 9)) + and (not ((n % 100) == (n % 100) and ((n % 100) >= 11 and (n % 100) <= 19))) ) else "many" - if (not (0 == 0 and ((0 == 0)))) + if (not (0 == 0 and (0 == 0))) else "one" if ( - ((n % 10) == (n % 10) and (((n % 10) == 1))) - and (not ((n % 100) == (n % 100) and (((n % 100) >= 11 and (n % 100) <= 19)))) + ((n % 10) == (n % 10) and ((n % 10) == 1)) + and (not ((n % 100) == (n % 100) and ((n % 100) >= 11 and (n % 100) <= 19))) ) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "sk", - 1: "pr", - 2: "an", - 3: "tr", - 4: "kt", - 5: "pn", - 6: "št", + 0: "pr", + 1: "an", + 2: "tr", + 3: "kt", + 4: "pn", + 5: "št", + 6: "sk", }, - "narrow": {0: "S", 1: "P", 2: "A", 3: "T", 4: "K", 5: "P", 6: "Š"}, - "short": {0: "Sk", 1: "Pr", 2: "An", 3: "Tr", 4: "Kt", 5: "Pn", 6: "Št"}, + "narrow": {0: "P", 1: "A", 2: "T", 3: "K", 4: "P", 5: "Š", 6: "S"}, + "short": {0: "Pr", 1: "An", 2: "Tr", 3: "Kt", 4: "Pn", 5: "Št", 6: "Sk"}, "wide": { - 0: "sekmadienis", - 1: "pirmadienis", - 2: "antradienis", - 3: "trečiadienis", - 4: "ketvirtadienis", - 5: "penktadienis", - 6: "šeštadienis", + 0: "pirmadienis", + 1: "antradienis", + 2: "trečiadienis", + 3: "ketvirtadienis", + 4: "penktadienis", + 5: "šeštadienis", + 6: "sekmadienis", }, }, "months": { @@ -253,6 +252,12 @@ "evening1": "vakaras", "night1": "naktis", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/tz/data/__init__.py b/src/pendulum/locales/nb/__init__.py similarity index 100% rename from pendulum/tz/data/__init__.py rename to src/pendulum/locales/nb/__init__.py diff --git a/pendulum/locales/nb/custom.py b/src/pendulum/locales/nb/custom.py similarity index 87% rename from pendulum/locales/nb/custom.py rename to src/pendulum/locales/nb/custom.py index 216dd046..554e7f3d 100644 --- a/pendulum/locales/nb/custom.py +++ b/src/pendulum/locales/nb/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ nn custom locale file. """ +from __future__ import annotations + translations = { # Relative time diff --git a/pendulum/locales/nb/locale.py b/src/pendulum/locales/nb/locale.py similarity index 80% rename from pendulum/locales/nb/locale.py rename to src/pendulum/locales/nb/locale.py index 0ad08d18..084f019e 100644 --- a/pendulum/locales/nb/locale.py +++ b/src/pendulum/locales/nb/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.nb.custom import translations as custom_translations """ @@ -12,37 +11,37 @@ locale = { - "plural": lambda n: "one" if (n == n and ((n == 1))) else "other", + "plural": lambda n: "one" if (n == n and (n == 1)) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "søn.", - 1: "man.", - 2: "tir.", - 3: "ons.", - 4: "tor.", - 5: "fre.", - 6: "lør.", + 0: "man.", + 1: "tir.", + 2: "ons.", + 3: "tor.", + 4: "fre.", + 5: "lør.", + 6: "søn.", }, - "narrow": {0: "S", 1: "M", 2: "T", 3: "O", 4: "T", 5: "F", 6: "L"}, + "narrow": {0: "M", 1: "T", 2: "O", 3: "T", 4: "F", 5: "L", 6: "S"}, "short": { - 0: "sø.", - 1: "ma.", - 2: "ti.", - 3: "on.", - 4: "to.", - 5: "fr.", - 6: "lø.", + 0: "ma.", + 1: "ti.", + 2: "on.", + 3: "to.", + 4: "fr.", + 5: "lø.", + 6: "sø.", }, "wide": { - 0: "søndag", - 1: "mandag", - 2: "tirsdag", - 3: "onsdag", - 4: "torsdag", - 5: "fredag", - 6: "lørdag", + 0: "mandag", + 1: "tirsdag", + 2: "onsdag", + 3: "torsdag", + 4: "fredag", + 5: "lørdag", + 6: "søndag", }, }, "months": { @@ -148,6 +147,12 @@ "evening1": "kvelden", "night1": "natten", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/utils/__init__.py b/src/pendulum/locales/nl/__init__.py similarity index 100% rename from pendulum/utils/__init__.py rename to src/pendulum/locales/nl/__init__.py diff --git a/pendulum/locales/nl/custom.py b/src/pendulum/locales/nl/custom.py similarity index 88% rename from pendulum/locales/nl/custom.py rename to src/pendulum/locales/nl/custom.py index 2b8790ec..ca906739 100644 --- a/pendulum/locales/nl/custom.py +++ b/src/pendulum/locales/nl/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ nl custom locale file. """ +from __future__ import annotations + translations = { "units": {"few_second": "enkele seconden"}, diff --git a/pendulum/locales/nl/locale.py b/src/pendulum/locales/nl/locale.py similarity index 80% rename from pendulum/locales/nl/locale.py rename to src/pendulum/locales/nl/locale.py index 1e4d67ed..68b54e74 100644 --- a/pendulum/locales/nl/locale.py +++ b/src/pendulum/locales/nl/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.nl.custom import translations as custom_translations """ @@ -13,30 +12,30 @@ locale = { "plural": lambda n: "one" - if ((n == n and ((n == 1))) and (0 == 0 and ((0 == 0)))) + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "zo", - 1: "ma", - 2: "di", - 3: "wo", - 4: "do", - 5: "vr", - 6: "za", + 0: "ma", + 1: "di", + 2: "wo", + 3: "do", + 4: "vr", + 5: "za", + 6: "zo", }, - "narrow": {0: "Z", 1: "M", 2: "D", 3: "W", 4: "D", 5: "V", 6: "Z"}, - "short": {0: "zo", 1: "ma", 2: "di", 3: "wo", 4: "do", 5: "vr", 6: "za"}, + "narrow": {0: "M", 1: "D", 2: "W", 3: "D", 4: "V", 5: "Z", 6: "Z"}, + "short": {0: "ma", 1: "di", 2: "wo", 3: "do", 4: "vr", 5: "za", 6: "zo"}, "wide": { - 0: "zondag", - 1: "maandag", - 2: "dinsdag", - 3: "woensdag", - 4: "donderdag", - 5: "vrijdag", - 6: "zaterdag", + 0: "maandag", + 1: "dinsdag", + 2: "woensdag", + 3: "donderdag", + 4: "vrijdag", + 5: "zaterdag", + 6: "zondag", }, }, "months": { @@ -131,6 +130,12 @@ "afternoon1": "‘s middags", "evening1": "‘s avonds", "night1": "‘s nachts", + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, }, "custom": custom_translations, diff --git a/tests/tz/zoneinfo/__init__.py b/src/pendulum/locales/nn/__init__.py similarity index 100% rename from tests/tz/zoneinfo/__init__.py rename to src/pendulum/locales/nn/__init__.py diff --git a/pendulum/locales/nn/custom.py b/src/pendulum/locales/nn/custom.py similarity index 87% rename from pendulum/locales/nn/custom.py rename to src/pendulum/locales/nn/custom.py index 216dd046..554e7f3d 100644 --- a/pendulum/locales/nn/custom.py +++ b/src/pendulum/locales/nn/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ nn custom locale file. """ +from __future__ import annotations + translations = { # Relative time diff --git a/pendulum/locales/nn/locale.py b/src/pendulum/locales/nn/locale.py similarity index 78% rename from pendulum/locales/nn/locale.py rename to src/pendulum/locales/nn/locale.py index d7ad7909..737ee6db 100644 --- a/pendulum/locales/nn/locale.py +++ b/src/pendulum/locales/nn/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.nn.custom import translations as custom_translations """ @@ -12,37 +11,37 @@ locale = { - "plural": lambda n: "one" if (n == n and ((n == 1))) else "other", + "plural": lambda n: "one" if (n == n and (n == 1)) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "søn.", - 1: "mån.", - 2: "tys.", - 3: "ons.", - 4: "tor.", - 5: "fre.", - 6: "lau.", + 0: "mån.", + 1: "tys.", + 2: "ons.", + 3: "tor.", + 4: "fre.", + 5: "lau.", + 6: "søn.", }, - "narrow": {0: "S", 1: "M", 2: "T", 3: "O", 4: "T", 5: "F", 6: "L"}, + "narrow": {0: "M", 1: "T", 2: "O", 3: "T", 4: "F", 5: "L", 6: "S"}, "short": { - 0: "sø.", - 1: "må.", - 2: "ty.", - 3: "on.", - 4: "to.", - 5: "fr.", - 6: "la.", + 0: "må.", + 1: "ty.", + 2: "on.", + 3: "to.", + 4: "fr.", + 5: "la.", + 6: "sø.", }, "wide": { - 0: "søndag", - 1: "måndag", - 2: "tysdag", - 3: "onsdag", - 4: "torsdag", - 5: "fredag", - 6: "laurdag", + 0: "måndag", + 1: "tysdag", + 2: "onsdag", + 3: "torsdag", + 4: "fredag", + 5: "laurdag", + 6: "søndag", }, }, "months": { @@ -139,6 +138,12 @@ }, }, "day_periods": {"am": "formiddag", "pm": "ettermiddag"}, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/pendulum/py.typed b/src/pendulum/locales/pl/__init__.py similarity index 100% rename from pendulum/py.typed rename to src/pendulum/locales/pl/__init__.py diff --git a/pendulum/locales/pl/custom.py b/src/pendulum/locales/pl/custom.py similarity index 87% rename from pendulum/locales/pl/custom.py rename to src/pendulum/locales/pl/custom.py index d93465e5..0aaab900 100644 --- a/pendulum/locales/pl/custom.py +++ b/src/pendulum/locales/pl/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ pl custom locale file. """ +from __future__ import annotations + translations = { "units": {"few_second": "kilka sekund"}, diff --git a/pendulum/locales/pl/locale.py b/src/pendulum/locales/pl/locale.py similarity index 82% rename from pendulum/locales/pl/locale.py rename to src/pendulum/locales/pl/locale.py index 7f83ee50..c7091202 100644 --- a/pendulum/locales/pl/locale.py +++ b/src/pendulum/locales/pl/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.pl.custom import translations as custom_translations """ @@ -15,61 +14,61 @@ "plural": lambda n: "few" if ( ( - (0 == 0 and ((0 == 0))) - and ((n % 10) == (n % 10) and (((n % 10) >= 2 and (n % 10) <= 4))) + (0 == 0 and (0 == 0)) + and ((n % 10) == (n % 10) and ((n % 10) >= 2 and (n % 10) <= 4)) ) - and (not ((n % 100) == (n % 100) and (((n % 100) >= 12 and (n % 100) <= 14)))) + and (not ((n % 100) == (n % 100) and ((n % 100) >= 12 and (n % 100) <= 14))) ) else "many" if ( ( ( - ((0 == 0 and ((0 == 0))) and (not (n == n and ((n == 1))))) - and ((n % 10) == (n % 10) and (((n % 10) >= 0 and (n % 10) <= 1))) + ((0 == 0 and (0 == 0)) and (not (n == n and (n == 1)))) + and ((n % 10) == (n % 10) and ((n % 10) >= 0 and (n % 10) <= 1)) ) or ( - (0 == 0 and ((0 == 0))) - and ((n % 10) == (n % 10) and (((n % 10) >= 5 and (n % 10) <= 9))) + (0 == 0 and (0 == 0)) + and ((n % 10) == (n % 10) and ((n % 10) >= 5 and (n % 10) <= 9)) ) ) or ( - (0 == 0 and ((0 == 0))) - and ((n % 100) == (n % 100) and (((n % 100) >= 12 and (n % 100) <= 14))) + (0 == 0 and (0 == 0)) + and ((n % 100) == (n % 100) and ((n % 100) >= 12 and (n % 100) <= 14)) ) ) else "one" - if ((n == n and ((n == 1))) and (0 == 0 and ((0 == 0)))) + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "niedz.", - 1: "pon.", - 2: "wt.", - 3: "śr.", - 4: "czw.", - 5: "pt.", - 6: "sob.", + 0: "pon.", + 1: "wt.", + 2: "śr.", + 3: "czw.", + 4: "pt.", + 5: "sob.", + 6: "niedz.", }, - "narrow": {0: "n", 1: "p", 2: "w", 3: "ś", 4: "c", 5: "p", 6: "s"}, + "narrow": {0: "p", 1: "w", 2: "ś", 3: "c", 4: "p", 5: "s", 6: "n"}, "short": { - 0: "nie", - 1: "pon", - 2: "wto", - 3: "śro", - 4: "czw", - 5: "pią", - 6: "sob", + 0: "pon", + 1: "wto", + 2: "śro", + 3: "czw", + 4: "pią", + 5: "sob", + 6: "nie", }, "wide": { - 0: "niedziela", - 1: "poniedziałek", - 2: "wtorek", - 3: "środa", - 4: "czwartek", - 5: "piątek", - 6: "sobota", + 0: "poniedziałek", + 1: "wtorek", + 2: "środa", + 3: "czwartek", + 4: "piątek", + 5: "sobota", + 6: "niedziela", }, }, "months": { @@ -277,6 +276,12 @@ "evening1": "wieczorem", "night1": "w nocy", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/src/pendulum/locales/pt_br/__init__.py b/src/pendulum/locales/pt_br/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pendulum/locales/pt_br/custom.py b/src/pendulum/locales/pt_br/custom.py similarity index 86% rename from pendulum/locales/pt_br/custom.py rename to src/pendulum/locales/pt_br/custom.py index 065a1401..87a7702d 100644 --- a/pendulum/locales/pt_br/custom.py +++ b/src/pendulum/locales/pt_br/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ pt-br custom locale file. """ +from __future__ import annotations + translations = { # Relative time diff --git a/pendulum/locales/pt_br/locale.py b/src/pendulum/locales/pt_br/locale.py similarity index 78% rename from pendulum/locales/pt_br/locale.py rename to src/pendulum/locales/pt_br/locale.py index 307f34f6..793cba8c 100644 --- a/pendulum/locales/pt_br/locale.py +++ b/src/pendulum/locales/pt_br/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.pt_br.custom import translations as custom_translations """ @@ -13,38 +12,38 @@ locale = { "plural": lambda n: "one" - if ((n == n and ((n >= 0 and n <= 2))) and (not (n == n and ((n == 2))))) + if ((n == n and (n >= 0 and n <= 2)) and (not (n == n and (n == 2)))) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "dom", - 1: "seg", - 2: "ter", - 3: "qua", - 4: "qui", - 5: "sex", - 6: "sáb", + 0: "seg", + 1: "ter", + 2: "qua", + 3: "qui", + 4: "sex", + 5: "sáb", + 6: "dom", }, - "narrow": {0: "D", 1: "S", 2: "T", 3: "Q", 4: "Q", 5: "S", 6: "S"}, + "narrow": {0: "S", 1: "T", 2: "Q", 3: "Q", 4: "S", 5: "S", 6: "D"}, "short": { - 0: "dom", - 1: "seg", - 2: "ter", - 3: "qua", - 4: "qui", - 5: "sex", - 6: "sáb", + 0: "seg", + 1: "ter", + 2: "qua", + 3: "qui", + 4: "sex", + 5: "sáb", + 6: "dom", }, "wide": { - 0: "domingo", - 1: "segunda-feira", - 2: "terça-feira", - 3: "quarta-feira", - 4: "quinta-feira", - 5: "sexta-feira", - 6: "sábado", + 0: "segunda-feira", + 1: "terça-feira", + 2: "quarta-feira", + 3: "quinta-feira", + 4: "sexta-feira", + 5: "sábado", + 6: "domingo", }, }, "months": { @@ -141,6 +140,12 @@ "evening1": "da noite", "night1": "da madrugada", }, + "week_data": { + "min_days": 1, + "first_day": 6, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/src/pendulum/locales/ru/__init__.py b/src/pendulum/locales/ru/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pendulum/locales/ru/custom.py b/src/pendulum/locales/ru/custom.py similarity index 87% rename from pendulum/locales/ru/custom.py rename to src/pendulum/locales/ru/custom.py index 38f86efa..e1f87ffe 100644 --- a/pendulum/locales/ru/custom.py +++ b/src/pendulum/locales/ru/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ ru custom locale file. """ +from __future__ import annotations + translations = { # Relative time diff --git a/pendulum/locales/ru/locale.py b/src/pendulum/locales/ru/locale.py similarity index 83% rename from pendulum/locales/ru/locale.py rename to src/pendulum/locales/ru/locale.py index a0800357..b9eab83a 100644 --- a/pendulum/locales/ru/locale.py +++ b/src/pendulum/locales/ru/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.ru.custom import translations as custom_translations """ @@ -15,53 +14,53 @@ "plural": lambda n: "few" if ( ( - (0 == 0 and ((0 == 0))) - and ((n % 10) == (n % 10) and (((n % 10) >= 2 and (n % 10) <= 4))) + (0 == 0 and (0 == 0)) + and ((n % 10) == (n % 10) and ((n % 10) >= 2 and (n % 10) <= 4)) ) - and (not ((n % 100) == (n % 100) and (((n % 100) >= 12 and (n % 100) <= 14)))) + and (not ((n % 100) == (n % 100) and ((n % 100) >= 12 and (n % 100) <= 14))) ) else "many" if ( ( - ((0 == 0 and ((0 == 0))) and ((n % 10) == (n % 10) and (((n % 10) == 0)))) + ((0 == 0 and (0 == 0)) and ((n % 10) == (n % 10) and ((n % 10) == 0))) or ( - (0 == 0 and ((0 == 0))) - and ((n % 10) == (n % 10) and (((n % 10) >= 5 and (n % 10) <= 9))) + (0 == 0 and (0 == 0)) + and ((n % 10) == (n % 10) and ((n % 10) >= 5 and (n % 10) <= 9)) ) ) or ( - (0 == 0 and ((0 == 0))) - and ((n % 100) == (n % 100) and (((n % 100) >= 11 and (n % 100) <= 14))) + (0 == 0 and (0 == 0)) + and ((n % 100) == (n % 100) and ((n % 100) >= 11 and (n % 100) <= 14)) ) ) else "one" if ( - ((0 == 0 and ((0 == 0))) and ((n % 10) == (n % 10) and (((n % 10) == 1)))) - and (not ((n % 100) == (n % 100) and (((n % 100) == 11)))) + ((0 == 0 and (0 == 0)) and ((n % 10) == (n % 10) and ((n % 10) == 1))) + and (not ((n % 100) == (n % 100) and ((n % 100) == 11))) ) else "other", "ordinal": lambda n: "other", "translations": { "days": { "abbreviated": { - 0: "вс", - 1: "пн", - 2: "вт", - 3: "ср", - 4: "чт", - 5: "пт", - 6: "сб", + 0: "пн", + 1: "вт", + 2: "ср", + 3: "чт", + 4: "пт", + 5: "сб", + 6: "вс", }, - "narrow": {0: "вс", 1: "пн", 2: "вт", 3: "ср", 4: "чт", 5: "пт", 6: "сб"}, - "short": {0: "вс", 1: "пн", 2: "вт", 3: "ср", 4: "чт", 5: "пт", 6: "сб"}, + "narrow": {0: "пн", 1: "вт", 2: "ср", 3: "чт", 4: "пт", 5: "сб", 6: "вс"}, + "short": {0: "пн", 1: "вт", 2: "ср", 3: "чт", 4: "пт", 5: "сб", 6: "вс"}, "wide": { - 0: "воскресенье", - 1: "понедельник", - 2: "вторник", - 3: "среда", - 4: "четверг", - 5: "пятница", - 6: "суббота", + 0: "понедельник", + 1: "вторник", + 2: "среда", + 3: "четверг", + 4: "пятница", + 5: "суббота", + 6: "воскресенье", }, }, "months": { @@ -268,6 +267,12 @@ "evening1": "вечера", "night1": "ночи", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/src/pendulum/locales/sk/__init__.py b/src/pendulum/locales/sk/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/pendulum/locales/sk/custom.py b/src/pendulum/locales/sk/custom.py new file mode 100644 index 00000000..0059c11e --- /dev/null +++ b/src/pendulum/locales/sk/custom.py @@ -0,0 +1,22 @@ +""" +sk custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "ago": "pred {}", + "from_now": "o {}", + "after": "{0} po", + "before": "{0} pred", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "LLLL": "dddd, D. MMMM YYYY HH:mm", + "LLL": "D. MMMM YYYY HH:mm", + "LL": "D. MMMM YYYY", + "L": "DD.MM.YYYY", + }, +} diff --git a/src/pendulum/locales/sk/locale.py b/src/pendulum/locales/sk/locale.py new file mode 100644 index 00000000..530303fb --- /dev/null +++ b/src/pendulum/locales/sk/locale.py @@ -0,0 +1,274 @@ +from __future__ import annotations + +from pendulum.locales.sk.custom import translations as custom_translations + + +""" +sk locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ((n == n and (n >= 2 and n <= 4)) and (0 == 0 and (0 == 0))) + else "many" + if (not (0 == 0 and (0 == 0))) + else "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "po", + 1: "ut", + 2: "st", + 3: "št", + 4: "pi", + 5: "so", + 6: "ne", + }, + "narrow": { + 0: "p", + 1: "u", + 2: "s", + 3: "š", + 4: "p", + 5: "s", + 6: "n", + }, + "short": { + 0: "po", + 1: "ut", + 2: "st", + 3: "št", + 4: "pi", + 5: "so", + 6: "ne", + }, + "wide": { + 0: "pondelok", + 1: "utorok", + 2: "streda", + 3: "štvrtok", + 4: "piatok", + 5: "sobota", + 6: "nedeľa", + }, + }, + "months": { + "abbreviated": { + 1: "jan", + 2: "feb", + 3: "mar", + 4: "apr", + 5: "máj", + 6: "jún", + 7: "júl", + 8: "aug", + 9: "sep", + 10: "okt", + 11: "nov", + 12: "dec", + }, + "narrow": { + 1: "j", + 2: "f", + 3: "m", + 4: "a", + 5: "m", + 6: "j", + 7: "j", + 8: "a", + 9: "s", + 10: "o", + 11: "n", + 12: "d", + }, + "wide": { + 1: "januára", + 2: "februára", + 3: "marca", + 4: "apríla", + 5: "mája", + 6: "júna", + 7: "júla", + 8: "augusta", + 9: "septembra", + 10: "októbra", + 11: "novembra", + 12: "decembra", + }, + }, + "units": { + "year": { + "one": "{0} rok", + "few": "{0} roky", + "many": "{0} roka", + "other": "{0} rokov", + }, + "month": { + "one": "{0} mesiac", + "few": "{0} mesiace", + "many": "{0} mesiaca", + "other": "{0} mesiacov", + }, + "week": { + "one": "{0} týždeň", + "few": "{0} týždne", + "many": "{0} týždňa", + "other": "{0} týždňov", + }, + "day": { + "one": "{0} deň", + "few": "{0} dni", + "many": "{0} dňa", + "other": "{0} dní", + }, + "hour": { + "one": "{0} hodina", + "few": "{0} hodiny", + "many": "{0} hodiny", + "other": "{0} hodín", + }, + "minute": { + "one": "{0} minúta", + "few": "{0} minúty", + "many": "{0} minúty", + "other": "{0} minút", + }, + "second": { + "one": "{0} sekunda", + "few": "{0} sekundy", + "many": "{0} sekundy", + "other": "{0} sekúnd", + }, + "microsecond": { + "one": "{0} mikrosekunda", + "few": "{0} mikrosekundy", + "many": "{0} mikrosekundy", + "other": "{0} mikrosekúnd", + }, + }, + "relative": { + "year": { + "future": { + "other": "o {0} rokov", + "one": "o {0} rok", + "few": "o {0} roky", + "many": "o {0} roka", + }, + "past": { + "other": "pred {0} rokmi", + "one": "pred {0} rokom", + "few": "pred {0} rokmi", + "many": "pred {0} roka", + }, + }, + "month": { + "future": { + "other": "o {0} mesiacov", + "one": "o {0} mesiac", + "few": "o {0} mesiace", + "many": "o {0} mesiaca", + }, + "past": { + "other": "pred {0} mesiacmi", + "one": "pred {0} mesiacom", + "few": "pred {0} mesiacmi", + "many": "pred {0} mesiaca", + }, + }, + "week": { + "future": { + "other": "o {0} týždňov", + "one": "o {0} týždeň", + "few": "o {0} týždne", + "many": "o {0} týždňa", + }, + "past": { + "other": "pred {0} týždňami", + "one": "pred {0} týždňom", + "few": "pred {0} týždňami", + "many": "pred {0} týždňa", + }, + }, + "day": { + "future": { + "other": "o {0} dní", + "one": "o {0} deň", + "few": "o {0} dni", + "many": "o {0} dňa", + }, + "past": { + "other": "pred {0} dňami", + "one": "pred {0} dňom", + "few": "pred {0} dňami", + "many": "pred {0} dňa", + }, + }, + "hour": { + "future": { + "other": "o {0} hodín", + "one": "o {0} hodinu", + "few": "o {0} hodiny", + "many": "o {0} hodiny", + }, + "past": { + "other": "pred {0} hodinami", + "one": "pred {0} hodinou", + "few": "pred {0} hodinami", + "many": "pred {0} hodinou", + }, + }, + "minute": { + "future": { + "other": "o {0} minút", + "one": "o {0} minútu", + "few": "o {0} minúty", + "many": "o {0} minúty", + }, + "past": { + "other": "pred {0} minútami", + "one": "pred {0} minútou", + "few": "pred {0} minútami", + "many": "pred {0} minúty", + }, + }, + "second": { + "future": { + "other": "o {0} sekúnd", + "one": "o {0} sekundu", + "few": "o {0} sekundy", + "many": "o {0} sekundy", + }, + "past": { + "other": "pred {0} sekundami", + "one": "pred {0} sekundou", + "few": "pred {0} sekundami", + "many": "pred {0} sekundy", + }, + }, + }, + "day_periods": { + "midnight": "o polnoci", + "am": "AM", + "noon": "napoludnie", + "pm": "PM", + "morning1": "ráno", + "morning2": "dopoludnia", + "afternoon1": "popoludní", + "evening1": "večer", + "night1": "v noci", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/sv/__init__.py b/src/pendulum/locales/sv/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/pendulum/locales/sv/custom.py b/src/pendulum/locales/sv/custom.py new file mode 100644 index 00000000..83f36b13 --- /dev/null +++ b/src/pendulum/locales/sv/custom.py @@ -0,0 +1,22 @@ +""" +sv custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "ago": "{} sedan", + "from_now": "från nu {}", + "after": "{0} efter", + "before": "{0} innan", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "L": "YYYY-MM-DD", + "LL": "D MMMM YYYY", + "LLL": "D MMMM YYYY, HH:mm", + "LLLL": "dddd, D MMMM YYYY, HH:mm", + }, +} diff --git a/src/pendulum/locales/sv/locale.py b/src/pendulum/locales/sv/locale.py new file mode 100644 index 00000000..c3c44726 --- /dev/null +++ b/src/pendulum/locales/sv/locale.py @@ -0,0 +1,230 @@ +from __future__ import annotations + +from pendulum.locales.sv.custom import translations as custom_translations + + +""" +sv locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" + if ((n == n and (n == 1)) and (0 == 0 and (0 == 0))) + else "other", + "ordinal": lambda n: "one" + if ( + ((n % 10) == (n % 10) and (((n % 10) == 1) or ((n % 10) == 2))) + and (not ((n % 100) == (n % 100) and (((n % 100) == 11) or ((n % 100) == 12)))) + ) + else "other", + "translations": { + "days": { + "abbreviated": { + 0: "mån", + 1: "tis", + 2: "ons", + 3: "tors", + 4: "fre", + 5: "lör", + 6: "sön", + }, + "narrow": { + 0: "M", + 1: "T", + 2: "O", + 3: "T", + 4: "F", + 5: "L", + 6: "S", + }, + "short": { + 0: "må", + 1: "ti", + 2: "on", + 3: "to", + 4: "fr", + 5: "lö", + 6: "sö", + }, + "wide": { + 0: "måndag", + 1: "tisdag", + 2: "onsdag", + 3: "torsdag", + 4: "fredag", + 5: "lördag", + 6: "söndag", + }, + }, + "months": { + "abbreviated": { + 1: "jan.", + 2: "feb.", + 3: "mars", + 4: "apr.", + 5: "maj", + 6: "juni", + 7: "juli", + 8: "aug.", + 9: "sep.", + 10: "okt.", + 11: "nov.", + 12: "dec.", + }, + "narrow": { + 1: "J", + 2: "F", + 3: "M", + 4: "A", + 5: "M", + 6: "J", + 7: "J", + 8: "A", + 9: "S", + 10: "O", + 11: "N", + 12: "D", + }, + "wide": { + 1: "januari", + 2: "februari", + 3: "mars", + 4: "april", + 5: "maj", + 6: "juni", + 7: "juli", + 8: "augusti", + 9: "september", + 10: "oktober", + 11: "november", + 12: "december", + }, + }, + "units": { + "year": { + "one": "{0} år", + "other": "{0} år", + }, + "month": { + "one": "{0} månad", + "other": "{0} månader", + }, + "week": { + "one": "{0} vecka", + "other": "{0} veckor", + }, + "day": { + "one": "{0} dygn", + "other": "{0} dygn", + }, + "hour": { + "one": "{0} timme", + "other": "{0} timmar", + }, + "minute": { + "one": "{0} minut", + "other": "{0} minuter", + }, + "second": { + "one": "{0} sekund", + "other": "{0} sekunder", + }, + "microsecond": { + "one": "{0} mikrosekund", + "other": "{0} mikrosekunder", + }, + }, + "relative": { + "year": { + "future": { + "other": "om {0} år", + "one": "om {0} år", + }, + "past": { + "other": "för {0} år sedan", + "one": "för {0} år sedan", + }, + }, + "month": { + "future": { + "other": "om {0} månader", + "one": "om {0} månad", + }, + "past": { + "other": "för {0} månader sedan", + "one": "för {0} månad sedan", + }, + }, + "week": { + "future": { + "other": "om {0} veckor", + "one": "om {0} vecka", + }, + "past": { + "other": "för {0} veckor sedan", + "one": "för {0} vecka sedan", + }, + }, + "day": { + "future": { + "other": "om {0} dagar", + "one": "om {0} dag", + }, + "past": { + "other": "för {0} dagar sedan", + "one": "för {0} dag sedan", + }, + }, + "hour": { + "future": { + "other": "om {0} timmar", + "one": "om {0} timme", + }, + "past": { + "other": "för {0} timmar sedan", + "one": "för {0} timme sedan", + }, + }, + "minute": { + "future": { + "other": "om {0} minuter", + "one": "om {0} minut", + }, + "past": { + "other": "för {0} minuter sedan", + "one": "för {0} minut sedan", + }, + }, + "second": { + "future": { + "other": "om {0} sekunder", + "one": "om {0} sekund", + }, + "past": { + "other": "för {0} sekunder sedan", + "one": "för {0} sekund sedan", + }, + }, + }, + "day_periods": { + "midnight": "midnatt", + "am": "fm", + "pm": "em", + "morning1": "på morgonen", + "morning2": "på förmiddagen", + "afternoon1": "på eftermiddagen", + "evening1": "på kvällen", + "night1": "på natten", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/tr/__init__.py b/src/pendulum/locales/tr/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/pendulum/locales/tr/custom.py b/src/pendulum/locales/tr/custom.py new file mode 100644 index 00000000..b7fd3c7a --- /dev/null +++ b/src/pendulum/locales/tr/custom.py @@ -0,0 +1,24 @@ +""" +tr custom locale file. +""" +from __future__ import annotations + + +translations = { + # Relative time + "ago": "{} önce", + "from_now": "{} içinde", + "after": "{0} sonra", + "before": "{0} önce", + # Ordinals + "ordinal": {"one": ".", "two": ".", "few": ".", "other": "."}, + # Date formats + "date_formats": { + "LTS": "h:mm:ss A", + "LT": "h:mm A", + "L": "MM/DD/YYYY", + "LL": "MMMM D, YYYY", + "LLL": "MMMM D, YYYY h:mm A", + "LLLL": "dddd, MMMM D, YYYY h:mm A", + }, +} diff --git a/src/pendulum/locales/tr/locale.py b/src/pendulum/locales/tr/locale.py new file mode 100644 index 00000000..f0233f21 --- /dev/null +++ b/src/pendulum/locales/tr/locale.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +from pendulum.locales.tr.custom import translations as custom_translations + + +""" +tr locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "one" if (n == n and (n == 1)) else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "Pzt", + 1: "Sal", + 2: "Çar", + 3: "Per", + 4: "Cum", + 5: "Cmt", + 6: "Paz", + }, + "narrow": { + 0: "P", + 1: "S", + 2: "Ç", + 3: "P", + 4: "C", + 5: "C", + 6: "P", + }, + "short": { + 0: "Pt", + 1: "Sa", + 2: "Ça", + 3: "Pe", + 4: "Cu", + 5: "Ct", + 6: "Pa", + }, + "wide": { + 0: "Pazartesi", + 1: "Salı", + 2: "Çarşamba", + 3: "Perşembe", + 4: "Cuma", + 5: "Cumartesi", + 6: "Pazar", + }, + }, + "months": { + "abbreviated": { + 1: "Oca", + 2: "Şub", + 3: "Mar", + 4: "Nis", + 5: "May", + 6: "Haz", + 7: "Tem", + 8: "Ağu", + 9: "Eyl", + 10: "Eki", + 11: "Kas", + 12: "Ara", + }, + "narrow": { + 1: "O", + 2: "Ş", + 3: "M", + 4: "N", + 5: "M", + 6: "H", + 7: "T", + 8: "A", + 9: "E", + 10: "E", + 11: "K", + 12: "A", + }, + "wide": { + 1: "Ocak", + 2: "Şubat", + 3: "Mart", + 4: "Nisan", + 5: "Mayıs", + 6: "Haziran", + 7: "Temmuz", + 8: "Ağustos", + 9: "Eylül", + 10: "Ekim", + 11: "Kasım", + 12: "Aralık", + }, + }, + "units": { + "year": { + "one": "{0} yıl", + "other": "{0} yıl", + }, + "month": { + "one": "{0} ay", + "other": "{0} ay", + }, + "week": { + "one": "{0} hafta", + "other": "{0} hafta", + }, + "day": { + "one": "{0} gün", + "other": "{0} gün", + }, + "hour": { + "one": "{0} saat", + "other": "{0} saat", + }, + "minute": { + "one": "{0} dakika", + "other": "{0} dakika", + }, + "second": { + "one": "{0} saniye", + "other": "{0} saniye", + }, + "microsecond": { + "one": "{0} mikrosaniye", + "other": "{0} mikrosaniye", + }, + }, + "relative": { + "year": { + "future": { + "other": "{0} yıl sonra", + "one": "{0} yıl sonra", + }, + "past": { + "other": "{0} yıl önce", + "one": "{0} yıl önce", + }, + }, + "month": { + "future": { + "other": "{0} ay sonra", + "one": "{0} ay sonra", + }, + "past": { + "other": "{0} ay önce", + "one": "{0} ay önce", + }, + }, + "week": { + "future": { + "other": "{0} hafta sonra", + "one": "{0} hafta sonra", + }, + "past": { + "other": "{0} hafta önce", + "one": "{0} hafta önce", + }, + }, + "day": { + "future": { + "other": "{0} gün sonra", + "one": "{0} gün sonra", + }, + "past": { + "other": "{0} gün önce", + "one": "{0} gün önce", + }, + }, + "hour": { + "future": { + "other": "{0} saat sonra", + "one": "{0} saat sonra", + }, + "past": { + "other": "{0} saat önce", + "one": "{0} saat önce", + }, + }, + "minute": { + "future": { + "other": "{0} dakika sonra", + "one": "{0} dakika sonra", + }, + "past": { + "other": "{0} dakika önce", + "one": "{0} dakika önce", + }, + }, + "second": { + "future": { + "other": "{0} saniye sonra", + "one": "{0} saniye sonra", + }, + "past": { + "other": "{0} saniye önce", + "one": "{0} saniye önce", + }, + }, + }, + "day_periods": { + "midnight": "gece yarısı", + "am": "ÖÖ", + "noon": "öğle", + "pm": "ÖS", + "morning1": "sabah", + "morning2": "öğleden önce", + "afternoon1": "öğleden sonra", + "afternoon2": "akşamüstü", + "evening1": "akşam", + "night1": "gece", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/ua/__init__.py b/src/pendulum/locales/ua/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/pendulum/locales/ua/custom.py b/src/pendulum/locales/ua/custom.py new file mode 100644 index 00000000..ea96d8d0 --- /dev/null +++ b/src/pendulum/locales/ua/custom.py @@ -0,0 +1,23 @@ +""" +ua custom locale file. +""" +from __future__ import annotations + + +translations = { + "units": {"few_second": "кілька секунд"}, + # Relative time + "ago": "{} тому", + "from_now": "за {}", + "after": "{0} посіля", + "before": "{0} до", + # Date formats + "date_formats": { + "LTS": "HH:mm:ss", + "LT": "HH:mm", + "L": "DD.MM.YYYY", + "LL": "D MMMM YYYY р.", + "LLL": "D MMMM YYYY р., HH:mm", + "LLLL": "dddd, D MMMM YYYY р., HH:mm", + }, +} diff --git a/src/pendulum/locales/ua/locale.py b/src/pendulum/locales/ua/locale.py new file mode 100644 index 00000000..b7435832 --- /dev/null +++ b/src/pendulum/locales/ua/locale.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +from pendulum.locales.ua.custom import translations as custom_translations + + +""" +ua locale file. + +It has been generated automatically and must not be modified directly. +""" + + +locale = { + "plural": lambda n: "few" + if ( + ( + (0 == 0 and (0 == 0)) + and ((n % 10) == (n % 10) and ((n % 10) >= 2 and (n % 10) <= 4)) + ) + and (not ((n % 100) == (n % 100) and ((n % 100) >= 12 and (n % 100) <= 14))) + ) + else "many" + if ( + ( + ((0 == 0 and (0 == 0)) and ((n % 10) == (n % 10) and ((n % 10) == 0))) + or ( + (0 == 0 and (0 == 0)) + and ((n % 10) == (n % 10) and ((n % 10) >= 5 and (n % 10) <= 9)) + ) + ) + or ( + (0 == 0 and (0 == 0)) + and ((n % 100) == (n % 100) and ((n % 100) >= 11 and (n % 100) <= 14)) + ) + ) + else "one" + if ( + ((0 == 0 and (0 == 0)) and ((n % 10) == (n % 10) and ((n % 10) == 1))) + and (not ((n % 100) == (n % 100) and ((n % 100) == 11))) + ) + else "other", + "ordinal": lambda n: "other", + "translations": { + "days": { + "abbreviated": { + 0: "пн", + 1: "вт", + 2: "ср", + 3: "чт", + 4: "пт", + 5: "сб", + 6: "нд", + }, + "narrow": {0: "пн", 1: "вт", 2: "ср", 3: "чт", 4: "пт", 5: "сб", 6: "нд"}, + "short": {0: "пн", 1: "вт", 2: "ср", 3: "чт", 4: "пт", 5: "сб", 6: "нд"}, + "wide": { + 0: "понеділок", + 1: "вівторок", + 2: "середа", + 3: "четвер", + 4: "п'ятниця", + 5: "субота", + 6: "неділя", + }, + }, + "months": { + "abbreviated": { + 1: "січ.", + 2: "лют.", + 3: "бер.", + 4: "квіт.", + 5: "трав.", + 6: "черв.", + 7: "лип.", + 8: "серп.", + 9: "вер.", + 10: "жовт.", + 11: "лист.", + 12: "груд.", + }, + "narrow": { + 1: "С", + 2: "Л", + 3: "Б", + 4: "К", + 5: "Т", + 6: "Ч", + 7: "Л", + 8: "С", + 9: "В", + 10: "Ж", + 11: "Л", + 12: "Г", + }, + "wide": { + 1: "січня", + 2: "лютого", + 3: "березня", + 4: "квітня", + 5: "травня", + 6: "червня", + 7: "липня", + 8: "серпня", + 9: "вересня", + 10: "жовтня", + 11: "листопада", + 12: "грудня", + }, + }, + "units": { + "year": { + "one": "{0} рік", + "few": "{0} роки", + "many": "{0} років", + "other": "{0} роки", + }, + "month": { + "one": "{0} місяць", + "few": "{0} місяця", + "many": "{0} місяців", + "other": "{0} місяці", + }, + "week": { + "one": "{0} тиждень", + "few": "{0} тижня", + "many": "{0} тижнів", + "other": "{0} тижні", + }, + "day": { + "one": "{0} день", + "few": "{0} дні", + "many": "{0} днів", + "other": "{0} дні", + }, + "hour": { + "one": "{0} година", + "few": "{0} години", + "many": "{0} годин", + "other": "{0} години", + }, + "minute": { + "one": "{0} хвилина", + "few": "{0} хвилини", + "many": "{0} хвилин", + "other": "{0} хвилини", + }, + "second": { + "one": "{0} секунда", + "few": "{0} секунди", + "many": "{0} секунд", + "other": "{0} секунди", + }, + "microsecond": { + "one": "{0} мікросекунда", + "few": "{0} мікросекунди", + "many": "{0} мікросекунд", + "other": "{0} мікросекунд", + }, + }, + "relative": { + "year": { + "future": { + "other": "за {0} роки", + "one": "за {0} рік", + "few": "за {0} роки", + "many": "за {0} років", + }, + "past": { + "other": "{0} роки тому", + "one": "{0} рік тому", + "few": "{0} роки тому", + "many": "{0} років тому", + }, + }, + "month": { + "future": { + "other": "за {0} місяці", + "one": "за {0} місяць", + "few": "за {0} місяця", + "many": "за {0} місяців", + }, + "past": { + "other": "{0} місяці тому", + "one": "{0} місяц тому", + "few": "{0} місяця тому", + "many": "{0} місяців тому", + }, + }, + "week": { + "future": { + "other": "за {0} тижні", + "one": "за {0} тиждень", + "few": "за {0} тижня", + "many": "за {0} тижднів", + }, + "past": { + "other": "{0} тижні тому", + "one": "{0} тиждень тому", + "few": "{0} тижня тому", + "many": "{0} тижнів тому", + }, + }, + "day": { + "future": { + "other": "за {0} дні", + "one": "за {0} день", + "few": "за {0} дні", + "many": "за {0} днів", + }, + "past": { + "other": "{0} дні тому", + "one": "{0} день тому", + "few": "{0} дні тому", + "many": "{0} днів тому", + }, + }, + "hour": { + "future": { + "other": "за {0} години", + "one": "за {0} година", + "few": "за {0} години", + "many": "за {0} годин", + }, + "past": { + "other": "{0} години тому", + "one": "{0} година тому", + "few": "{0} години тому", + "many": "{0} годин тому", + }, + }, + "minute": { + "future": { + "other": "за {0} хвилини", + "one": "за {0} хвилина", + "few": "за {0} хвилини", + "many": "за {0} хвилин", + }, + "past": { + "other": "{0} хвилини тому", + "one": "{0} хвилина тому", + "few": "{0} хвилини тому", + "many": "{0} хвилин тому", + }, + }, + "second": { + "future": { + "other": "за {0} секунди", + "one": "за {0} секунду", + "few": "за {0} секунди", + "many": "за {0} секунд", + }, + "past": { + "other": "{0} секунди тому", + "one": "{0} секунду тому", + "few": "{0} секунди тому", + "many": "{0} секунд тому", + }, + }, + }, + "day_periods": { + "midnight": "опівночі", + "am": "AM", + "noon": "полудень", + "pm": "PM", + "morning1": "ранку", + "morning2": "до півдня", + "afternoon1": "дня", + "afternoon2": "пополуднє", + "evening1": "ввечері", + "evening2": "увечері", + "night1": "в ніч", + }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, + }, + "custom": custom_translations, +} diff --git a/src/pendulum/locales/zh/__init__.py b/src/pendulum/locales/zh/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pendulum/locales/zh/custom.py b/src/pendulum/locales/zh/custom.py similarity index 85% rename from pendulum/locales/zh/custom.py rename to src/pendulum/locales/zh/custom.py index eb9bb810..cf47a403 100644 --- a/pendulum/locales/zh/custom.py +++ b/src/pendulum/locales/zh/custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - - """ zh custom locale file. """ +from __future__ import annotations + translations = { # Relative time diff --git a/pendulum/locales/zh/locale.py b/src/pendulum/locales/zh/locale.py similarity index 76% rename from pendulum/locales/zh/locale.py rename to src/pendulum/locales/zh/locale.py index f292924e..eb85ab7e 100644 --- a/pendulum/locales/zh/locale.py +++ b/src/pendulum/locales/zh/locale.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from __future__ import annotations -from .custom import translations as custom_translations +from pendulum.locales.zh.custom import translations as custom_translations """ @@ -17,24 +16,24 @@ "translations": { "days": { "abbreviated": { - 0: "周日", - 1: "周一", - 2: "周二", - 3: "周三", - 4: "周四", - 5: "周五", - 6: "周六", + 0: "周一", + 1: "周二", + 2: "周三", + 3: "周四", + 4: "周五", + 5: "周六", + 6: "周日", }, - "narrow": {0: "日", 1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六"}, - "short": {0: "周日", 1: "周一", 2: "周二", 3: "周三", 4: "周四", 5: "周五", 6: "周六"}, + "narrow": {0: "一", 1: "二", 2: "三", 3: "四", 4: "五", 5: "六", 6: "日"}, + "short": {0: "周一", 1: "周二", 2: "周三", 3: "周四", 4: "周五", 5: "周六", 6: "周日"}, "wide": { - 0: "星期日", - 1: "星期一", - 2: "星期二", - 3: "星期三", - 4: "星期四", - 5: "星期五", - 6: "星期六", + 0: "星期一", + 1: "星期二", + 2: "星期三", + 3: "星期四", + 4: "星期五", + 5: "星期六", + 6: "星期日", }, }, "months": { @@ -111,6 +110,12 @@ "evening1": "晚上", "night1": "凌晨", }, + "week_data": { + "min_days": 1, + "first_day": 0, + "weekend_start": 5, + "weekend_end": 6, + }, }, "custom": custom_translations, } diff --git a/src/pendulum/mixins/__init__.py b/src/pendulum/mixins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pendulum/mixins/default.py b/src/pendulum/mixins/default.py similarity index 54% rename from pendulum/mixins/default.py rename to src/pendulum/mixins/default.py index fdb05b0f..f2531f3d 100644 --- a/pendulum/mixins/default.py +++ b/src/pendulum/mixins/default.py @@ -1,36 +1,30 @@ -from ..formatting import Formatter +from __future__ import annotations +from pendulum.formatting import Formatter -_formatter = Formatter() +_formatter = Formatter() -class FormattableMixin(object): - _formatter = _formatter +class FormattableMixin: + _formatter: Formatter = _formatter - def format(self, fmt, locale=None): + def format(self, fmt: str, locale: str | None = None) -> str: """ Formats the instance using the given format. :param fmt: The format to use - :type fmt: str - :param locale: The locale to use - :type locale: str or None - - :rtype: str """ return self._formatter.format(self, fmt, locale) - def for_json(self): + def for_json(self) -> str: """ - Methods for automatic json serialization by simplejson - - :rtype: str + Methods for automatic json serialization by simplejson. """ - return str(self) + return self.isoformat() - def __format__(self, format_spec): + def __format__(self, format_spec: str) -> str: if len(format_spec) > 0: if "%" in format_spec: return self.strftime(format_spec) @@ -39,5 +33,5 @@ def __format__(self, format_spec): return str(self) - def __str__(self): + def __str__(self) -> str: return self.isoformat() diff --git a/pendulum/parser.py b/src/pendulum/parser.py similarity index 61% rename from pendulum/parser.py rename to src/pendulum/parser.py index 0df76161..833bae3c 100644 --- a/pendulum/parser.py +++ b/src/pendulum/parser.py @@ -1,46 +1,52 @@ -from __future__ import absolute_import +from __future__ import annotations import datetime -import typing +import os +import typing as t import pendulum -from .date import Date -from .datetime import DateTime -from .parsing import _Interval -from .parsing import parse as base_parse -from .time import Duration -from .time import Time -from .tz import UTC +from pendulum.duration import Duration +from pendulum.parsing import _Interval +from pendulum.parsing import parse as base_parse +from pendulum.tz.timezone import UTC +if t.TYPE_CHECKING: + from pendulum.date import Date + from pendulum.datetime import DateTime + from pendulum.interval import Interval + from pendulum.time import Time + +with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" + try: - from .parsing._iso8601 import Duration as CDuration + if not with_extensions: + raise ImportError() + + from pendulum._pendulum import Duration as RustDuration except ImportError: - CDuration = None + RustDuration = None # type: ignore[assignment,misc] -def parse( - text, **options -): # type: (str, **typing.Any) -> typing.Union[Date, Time, DateTime, Duration] +def parse(text: str, **options: t.Any) -> Date | Time | DateTime | Duration: # Use the mock now value if it exists - options["now"] = options.get("now", pendulum.get_test_now()) + options["now"] = options.get("now") return _parse(text, **options) -def _parse(text, **options): +def _parse( + text: str, **options: t.Any +) -> Date | DateTime | Time | Duration | Interval[DateTime]: """ Parses a string with the given options. :param text: The string to parse. - :type text: str - - :rtype: mixed """ # Handling special cases if text == "now": - return pendulum.now() + return pendulum.now(tz=options.get("tz", UTC)) parsed = base_parse(text, **options) @@ -71,7 +77,7 @@ def _parse(text, **options): if parsed.start is not None: dt = pendulum.instance(parsed.start, tz=options.get("tz", UTC)) - return pendulum.period( + return pendulum.interval( dt, dt.add( years=duration.years, @@ -85,9 +91,11 @@ def _parse(text, **options): ), ) - dt = pendulum.instance(parsed.end, tz=options.get("tz", UTC)) + dt = pendulum.instance( + t.cast("datetime.datetime", parsed.end), tz=options.get("tz", UTC) + ) - return pendulum.period( + return pendulum.interval( dt.subtract( years=duration.years, months=duration.months, @@ -101,12 +109,19 @@ def _parse(text, **options): dt, ) - return pendulum.period( - pendulum.instance(parsed.start, tz=options.get("tz", UTC)), - pendulum.instance(parsed.end, tz=options.get("tz", UTC)), + return pendulum.interval( + pendulum.instance( + t.cast("datetime.datetime", parsed.start), tz=options.get("tz", UTC) + ), + pendulum.instance( + t.cast("datetime.datetime", parsed.end), tz=options.get("tz", UTC) + ), ) - if CDuration and isinstance(parsed, CDuration): + if isinstance(parsed, Duration): + return parsed + + if RustDuration is not None and isinstance(parsed, RustDuration): return pendulum.duration( years=parsed.years, months=parsed.months, @@ -118,4 +133,4 @@ def _parse(text, **options): microseconds=parsed.microseconds, ) - return parsed + raise NotImplementedError diff --git a/pendulum/parsing/__init__.py b/src/pendulum/parsing/__init__.py similarity index 67% rename from pendulum/parsing/__init__.py rename to src/pendulum/parsing/__init__.py index 1b5c8b91..20ebed40 100644 --- a/pendulum/parsing/__init__.py +++ b/src/pendulum/parsing/__init__.py @@ -1,30 +1,36 @@ +from __future__ import annotations + +import contextlib import copy import os import re -import struct from datetime import date from datetime import datetime from datetime import time +from typing import Any +from typing import cast from dateutil import parser -from .exceptions import ParserError +from pendulum.parsing.exceptions import ParserError with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" try: - if not with_extensions or struct.calcsize("P") == 4: + if not with_extensions: raise ImportError() - from ._iso8601 import parse_iso8601 + from pendulum._pendulum import Duration + from pendulum._pendulum import parse_iso8601 except ImportError: - from .iso8601 import parse_iso8601 + from pendulum.duration import Duration # type: ignore[assignment] # noqa: TC001 + from pendulum.parsing.iso8601 import parse_iso8601 # type: ignore[assignment] COMMON = re.compile( - # Date (optional) + # Date (optional) # noqa: ERA001 "^" "(?P" " (?P" # Classic date (YYYY-MM-DD) @@ -35,10 +41,11 @@ " )?" " )" ")?" - # Time (optional) + # Time (optional) # noqa: ERA001 "(?P