pax_global_header00006660000000000000000000000064150706603330014515gustar00rootroot0000000000000052 comment=61cc6eb2b2c493c0744af546dbe4d3b6cc4dbdc1 croniter-6.1.0rc1/000077500000000000000000000000001507066033300137545ustar00rootroot00000000000000croniter-6.1.0rc1/.dockerignore000066400000000000000000000001041507066033300164230ustar00rootroot00000000000000Dockerfile docker-compose* .git /dist/ /.tox/ /.idea/ /.coverage croniter-6.1.0rc1/.github/000077500000000000000000000000001507066033300153145ustar00rootroot00000000000000croniter-6.1.0rc1/.github/workflows/000077500000000000000000000000001507066033300173515ustar00rootroot00000000000000croniter-6.1.0rc1/.github/workflows/cicd.yml000066400000000000000000000043121507066033300207760ustar00rootroot00000000000000name: CI/CD on: push: branches: - main pull_request: workflow_dispatch: schedule: [{cron: '1 0 * * 6'}] env: DOCKER_BUILDKIT: "1" COMPOSE_DOCKER_CLI_BUILD: "1" BUILDKIT_PROGRESS: "plain" RELEASABLE_REPOS: "^pallets-eco/croniter" RELEASABLE_BRANCHES: "^(refs/heads/)?(master|main|new-packaging)$" FORCE_COLOR: "true" jobs: test-code-QA: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.13"] steps: - uses: actions/checkout@v4 - name: Cache tox environments uses: actions/cache@v4 with: path: .tox key: test-code-QA-${{ runner.os }}-${{ matrix.python }}-toxQA-${{ hashFiles('**/pyproject.toml') }} - name: Cache uv uses: actions/cache@v4 with: path: ~/.cache/uv key: test-code-QA-${{ runner.os }}-${{ matrix.python }}-uvQA-${{ hashFiles('**/pyproject.toml') }} - name: Install uv and tox shell: sh run: | curl -LsSf https://astral.sh/uv/install.sh | sh uv tool install tox - name: formatters check run: tox -e lint,mypy,fmt test-py3: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Cache tox environments uses: actions/cache@v4 with: path: .tox key: test-py3-${{ runner.os }}-${{ matrix.python }}-tox-${{ hashFiles('**/pyproject.toml') }} - name: Cache uv uses: actions/cache@v4 with: path: ~/.cache/uv key: test-py3-${{ runner.os }}-${{ matrix.python }}-uvQA-${{ hashFiles('**/pyproject.toml') }} - name: Install uv and tox shell: sh run: | curl -LsSf https://astral.sh/uv/install.sh | sh uv tool install tox - name: run tests with coverage run: tox -e cov test-32bits: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Test with pytest run: | docker compose run --build --rm --env FORCE_COLOR=true --env UV_LINK_MODE=copy app tox -e test env: COMPOSE_FILE: "docker-compose.yml:docker-compose-build.yml:docker-compose-32bits.yml" croniter-6.1.0rc1/.github/workflows/dockerimages.yml000066400000000000000000000032601507066033300225320ustar00rootroot00000000000000name: docker images builder on: push: {branches: [master, docker]} # pull_request: # workflow_dispatch: schedule: [{cron: '1 0 * * 6'}] env: DOCKER_BUILDKIT: "1" COMPOSE_DOCKER_CLI_BUILD: "1" BUILDKIT_PROGRESS: "plain" RELEASABLE_REPOS: "^pallets-eco/croniter" RELEASABLE_BRANCHES: "^(refs/heads/)?(master|main|new-packaging)$" jobs: docker-build: runs-on: ubuntu-24.04 strategy: max-parallel: 5 fail-fast: false matrix: FLAVOR: [32bits, latest] env: {FLAVOR: "${{matrix.FLAVOR}}"} steps: - name: Set vars run: |- if ( echo "$GITHUB_REF" | egrep -q "${RELEASABLE_BRANCHES}" ) \ && ( echo "$GITHUB_REPOSITORY" | egrep -q "${RELEASABLE_REPOS}" ) then releasable=true;else releasable=false;fi echo "releasable=$releasable" >> $GITHUB_OUTPUT id: v - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - uses: actions/checkout@v4 - name: Build run: | set -ex export COMPOSE_FILE="docker-compose.yml:docker-compose-build.yml" if ( echo "${{matrix.FLAVOR}}" | grep -q 32 );then export COMPOSE_FILE="${COMPOSE_FILE}:docker-compose-32bits.yml:docker-compose-build-32bits.yml" fi echo ${COMPOSE_FILE} docker compose build - name: test run: set -ex && docker compose run --rm app tox --current-env - name: Release run: | if [ "x${{steps.v.outputs.releasable}}" = "xtrue" ];then set -ex && docker push pallets-eco/croniter:${{matrix.FLAVOR}} fi croniter-6.1.0rc1/.github/workflows/publish-to-test-pypi.yml000066400000000000000000000037701507066033300241250ustar00rootroot00000000000000name: Publish Python 🐍 distribution πŸ“¦ to PyPI and TestPyPI on: push jobs: build: name: Build distribution πŸ“¦ runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.9" - name: Install pypa/build run: >- python3 -m pip install build --user - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ publish-to-pypi: name: >- Publish Python 🐍 distribution πŸ“¦ to PyPI if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes needs: - build runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/croniter permissions: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution πŸ“¦ to PyPI uses: pypa/gh-action-pypi-publish@release/v1 publish-to-testpypi: name: Publish Python 🐍 distribution πŸ“¦ to TestPyPI needs: - build runs-on: ubuntu-latest environment: name: testpypi url: https://test.pypi.org/p/croniter permissions: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution πŸ“¦ to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ croniter-6.1.0rc1/.gitignore000066400000000000000000000001771507066033300157510ustar00rootroot00000000000000/.*tox .env /dist \.*.cfg *.egg-info *.pyc *.swp /build /venv /.cache .idea *.py,cover .coverage /venv* .python-version /local croniter-6.1.0rc1/CHANGELOG.rst000066400000000000000000000402171507066033300160010ustar00rootroot00000000000000Changelog ============== 6.1.0rc1 (2025-10-10) ------------------ Features and Improvements ~~~~~~~~~~~~~~~~~~~~~~~~ - Support for `zoneinfo` timezones. [b4e7295, Benjamin Drung (@bdrung)] - Add type hints to `croniter.__init__`, `timestamp_to_datetime`, and property initializations. [b171ea9, 257741e, c4f5e44, Benjamin Drung (@bdrung)] - Simplify code for `max_years_between_matches`. [e065efa, Benjamin Drung (@bdrung)] - Extend type check for `hash_id` to empty str/bytes. [6fe1d43, Benjamin Drung (@bdrung)] - Drop unused `_get_next_nearest` and `_get_prev_nearest`. [d7dab3a, Benjamin Drung (@bdrung)] - Reduce line length to 99 and unfold long Python code lines. [ecea402, f38d5f4, Benjamin Drung (@bdrung)] Bugfixes ~~~~~~~~ - Fix memory leak by removing `TIMESTAMP_TO_DT_CACHE` global dict cache. [1a9d3c0, RafaΕ‚ Safin (@rafsaf)] - Fix default value of `second_at_beginning` to a boolean. [dc63ed2, Benjamin Drung (@bdrung)] - Fix always missing the timestamp to datetime cache. [18eb299, Benjamin Drung (@bdrung)] - Fix skipping first of March. [de21a9d, Benjamin Drung (@bdrung)] - Fix DST handling by rewriting the DST logic. [f48387b, Benjamin Drung (@bdrung)] - Fix all flake8 complaints. [08c1f83, Benjamin Drung (@bdrung)] Testing and Documentation ~~~~~~~~~~~~~~~~~~~~~~~~~ - Test: fix date in DST test. [871e391, Benjamin Drung (@bdrung)] - Test: document time jumps in DST test cases. [2d72258, Benjamin Drung (@bdrung)] - Test: use isoformat() to compare dates with timezone information. [1dd3562, Benjamin Drung (@bdrung)] Other ~~~~~ - Announce back that croniter is maintained now as part of pallets-eco. [Jarek Potiuk (@jarekpotiuk)] 6.0.0 (2024-12-17) ------------------ - Announce for now that croniter dev is ended (CRA). - Rework timestamp_to_datetime to use whatever timezone [kiorky] - Make datetime_to_timestamp & timestamp_to_datetime public [kiorky] - Fix EPOCH calculation in case of non UTC & 32 bits based systems [kiorky] - Apply isort formatter [kiorky] - Reintegrate test_speed [kiorky] - Apply black formatter [evanpurkhiser, kiorky] - Code quality changes [evanpurkhiser, kiorky] - Remove unused _get_caller_globals_and_locals [evanpurkhiser] - Remove single-use bad_length [evanpurkhiser] - Remove unused `days` in `proc_month` [evanpurkhiser] - Use `field_index` over `i` for readability [evanpurkhiser] - Always use `"""` for docstrings [evanpurkhiser] - Make helper instance methods that do not use self static [evanpurkhiser] - Remove unusd call to sys.exc_info [evanpurkhiser] - Remove unused `ALPHAS` [evanpurkhiser] - Improve `croniter.expand` documentation [evanpurkhiser] 5.0.1 (2024-10-29) ------------------ - Community wanted: Reintroduce 7 as DayOfWeek in deviation from standard cron (#90). [kiorky] 4.0.0 (2024-10-28) ------------------ - Remove DayOfWeek alias 7 to DayOfWeek 0 to stick to standard cron (#90). [kiorky] - Fix DOW ranges calculations when lastday is a Sunday. [kiorky] 3.0.4 (2024-10-25) ------------------ - Fix overflow on 32bits systems (#87) [kiorky] - Fix python2 testing (related to #93) [kiorky] - Modernize packaging. Special thanks to Aarni Koskela (akx) for all the inputs. [kiorky, akx] 3.0.3 (2024-07-26) ------------------ - fix lint [kiorky] 3.0.2 (2024-07-26) ------------------ - Fix start_time not respected in get_next/get_prev/all_next/all_prev (#86) [hesstobi, kiorky] 3.0.1 (2024-07-25) ------------------ - Add an `update_current` argument to get_next/get_prev/all_next/all_prev to facilitate writing of some downstream code, see #83. [kiorky] 3.0.0 (2024-07-23) ------------------ - Support for year field [zhouyizhen, kiorky] - Better support for 6 fields (second), and 7 fields crons [zhouyizhen, kiorky] - Better fix hashed expressions omitting some entries (#82, #42, #30) fix is retained over #42 initial fix [zhouyizhen, kiorky] - Ensure match return false when not time available (#81) [zhouyizhen, kiorky] 2.0.7 (2024-07-16) ------------------ - fix doc 2.0.6 (2024-07-16) ------------------ - Implement second_at_beginning [zhouyizhen, kiorky] - Support question mark as wildcard [zhouyizhen, kiorky] - Support to start a cron from a reference start time [mghextreme, kiorky] 2.0.5 (2024-04-20) ------------------ - No changes, fix lint [kiorky] 2.0.4 (2024-04-20) ------------------ - Support hashid strings in is_valid [george-kuanli-peng, kiorky] - Avoid over-optimization in crontab expansions [Cherie0125, liqirui , kiorky] 2.0.3 (2024-03-19) ------------------ - Add match_range function [salitaba] 2.0.2 (2024-02-29) ------------------ - fix leap year (29 days in February) [zed2015] 2.0.1 (2023-10-11) ------------------ - Fix release issue [kiorky] 2.0.0 (2023-10-10) ------------------ - Add Python 3.12 support [rafsaf] - Make major release instructions [kiorky] 1.4.1 (2023-06-15) ------------------ - Make a retrocompatible version of 1.4.0 change about supporting VIXIECRON bug. (fix #47) [kiorky] 1.4.0 (2023-06-15) ------------------ - Added "implement_cron_bug" flag to make the cron parser compatible with a bug in Vixie/ISC Cron [kiorky, David White ] *WARNING*: EXPAND METHOD CHANGES RETURN VALUE 1.3.15 (2023-05-25) ------------------- - Fix hashed expressions omitting some entries [@waltervos/Walter Vos ] - Enhance .match() precision for 6 position expressions [@szpol/szymon ] 1.3.14 (2023-04-12) ------------------- - Lint 1.3.13 (2023-04-12) ------------------- - Add check for range begin/end 1.3.12 (2023-04-12) ------------------- - restore py2 compat 1.3.11 (2023-04-12) ------------------- - Do not expose `i` into global namespace 1.3.10 (2023-04-07) ------------------- - Fix DOW hash parsing [kiorky] - better error handling on py3 [kiorky] 1.3.8 (2022-11-22) ------------------ - Add Python 3.11 support and move docs files to main folder [rafsaf] 1.3.7 (2022-09-06) ------------------ - fix tests - Fix croniter_range infinite loop [Shachar Snapiri ] 1.3.5 (2022-05-14) ------------------ - Add Python 3.10 support [eelkevdbos] 1.3.4 (2022-02-18) ------------------ - Really fix compat for tests under py27 [kiorky] 1.3.3 (2022-02-18) ------------------ - Fix compat for tests under py27 [kiorky] 1.3.2 (2022-02-18) ------------------ - Fix #12: regressions with set_current [kiorky, agateblue] 1.3.1 (2022-02-15) ------------------ - Restore compat with python2 [kiorky] 1.3.0 (2022-02-15) ------------------ - Add a way to make next() easier to use. This fixes #11 [kiorky] 1.2.0 (2022-01-14) ------------------ - Enforce validation for day=1. Before this release we used to support day=0 and it was silently glided to day=1 to support having both day in day in 4th field when it came to have 6fields cron forms (second repeat). It will now raises a CroniterBadDateError. See https://github.com/kiorky/croniter/issues/6 [kiorky] 1.1.0 (2021-12-03) ------------------ - Enforce validation for month=1. Before this release we used to support month=0 and it was silently glided to month=1 to support having both day in month in 4th field when it came to have 6fields cron forms (second repeat). It will now raises a CroniterBadDateError. See https://github.com/kiorky/croniter/issues/6 [kiorky] 1.0.15 (2021-06-25) ------------------- - restore py2 [kiorky] 1.0.14 (2021-06-25) ------------------- - better type checks [kiorky] 1.0.13 (2021-05-06) ------------------- - Fix ZeroDivisionError with ``* * R/0 * *`` [cuu508] 1.0.12 (2021-04-13) ------------------- - Add support for hashed/random/keyword expressions Ryan Finnie (rfinnie) - Review support support for hashed/random/keyword expression and add expanders reactor [ kiorky ] 1.0.11 (2021-04-07) ------------------- - fix bug: bad case:``0 6 30 3 *`` [zed2015(zhangchi)] - Add support for ``L`` in the day_of_week component. This enable expressions like ``* * * * L4``, which means last Thursday of the month. This resolves #159. [Kintyre] - Create ``CroniterUnsupportedSyntaxError`` exception for situations where CRON syntax may be valid but some combinations of features is not supported. Currently, this is used when the ``day_of_week`` component has a combination of literal values and nth/last syntax at the same time. For example, ``0 0 * * 1,L6`` or ``0 0 * * 15,sat#1`` will both raise this exception because of mixing literal days of the week with nth-weekday or last-weekday syntax. This *may* impact existing cron expressions in prior releases, because ``0 0 * * 15,sat#1`` was previously allowed but incorrectly handled. [Kintyre] - Update ``croniter_range()`` to allow an alternate ``croniter`` class to be used. Helpful when using a custom class derived from croniter. [Kintyre] 1.0.10 (2021-03-25) ------------------- - Remove external library ``natsort``. Sorting of cron expression components now handled with ``sorted()`` with a custom ``key`` function. [Kintyre] 1.0.9 (2021-03-23) ------------------ - Remove futures dependency [kiorky] 1.0.8 (2021-03-06) ------------------ - Update `_expand` to lowercase each component of the expression. This is in relation to #157. With this change, croniter accepts and correctly handles `* * 10-L * *`. [cuu508] 1.0.7 (2021-03-02) ------------------ - Fix _expand to reject int literals with underscores [cuu508] - Remove a debug statement to make flake8 happy [cuu508] 1.0.6 (2021-02-01) ------------------ - Fix combination of star and invalid expression bugs [kiorky] 1.0.5 (2021-01-29) ------------------ - Security fix: fix overflow when using cron ranges [kiorky] 1.0.4 (2021-01-29) ------------------ - Spelling fix release 1.0.3 (2021-01-29) ------------------ - Fix #155: raise CroniterBadCronError when error syntax [kiorky] 1.0.2 (2021-01-19) ------------------ - Fix match when datetime has microseconds [kiorky] 1.0.1 (2021-01-06) ------------------ - no changes, just to make sense with new semver2 (making croniter on a stable state) [kiorky] 0.3.37 (2020-12-31) ------------------- - Added Python 3.8 and 3.9 support [eumiro] 0.3.36 (2020-11-02) ------------------- - Updated docs section regarding ``max_years_between_matches`` to be more shorter and hopefully more relevant. [Kintyre] - Don't install tests [scop] 0.3.35 (2020-10-11) ------------------- - Handle L in ranges. This fixes #142. [kiorky] - Add a new initialization parameter ``max_years_between_matches`` to support finding the next/previous date beyond the default 1 year window, if so desired. Updated README to include additional notes and example of this usage. Fixes #145. [Kintyre] - The ``croniter_range()`` function was updated to automatically determines the appropriate ``max_years_between_matches`` value, this preventing handling of the ``CroniterBadDateError`` exception. [Kintyre] - Updated exception handling classes: ``CroniterBadDateError`` now only applies during date finding operations (next/prev), and all parsing errors can now be caught using ``CroniterBadCronError``. The ``CroniterNotAlphaError`` exception is now a subclass of ``CroniterBadCronError``. A brief description of each exception class was added as an inline docstring. [Kintyre] - Updated iterable interfaces to replace the ``CroniterBadDateError`` with ``StopIteration`` if (and only if) the ``max_years_between_matches`` argument is provided. The rationale here is that if the user has specified the max tolerance between matches, then there's no need to further inform them of no additional matches. Just stop the iteration. This also keeps backwards compatibility. [Kintyre] - Minor docs update [Kintyre] 0.3.34 (2020-06-19) ------------------- - Feat ``croniter_range(start, stop, cron)`` [Kintyre] - Optimization for poorly written cron expression [Kintyre] 0.3.33 (2020-06-15) ------------------- - Make dateutil tz support more official [Kintyre] - Feat/support for day or [田口俑元] 0.3.32 (2020-05-27) ------------------- - document seconds repeats, fixes #122 [kiorky] - Implement match method, fixes #54 [kiorky] - Adding tests for #127 (test more DSTs and croniter behavior around) [kiorky] - Changed lag_hours comparison to absolute to manage dst boundary when getting previous [Sokkka] 0.3.31 (2020-01-02) ------------------- - Fix get_next() when start_time less then 1s before next instant [AlexHill] 0.3.30 (2019-04-20) ------------------- - credits 0.3.29 (2019-03-26) ------------------- - credits - history stripping (security) - Handle -Sun notation, This fixes `#119 `_. [kiorky] - Handle invalid ranges correctly, This fixes `#114 `_. [kiorky] 0.3.25 (2018-08-07) ------------------- - Pypi hygiene [hugovk] 0.3.24 (2018-06-20) ------------------- - fix `#107 `_: microsecond threshold [kiorky] 0.3.23 (2018-05-23) ------------------- - fix ``get_next`` while preserving the fix of ``get_prev`` in 7661c2aaa [Avikam Agur ] 0.3.22 (2018-05-16) ------------------- - Don't count previous minute if now is dynamic If the code is triggered from 5-asterisk based cron ``get_prev`` based on ``datetime.now()`` is expected to return current cron iteration and not previous execution. [Igor Khrol ] 0.3.20 (2017-11-06) ------------------- - More DST fixes [Kevin Rose ] 0.3.19 (2017-08-31) ------------------- - fix #87: backward dst changes [kiorky] 0.3.18 (2017-08-31) ------------------- - Add is valid method, refactor errors [otherpirate, Mauro Murari ] 0.3.17 (2017-05-22) ------------------- - DOW occurrence sharp style support. [kiorky, Kengo Seki ] 0.3.16 (2017-03-15) ------------------- - Better test suite [mrcrilly@github] - DST support [kiorky] 0.3.15 (2017-02-16) ------------------- - fix bug around multiple conditions and range_val in _get_prev_nearest_diff. [abeja-yuki@github] 0.3.14 (2017-01-25) ------------------- - issue #69: added day_or option to change behavior when day-of-month and day-of-week is given [Andreas Vogl ] 0.3.13 (2016-11-01) ------------------- - `Real fix for #34 `_ [kiorky@github] - `Modernize test infra `_ [kiorky@github] - `Release as a universal wheel `_ [adamchainz@github] - `Raise ValueError on negative numbers `_ [josegonzalez@github] - `Compare types using "issubclass" instead of exact match `_ [darkk@github] - `Implement step cron with a variable base `_ [josegonzalez@github] 0.3.12 (2016-03-10) ------------------- - support setting ret_type in __init__ [Brent Tubbs ] 0.3.11 (2016-01-13) ------------------- - Bug fix: The get_prev API crashed when last day of month token was used. Some essential logic was missing. [Iddo Aviram ] 0.3.10 (2015-11-29) ------------------- - The functionality of 'l' as day of month was broken, since the month variable was not properly updated [Iddo Aviram ] 0.3.9 (2015-11-19) ------------------ - Don't use datetime functions python 2.6 doesn't support [petervtzand] 0.3.8 (2015-06-23) ------------------ - Truncate microseconds by setting to 0 [Corey Wright] 0.3.7 (2015-06-01) ------------------ - converting sun in range sun-thu transforms to int 0 which is recognized as empty string; the solution was to convert sun to string "0" 0.3.6 (2015-05-29) ------------------ - Fix default behavior when no start_time given Default value for ``start_time`` parameter is calculated at module init time rather than call time. - Fix timezone support and stop depending on the system time zone 0.3.5 (2014-08-01) ------------------ - support for 'l' (last day of month) 0.3.4 (2014-01-30) ------------------ - Python 3 compat - QA Release 0.3.3 (2012-09-29) ------------------ - proper packaging croniter-6.1.0rc1/Dockerfile000066400000000000000000000005661507066033300157550ustar00rootroot00000000000000FROM debian:bookworm-slim SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-o", "nounset", "-o", "nolog", "-c"] COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN uv python install ENV PATH="/root/.local/bin:$PATH" RUN uv tool install tox WORKDIR /app ADD *.rst LICENSE *.txt *.py *.ini *.sh *.toml ./ ADD src/ src/ ENTRYPOINT ["/app/docker-entry.sh"] CMD [] croniter-6.1.0rc1/LICENSE000066400000000000000000000020501507066033300147560ustar00rootroot00000000000000Copyright (C) 2010-2012 Matsumoto Taichi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.croniter-6.1.0rc1/README.rst000066400000000000000000000364431507066033300154550ustar00rootroot00000000000000Introduction ============ .. contents:: croniter provides iteration for the datetime object with a cron like format. :: _ _ ___ _ __ ___ _ __ (_) |_ ___ _ __ / __| '__/ _ \| '_ \| | __/ _ \ '__| | (__| | | (_) | | | | | || __/ | \___|_| \___/|_| |_|_|\__\___|_| Website: https://github.com/pallets-eco/croniter Build Badge =========== .. image:: https://github.com/pallets-eco/croniter/actions/workflows/cicd.yml/badge.svg :target: https://github.com/pallets-eco/croniter/actions/workflows/cicd.yml Pallets Community Ecosystem =========================== .. important:: This project is part of the Pallets Community Ecosystem. Pallets is the open source organization that maintains Flask; Pallets-Eco enables community maintenance of Flask extensions. If you are interested in helping maintain this project, please reach out on the `Pallets Discord server `_. Usage ============ A simple example:: >>> from croniter import croniter >>> from datetime import datetime >>> base = datetime(2010, 1, 25, 4, 46) >>> iter = croniter('*/5 * * * *', base) # every 5 minutes >>> print(iter.get_next(datetime)) # 2010-01-25 04:50:00 >>> print(iter.get_next(datetime)) # 2010-01-25 04:55:00 >>> print(iter.get_next(datetime)) # 2010-01-25 05:00:00 >>> >>> iter = croniter('2 4 * * mon,fri', base) # 04:02 on every Monday and Friday >>> print(iter.get_next(datetime)) # 2010-01-26 04:02:00 >>> print(iter.get_next(datetime)) # 2010-01-30 04:02:00 >>> print(iter.get_next(datetime)) # 2010-02-02 04:02:00 >>> >>> iter = croniter('2 4 1 * wed', base) # 04:02 on every Wednesday OR on 1st day of month >>> print(iter.get_next(datetime)) # 2010-01-27 04:02:00 >>> print(iter.get_next(datetime)) # 2010-02-01 04:02:00 >>> print(iter.get_next(datetime)) # 2010-02-03 04:02:00 >>> >>> iter = croniter('2 4 1 * wed', base, day_or=False) # 04:02 on every 1st day of the month if it is a Wednesday >>> print(iter.get_next(datetime)) # 2010-09-01 04:02:00 >>> print(iter.get_next(datetime)) # 2010-12-01 04:02:00 >>> print(iter.get_next(datetime)) # 2011-06-01 04:02:00 >>> >>> iter = croniter('0 0 * * sat#1,sun#2', base) # 1st Saturday, and 2nd Sunday of the month >>> print(iter.get_next(datetime)) # 2010-02-06 00:00:00 >>> >>> iter = croniter('0 0 * * 5#3,L5', base) # 3rd and last Friday of the month >>> print(iter.get_next(datetime)) # 2010-01-29 00:00:00 >>> print(iter.get_next(datetime)) # 2010-02-19 00:00:00 All you need to know is how to use the constructor and the ``get_next`` method, the signature of these methods are listed below:: >>> def __init__(self, cron_format, start_time=time.time(), day_or=True) croniter iterates along with ``cron_format`` from ``start_time``. ``cron_format`` is **min hour day month day_of_week**, you can refer to http://en.wikipedia.org/wiki/Cron for more details. The ``day_or`` switch is used to control how croniter handles **day** and **day_of_week** entries. Default option is the cron behaviour, which connects those values using **OR**. If the switch is set to False, the values are connected using **AND**. This behaves like fcron and enables you to e.g. define a job that executes each 2nd Friday of a month by setting the days of month and the weekday. :: >>> def get_next(self, ret_type=float) get_next calculates the next value according to the cron expression and returns an object of type ``ret_type``. ``ret_type`` should be a ``float`` or a ``datetime`` object. Supported added for ``get_prev`` method. (>= 0.2.0):: >>> base = datetime(2010, 8, 25) >>> itr = croniter('0 0 1 * *', base) >>> print(itr.get_prev(datetime)) # 2010-08-01 00:00:00 >>> print(itr.get_prev(datetime)) # 2010-07-01 00:00:00 >>> print(itr.get_prev(datetime)) # 2010-06-01 00:00:00 You can validate your crons using ``is_valid`` class method. (>= 0.3.18):: >>> croniter.is_valid('0 0 1 * *') # True >>> croniter.is_valid('0 wrong_value 1 * *') # False About DST ========= Be sure to init your croniter instance with a TZ aware datetime for this to work! Example using zoneinfo:: >>> import zoneinfo >>> tz = zoneinfo.ZoneInfo("Europe/Berlin") >>> local_date = datetime(2017, 3, 26, tzinfo=tz) >>> val = croniter('0 0 * * *', local_date).get_next(datetime) Example using pytz:: >>> import pytz >>> tz = pytz.timezone("Europe/Paris") >>> local_date = tz.localize(datetime(2017, 3, 26)) >>> val = croniter('0 0 * * *', local_date).get_next(datetime) Example using python_dateutil:: >>> import dateutil.tz >>> tz = dateutil.tz.gettz('Asia/Tokyo') >>> local_date = datetime(2017, 3, 26, tzinfo=tz) >>> val = croniter('0 0 * * *', local_date).get_next(datetime) Example using python built in module:: >>> from datetime import datetime, timezone >>> local_date = datetime(2017, 3, 26, tzinfo=timezone.utc) >>> val = croniter('0 0 * * *', local_date).get_next(datetime) About second repeats ===================== Croniter is able to do second repetition crontabs form and by default seconds are the 6th field:: >>> base = datetime(2012, 4, 6, 13, 26, 10) >>> itr = croniter('* * * * * 15,25', base) >>> itr.get_next(datetime) # 4/6 13:26:15 >>> itr.get_next(datetime) # 4/6 13:26:25 >>> itr.get_next(datetime) # 4/6 13:27:15 You can also note that this expression will repeat every second from the start datetime.:: >>> croniter('* * * * * *', local_date).get_next(datetime) You can also use seconds as first field:: >>> itr = croniter('15,25 * * * * *', base, second_at_beginning=True) About year =========== Croniter also support year field. Year presents at the seventh field, which is after second repetition. The range of year field is from 1970 to 2099. To ignore second repetition, simply set second to ``0`` or any other const:: >>> base = datetime(2012, 4, 6, 2, 6, 59) >>> itr = croniter('0 0 1 1 * 0 2020/2', base) >>> itr.get_next(datetime) # 2020 1/1 0:0:0 >>> itr.get_next(datetime) # 2022 1/1 0:0:0 >>> itr.get_next(datetime) # 2024 1/1 0:0:0 Support for start_time shifts ============================== See https://github.com/pallets-eco/croniter/pull/76, You can set start_time=, then expand_from_start_time=True for your generations to be computed from start_time instead of calendar days:: >>> from pprint import pprint >>> iter = croniter('0 0 */7 * *', start_time=datetime(2024, 7, 11), expand_from_start_time=True);pprint([iter.get_next(datetime) for a in range(10)]) [datetime.datetime(2024, 7, 18, 0, 0), datetime.datetime(2024, 7, 25, 0, 0), datetime.datetime(2024, 8, 4, 0, 0), datetime.datetime(2024, 8, 11, 0, 0), datetime.datetime(2024, 8, 18, 0, 0), datetime.datetime(2024, 8, 25, 0, 0), datetime.datetime(2024, 9, 4, 0, 0), datetime.datetime(2024, 9, 11, 0, 0), datetime.datetime(2024, 9, 18, 0, 0), datetime.datetime(2024, 9, 25, 0, 0)] >>> # INSTEAD OF THE DEFAULT BEHAVIOR: >>> iter = croniter('0 0 */7 * *', start_time=datetime(2024, 7, 11), expand_from_start_time=False);pprint([iter.get_next(datetime) for a in range(10)]) [datetime.datetime(2024, 7, 15, 0, 0), datetime.datetime(2024, 7, 22, 0, 0), datetime.datetime(2024, 7, 29, 0, 0), datetime.datetime(2024, 8, 1, 0, 0), datetime.datetime(2024, 8, 8, 0, 0), datetime.datetime(2024, 8, 15, 0, 0), datetime.datetime(2024, 8, 22, 0, 0), datetime.datetime(2024, 8, 29, 0, 0), datetime.datetime(2024, 9, 1, 0, 0), datetime.datetime(2024, 9, 8, 0, 0)] Testing if a date matches a crontab =================================== Test for a match with (>=0.3.32):: >>> croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 0, 0, 0)) True >>> croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 2, 0, 0)) False >>> >>> croniter.match("2 4 1 * wed", datetime(2019, 1, 1, 4, 2, 0, 0)) # 04:02 on every Wednesday OR on 1st day of month True >>> croniter.match("2 4 1 * wed", datetime(2019, 1, 1, 4, 2, 0, 0), day_or=False) # 04:02 on every 1st day of the month if it is a Wednesday False Testing if a crontab matches in datetime range ============================================== Test for a match_range with (>=2.0.3):: >>> croniter.match_range("0 0 * * *", datetime(2019, 1, 13, 0, 59, 0, 0), datetime(2019, 1, 14, 0, 1, 0, 0)) True >>> croniter.match_range("0 0 * * *", datetime(2019, 1, 13, 0, 1, 0, 0), datetime(2019, 1, 13, 0, 59, 0, 0)) False >>> croniter.match_range("2 4 1 * wed", datetime(2019, 1, 1, 3, 2, 0, 0), datetime(2019, 1, 1, 5, 1, 0, 0)) # 04:02 on every Wednesday OR on 1st day of month True >>> croniter.match_range("2 4 1 * wed", datetime(2019, 1, 1, 3, 2, 0, 0), datetime(2019, 1, 1, 5, 2, 0, 0), day_or=False) # 04:02 on every 1st day of the month if it is a Wednesday False Gaps between date matches ========================= For performance reasons, croniter limits the amount of CPU cycles spent attempting to find the next match. Starting in v0.3.35, this behavior is configurable via the ``max_years_between_matches`` parameter, and the default window has been increased from 1 year to 50 years. The defaults should be fine for many use cases. Applications that evaluate multiple cron expressions or handle cron expressions from untrusted sources or end-users should use this parameter. Iterating over sparse cron expressions can result in increased CPU consumption or a raised ``CroniterBadDateError`` exception which indicates that croniter has given up attempting to find the next (or previous) match. Explicitly specifying ``max_years_between_matches`` provides a way to limit CPU utilization and simplifies the iterable interface by eliminating the need for ``CroniterBadDateError``. The difference in the iterable interface is based on the reasoning that whenever ``max_years_between_matches`` is explicitly agreed upon, there is no need for croniter to signal that it has given up; simply stopping the iteration is preferable. This example matches 4 AM Friday, January 1st. Since January 1st isn't often a Friday, there may be a few years between each occurrence. Setting the limit to 15 years ensures all matches:: >>> it = croniter("0 4 1 1 fri", datetime(2000,1,1), day_or=False, max_years_between_matches=15).all_next(datetime) >>> for i in range(5): ... print(next(it)) ... 2010-01-01 04:00:00 2016-01-01 04:00:00 2021-01-01 04:00:00 2027-01-01 04:00:00 2038-01-01 04:00:00 However, when only concerned with dates within the next 5 years, simply set ``max_years_between_matches=5`` in the above example. This will result in no matches found, but no additional cycles will be wasted on unwanted matches far in the future. Iterating over a range using cron ================================= Find matches within a range using the ``croniter_range()`` function. This is much like the builtin ``range(start,stop,step)`` function, but for dates. The `step` argument is a cron expression. Added in (>=0.3.34) List the first Saturday of every month in 2019:: >>> from croniter import croniter_range >>> for dt in croniter_range(datetime(2019, 1, 1), datetime(2019, 12, 31), "0 0 * * sat#1"): >>> print(dt) Hashed expressions ================== croniter supports Jenkins-style hashed expressions, using the "H" definition keyword and the required hash_id keyword argument. Hashed expressions remain consistent, given the same hash_id, but different hash_ids will evaluate completely different to each other. This allows, for example, for an even distribution of differently-named jobs without needing to manually spread them out. >>> itr = croniter("H H * * *", hash_id="hello") >>> itr.get_next(datetime) datetime.datetime(2021, 4, 10, 11, 10) >>> itr.get_next(datetime) datetime.datetime(2021, 4, 11, 11, 10) >>> itr = croniter("H H * * *", hash_id="hello") >>> itr.get_next(datetime) datetime.datetime(2021, 4, 10, 11, 10) >>> itr = croniter("H H * * *", hash_id="bonjour") >>> itr.get_next(datetime) datetime.datetime(2021, 4, 10, 20, 52) Random expressions ================== Random "R" definition keywords are supported, and remain consistent only within their croniter() instance. >>> itr = croniter("R R * * *") >>> itr.get_next(datetime) datetime.datetime(2021, 4, 10, 22, 56) >>> itr.get_next(datetime) datetime.datetime(2021, 4, 11, 22, 56) >>> itr = croniter("R R * * *") >>> itr.get_next(datetime) datetime.datetime(2021, 4, 11, 4, 19) Note about Ranges ================= Note that as a deviation from cron standard, croniter is somehow laxist with ranges and will allow ranges of ``Jan-Dec``, & ``Sun-Sat`` in reverse way and interpret them as following examples: - ``Apr-Jan``: from April to january - ``Sat-Sun``: Saturday, Sunday - ``Wed-Sun``: Wednesday to Saturday, Sunday Please note that if a /step is given, it will be respected. Note about Sunday ================= Note that as a deviation from cron standard, croniter like numerous cron implementations supports ``SUNDAY`` to be expressed as ``DAY7``, allowing such expressions: - ``0 0 * * 7`` - ``0 0 * * 6-7`` - ``0 0 * * 6,7`` Keyword expressions =================== Vixie cron-style "@" keyword expressions are supported. What they evaluate to depends on whether you supply hash_id: no hash_id corresponds to Vixie cron definitions (exact times, minute resolution), while with hash_id corresponds to Jenkins definitions (hashed within the period, second resolution). ============ ============ ================ Keyword No hash_id With hash_id ============ ============ ================ @midnight 0 0 * * * H H(0-2) * * * H @hourly 0 * * * * H * * * * H @daily 0 0 * * * H H * * * H @weekly 0 0 * * 0 H H * * H H @monthly 0 0 1 * * H H H * * H @yearly 0 0 1 1 * H H H H * H @annually 0 0 1 1 * H H H H * H ============ ============ ================ Upgrading ========== To 2.0.0 --------- - Install or upgrade pytz by using version specified requirements/base.txt if you have it installed `<=2021.1`. Develop this package ==================== :: git clone https://github.com/pallets-eco/croniter.git cd croniter virtualenv --no-site-packages venv3 venv3/bin/pip install --upgrade -r requirements/test.txt -r requirements/lint.txt -r requirements/format.txt -r requirements/tox.txt venv3/bin/black src/ venv3/bin/isort src/ venv3/bin/tox --current-env -e fmt,lint,test Make a new release ==================== We use zest.fullreleaser, a great release infrastructure. Do and follow these instructions :: venv3/bin/pip install --upgrade -r requirements/release.txt ./release.sh Contributors =============== Thanks to all who have contributed to this project! If you have contributed and your name is not listed below please let us know. - Aarni Koskela (akx) - ashb - bdrung - chris-baynes - djmitche - evanpurkhiser - GreatCombinator - Hinnack - ipartola - jlsandell - kiorky - lowell80 (Kintyre) - mag009 - mrmachine - potiuk - Ryan Finnie (rfinnie) - salitaba - scop - shazow - yuzawa-san - zed2015 croniter-6.1.0rc1/apt.txt000066400000000000000000000000761507066033300153040ustar00rootroot00000000000000python3 # dev deps python3-dev libpython3-dev cargo git-core croniter-6.1.0rc1/docker-compose-32bits.yml000066400000000000000000000001151507066033300205120ustar00rootroot00000000000000services: app: image: "${CRONITER_IMAGE:-pallets-eco/croniter:32bits}" croniter-6.1.0rc1/docker-compose-build-32bits.yml000066400000000000000000000001111507066033300216030ustar00rootroot00000000000000services: app: build: args: BASE: "debian-32:trixie" croniter-6.1.0rc1/docker-compose-build.yml000066400000000000000000000001341507066033300205040ustar00rootroot00000000000000services: app: build: context: . args: BUILDKIT_INLINE_CACHE: "1" croniter-6.1.0rc1/docker-compose.yml000066400000000000000000000004011507066033300174040ustar00rootroot00000000000000services: app: image: "${CRONITER_IMAGE:-pallets-eco/croniter:latest}" volumes: - ./docker-entry.sh:/app/docker-entry.sh - ./.dockertox:/app/.tox - ./pyproject.toml:/app/pyproject.toml - ./src:/app/src - ./tox.ini:/app/tox.ini croniter-6.1.0rc1/docker-entry.sh000077500000000000000000000001531507066033300167200ustar00rootroot00000000000000#!/usr/bin/env bash [[ -n "${SDEBUG}" ]] && set -x if [[ -z $@ ]];then exec bash else exec "$@" fi croniter-6.1.0rc1/pyproject.toml000066400000000000000000000041371507066033300166750ustar00rootroot00000000000000[build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [project] name = "croniter" version = "6.1.0rc1" description = "croniter provides iteration for datetime object with cron like format" readme = "README.rst" license = { file = "LICENSE" } authors = [ { name = "Matsumoto Taichi", email = "taichino@gmail.com" }, { name = "kiorky", email = "kiorky@cryptelium.net" }, { name = "Ash Berlin-Taylor", email = "ash@apache.org" }, { name = "Jarek Potiuk", email = "jarek@potiuk.com" }, ] requires-python = ">=3.9" keywords = [ "datetime", "iterator", "cron" ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13" ] dependencies = [ "python_dateutil" ] [project.urls] Homepage = "https://github.com/pallets-eco/croniter" [dependency-groups] dev = [ "pytest>=8.3.3", "pytest-cov>=5.0.0", "coverage>=4.2", "setuptools", "pytz>2021.1" ] lint = [ "ruff" ] mypy = [ "mypy", "types-python-dateutil", "types-pytz" ] format = [ "ruff", ] tox = [ "tox>=4", ] release = [ "zest.releaser[recommended]>=6.7" ] [tool.ruff] line-length = 99 [tool.ruff.lint.per-file-ignores] "*" = ["C901"] [tool.coverage.run] branch = true omit = [ ".tox/*", "src/croniter/tests*", "src/croniter/test_*.py" ] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "raise AssertionError", "raise NotImplementedError" ] [tool.isort] profile = "black" multi_line_output = 3 include_trailing_comma = true skip = [ ".tox", ".git" ] force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true line_length = 99 [tool.flit.module] directory = "src" name = "croniter" [tool.mypy] exclude = [ "dist/.*", # Exclude hidden files and directories ".*/\\..*" ] croniter-6.1.0rc1/release.sh000077500000000000000000000001701507066033300157310ustar00rootroot00000000000000#!/usr/bin/env bash set -ex . venv3/bin/activate fullrelease git push && git push --tags # vim:set et sts=4 ts=4 tw=80: croniter-6.1.0rc1/src/000077500000000000000000000000001507066033300145435ustar00rootroot00000000000000croniter-6.1.0rc1/src/croniter/000077500000000000000000000000001507066033300163705ustar00rootroot00000000000000croniter-6.1.0rc1/src/croniter/__init__.py000066400000000000000000000015121507066033300205000ustar00rootroot00000000000000from . import croniter as cron_m from .croniter import ( DAY_FIELD, HOUR_FIELD, MINUTE_FIELD, MONTH_FIELD, OVERFLOW32B_MODE, SECOND_FIELD, UTC_DT, YEAR_FIELD, CroniterBadCronError, CroniterBadDateError, CroniterBadTypeRangeError, CroniterError, CroniterNotAlphaError, CroniterUnsupportedSyntaxError, croniter, croniter_range, datetime_to_timestamp, ) __all__ = [ "DAY_FIELD", "HOUR_FIELD", "MINUTE_FIELD", "MONTH_FIELD", "OVERFLOW32B_MODE", "SECOND_FIELD", "UTC_DT", "YEAR_FIELD", "CroniterBadCronError", "CroniterBadDateError", "CroniterBadTypeRangeError", "CroniterError", "CroniterNotAlphaError", "CroniterUnsupportedSyntaxError", "cron_m", "croniter", "croniter_range", "datetime_to_timestamp", ] croniter-6.1.0rc1/src/croniter/croniter.py000066400000000000000000001462031507066033300205750ustar00rootroot00000000000000#!/usr/bin/env python import binascii import calendar import copy import datetime import math import platform import random import re import struct import sys import traceback as _traceback from time import time from typing import Any, Literal, Optional, Union from dateutil.relativedelta import relativedelta from dateutil.tz import datetime_exists, tzutc ExpandedExpression = list[Union[int, Literal["*", "l"]]] def is_32bit() -> bool: """ Detect if Python is running in 32-bit mode. Returns True if running on 32-bit Python, False for 64-bit. """ # Method 1: Check pointer size bits = struct.calcsize("P") * 8 # Method 2: Check platform architecture string try: architecture = platform.architecture()[0] except RuntimeError: architecture = None # Method 3: Check maxsize is_small_maxsize = sys.maxsize <= 2**32 # Evaluate all available methods is_32 = False if bits == 32: is_32 = True elif architecture and "32" in architecture: is_32 = True elif is_small_maxsize: is_32 = True return is_32 try: # https://github.com/python/cpython/issues/101069 detection if is_32bit(): datetime.datetime.fromtimestamp(3999999999) OVERFLOW32B_MODE = False except OverflowError: OVERFLOW32B_MODE = True UTC_DT = datetime.timezone.utc EPOCH = datetime.datetime.fromtimestamp(0, UTC_DT) M_ALPHAS: dict[str, Union[int, str]] = { "jan": 1, "feb": 2, "mar": 3, "apr": 4, # noqa: E241 "may": 5, "jun": 6, "jul": 7, "aug": 8, # noqa: E241 "sep": 9, "oct": 10, "nov": 11, "dec": 12, } DOW_ALPHAS: dict[str, Union[int, str]] = { "sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6, } MINUTE_FIELD = 0 HOUR_FIELD = 1 DAY_FIELD = 2 MONTH_FIELD = 3 DOW_FIELD = 4 SECOND_FIELD = 5 YEAR_FIELD = 6 UNIX_FIELDS = (MINUTE_FIELD, HOUR_FIELD, DAY_FIELD, MONTH_FIELD, DOW_FIELD) SECOND_FIELDS = (MINUTE_FIELD, HOUR_FIELD, DAY_FIELD, MONTH_FIELD, DOW_FIELD, SECOND_FIELD) YEAR_FIELDS = ( MINUTE_FIELD, HOUR_FIELD, DAY_FIELD, MONTH_FIELD, DOW_FIELD, SECOND_FIELD, YEAR_FIELD, ) step_search_re = re.compile(r"^([^-]+)-([^-/]+)(/(\d+))?$") only_int_re = re.compile(r"^\d+$") DAYS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) WEEKDAYS = "|".join(DOW_ALPHAS.keys()) MONTHS = "|".join(M_ALPHAS.keys()) star_or_int_re = re.compile(r"^(\d+|\*)$") special_dow_re = re.compile( rf"^(?P
((?P(({WEEKDAYS})(-({WEEKDAYS}))?)"
    rf"|(({MONTHS})(-({MONTHS}))?)|\w+)#)|l)(?P\d+)$"
)
re_star = re.compile("[*]")
hash_expression_re = re.compile(
    r"^(?Ph|r)(\((?P\d+)-(?P\d+)\))?(\/(?P\d+))?$"
)

CRON_FIELDS = {
    "unix": UNIX_FIELDS,
    "second": SECOND_FIELDS,
    "year": YEAR_FIELDS,
    len(UNIX_FIELDS): UNIX_FIELDS,
    len(SECOND_FIELDS): SECOND_FIELDS,
    len(YEAR_FIELDS): YEAR_FIELDS,
}
UNIX_CRON_LEN = len(UNIX_FIELDS)
SECOND_CRON_LEN = len(SECOND_FIELDS)
YEAR_CRON_LEN = len(YEAR_FIELDS)
# retrocompat
VALID_LEN_EXPRESSION = {a for a in CRON_FIELDS if isinstance(a, int)}
EXPRESSIONS: dict[tuple[str, Optional[bytes], bool], list[str]] = {}
MARKER = object()


def datetime_to_timestamp(d):
    if d.tzinfo is not None:
        d = d.replace(tzinfo=None) - d.utcoffset()

    return (d - datetime.datetime(1970, 1, 1)).total_seconds()


def _is_leap(year: int) -> bool:
    return year % 400 == 0 or (year % 4 == 0 and year % 100 != 0)


def _last_day_of_month(year: int, month: int) -> int:
    """Calculate the last day of the given month (honor leap years)."""
    last_day = DAYS[month - 1]
    if month == 2 and _is_leap(year):
        last_day += 1
    return last_day


def _is_successor(
    date: datetime.datetime, previous_date: datetime.datetime, is_prev: bool
) -> bool:
    """Check if the given date is a successor (after/before) of the previous date."""
    if is_prev:
        return date.astimezone(UTC_DT) < previous_date.astimezone(UTC_DT)
    return date.astimezone(UTC_DT) > previous_date.astimezone(UTC_DT)


def _timezone_delta(date1: datetime.datetime, date2: datetime.datetime) -> datetime.timedelta:
    """Calculate the timezone difference of the given dates."""
    offset1 = date1.utcoffset()
    offset2 = date2.utcoffset()
    assert offset1 is not None
    assert offset2 is not None
    return offset2 - offset1


def _add_tzinfo(
    date: datetime.datetime, previous_date: datetime.datetime, is_prev: bool
) -> tuple[datetime.datetime, bool]:
    """Add the tzinfo from the previous date to the given date.

    In case the new date is ambiguous, determine the correct date
    based on it being closer to the previous date but still a successor
    (after/before based on `is_prev`).

    In case the date does not exist, jump forward to the next existing date.
    """
    localize = getattr(previous_date.tzinfo, "localize", None)
    if localize is not None:
        # pylint: disable-next=import-outside-toplevel
        import pytz

        try:
            result = localize(date, is_dst=None)
        except pytz.NonExistentTimeError:
            while True:
                date += datetime.timedelta(minutes=1)
                try:
                    result = localize(date, is_dst=None)
                except pytz.NonExistentTimeError:
                    continue
                break
            return result, False
        except pytz.AmbiguousTimeError:
            closer = localize(date, is_dst=not is_prev)
            farther = localize(date, is_dst=is_prev)
            # TODO: Check negative DST
            assert (closer.astimezone(UTC_DT) > farther.astimezone(UTC_DT)) == is_prev
            if _is_successor(closer, previous_date, is_prev):
                result = closer
            else:
                assert _is_successor(farther, previous_date, is_prev)
                result = farther
        return result, True

    result = date.replace(fold=1 if is_prev else 0, tzinfo=previous_date.tzinfo)
    if not datetime_exists(result):
        while not datetime_exists(result):
            result += datetime.timedelta(minutes=1)
        return result, False

    # result is closer to the previous date
    farther = date.replace(fold=0 if is_prev else 1, tzinfo=previous_date.tzinfo)
    # Comparing the UTC offsets in the check for the date being ambiguous.
    if result.utcoffset() != farther.utcoffset():
        # TODO: Check negative DST
        assert (result.astimezone(UTC_DT) > farther.astimezone(UTC_DT)) == is_prev
        if not _is_successor(result, previous_date, is_prev):
            assert _is_successor(farther, previous_date, is_prev)
            result = farther
    return result, True


class CroniterError(ValueError):
    """General top-level Croniter base exception"""


class CroniterBadTypeRangeError(TypeError):
    """."""


class CroniterBadCronError(CroniterError):
    """Syntax, unknown value, or range error within a cron expression"""


class CroniterUnsupportedSyntaxError(CroniterBadCronError):
    """Valid cron syntax, but likely to produce inaccurate results"""

    # Extending CroniterBadCronError, which may be contridatory, but this allows
    # catching both errors with a single exception.  From a user perspective
    # these will likely be handled the same way.


class CroniterBadDateError(CroniterError):
    """Unable to find next/prev timestamp match"""


class CroniterNotAlphaError(CroniterBadCronError):
    """Cron syntax contains an invalid day or month abbreviation"""


class croniter:
    MONTHS_IN_YEAR = 12

    # This helps with expanding `*` fields into `lower-upper` ranges. Each item
    # in this tuple maps to the corresponding field index
    RANGES = ((0, 59), (0, 23), (1, 31), (1, 12), (0, 6), (0, 59), (1970, 2099))

    ALPHACONV: tuple[dict[str, Union[int, str]], ...] = (
        {},  # 0: min
        {},  # 1: hour
        {"l": "l"},  # 2: dom
        # 3: mon
        copy.deepcopy(M_ALPHAS),
        # 4: dow
        copy.deepcopy(DOW_ALPHAS),
        # 5: second
        {},
        # 6: year
        {},
    )

    LOWMAP: tuple[dict[int, int], ...] = ({}, {}, {0: 1}, {0: 1}, {7: 0}, {}, {})

    LEN_MEANS_ALL = (60, 24, 31, 12, 7, 60, 130)

    def __init__(
        self,
        expr_format: str,
        start_time: Optional[Union[datetime.datetime, float]] = None,
        ret_type: type = float,
        day_or: bool = True,
        max_years_between_matches: Optional[int] = None,
        is_prev: bool = False,
        hash_id: Optional[Union[bytes, str]] = None,
        implement_cron_bug: bool = False,
        second_at_beginning: bool = False,
        expand_from_start_time: bool = False,
    ) -> None:
        self._ret_type = ret_type
        self._day_or = day_or
        self._implement_cron_bug = implement_cron_bug
        self.second_at_beginning = bool(second_at_beginning)
        self._expand_from_start_time = expand_from_start_time

        if hash_id is not None:
            if not isinstance(hash_id, (bytes, str)):
                raise TypeError("hash_id must be bytes or UTF-8 string")
            if not isinstance(hash_id, bytes):
                hash_id = hash_id.encode("UTF-8")

        self._max_years_btw_matches_explicitly_set = max_years_between_matches is not None
        if max_years_between_matches is None:
            max_years_between_matches = 50
        self._max_years_between_matches = max(int(max_years_between_matches), 1)

        if start_time is None:
            start_time = time()

        self.tzinfo: Optional[datetime.tzinfo] = None

        self.start_time = 0.0
        self.dst_start_time = 0.0
        self.cur = 0.0
        self.set_current(start_time, force=True)

        self.expanded, self.nth_weekday_of_month = self.expand(
            expr_format,
            hash_id=hash_id,
            from_timestamp=self.dst_start_time if self._expand_from_start_time else None,
            second_at_beginning=second_at_beginning,
        )
        self.fields = CRON_FIELDS[len(self.expanded)]
        self.expressions = EXPRESSIONS[(expr_format, hash_id, second_at_beginning)]
        self._is_prev = is_prev

    @classmethod
    def _alphaconv(cls, index, key, expressions):
        try:
            return cls.ALPHACONV[index][key]
        except KeyError:
            raise CroniterNotAlphaError(f"[{' '.join(expressions)}] is not acceptable")

    def get_next(self, ret_type=None, start_time=None, update_current=True):
        if start_time and self._expand_from_start_time:
            raise ValueError(
                "start_time is not supported when using expand_from_start_time = True."
            )
        return self._get_next(
            ret_type=ret_type, start_time=start_time, is_prev=False, update_current=update_current
        )

    def get_prev(self, ret_type=None, start_time=None, update_current=True):
        return self._get_next(
            ret_type=ret_type, start_time=start_time, is_prev=True, update_current=update_current
        )

    def get_current(self, ret_type=None):
        ret_type = ret_type or self._ret_type
        if issubclass(ret_type, datetime.datetime):
            return self.timestamp_to_datetime(self.cur)
        return self.cur

    def set_current(
        self, start_time: Optional[Union[datetime.datetime, float]], force: bool = True
    ) -> float:
        if (force or (self.cur is None)) and start_time is not None:
            if isinstance(start_time, datetime.datetime):
                self.tzinfo = start_time.tzinfo
                start_time = self.datetime_to_timestamp(start_time)

            self.start_time = start_time
            self.dst_start_time = start_time
            self.cur = start_time
        return self.cur

    @staticmethod
    def datetime_to_timestamp(d: datetime.datetime) -> float:
        """
        Converts a `datetime` object `d` into a UNIX timestamp.
        """
        return datetime_to_timestamp(d)

    _datetime_to_timestamp = datetime_to_timestamp  # retrocompat

    def timestamp_to_datetime(self, timestamp: float, tzinfo: Any = MARKER) -> datetime.datetime:
        """
        Converts a UNIX `timestamp` into a `datetime` object.
        """
        if tzinfo is MARKER:  # allow to give tzinfo=None even if self.tzinfo is set
            tzinfo = self.tzinfo
        if OVERFLOW32B_MODE:
            # degraded mode to workaround Y2038
            # see https://github.com/python/cpython/issues/101069
            result = EPOCH.replace(tzinfo=None) + datetime.timedelta(seconds=timestamp)
        else:
            result = datetime.datetime.fromtimestamp(timestamp, tz=tzutc()).replace(tzinfo=None)
        if tzinfo:
            result = result.replace(tzinfo=UTC_DT).astimezone(tzinfo)
        return result

    _timestamp_to_datetime = timestamp_to_datetime  # retrocompat

    def _get_next(self, ret_type=None, start_time=None, is_prev=None, update_current=None):
        if update_current is None:
            update_current = True
        self.set_current(start_time, force=True)
        if is_prev is None:
            is_prev = self._is_prev
        self._is_prev = is_prev

        ret_type = ret_type or self._ret_type

        if not issubclass(ret_type, (float, datetime.datetime)):
            raise TypeError("Invalid ret_type, only 'float' or 'datetime' is acceptable.")

        result = self._calc_next(is_prev)
        timestamp = self.datetime_to_timestamp(result)
        if update_current:
            self.cur = timestamp
        if issubclass(ret_type, datetime.datetime):
            return result
        return timestamp

    # iterator protocol, to enable direct use of croniter
    # objects in a loop, like "for dt in croniter("5 0 * * *'): ..."
    # or for combining multiple croniters into single
    # dates feed using 'itertools' module
    def all_next(self, ret_type=None, start_time=None, update_current=None):
        """
        Returns a generator yielding consecutive dates.

        May be used instead of an implicit call to __iter__ whenever a
        non-default `ret_type` needs to be specified.
        """
        # In a Python 3.7+ world:  contextlib.suppress and contextlib.nullcontext could
        # be used instead
        try:
            while True:
                self._is_prev = False
                yield self._get_next(
                    ret_type=ret_type, start_time=start_time, update_current=update_current
                )
                start_time = None
        except CroniterBadDateError:
            if self._max_years_btw_matches_explicitly_set:
                return
            raise

    def all_prev(self, ret_type=None, start_time=None, update_current=None):
        """
        Returns a generator yielding previous dates.
        """
        try:
            while True:
                self._is_prev = True
                yield self._get_next(
                    ret_type=ret_type, start_time=start_time, update_current=update_current
                )
                start_time = None
        except CroniterBadDateError:
            if self._max_years_btw_matches_explicitly_set:
                return
            raise

    def iter(self, *args, **kwargs):
        return self.all_prev if self._is_prev else self.all_next

    def __iter__(self):
        return self

    __next__ = next = _get_next

    def _calc_next(self, is_prev: bool) -> datetime.datetime:
        current = self.timestamp_to_datetime(self.cur)
        expanded = self.expanded[:]
        nth_weekday_of_month = self.nth_weekday_of_month.copy()

        # exception to support day of month and day of week as defined in cron
        if (expanded[DAY_FIELD][0] != "*" and expanded[DOW_FIELD][0] != "*") and self._day_or:
            # If requested, handle a bug in vixie cron/ISC cron where day_of_month and
            # day_of_week form an intersection (AND) instead of a union (OR) if either
            # field is an asterisk or starts with an asterisk (https://crontab.guru/cron-bug.html)
            if self._implement_cron_bug and (
                re_star.match(self.expressions[DAY_FIELD])
                or re_star.match(self.expressions[DOW_FIELD])
            ):
                # To produce a schedule identical to the cron bug, we'll bypass the code
                # that makes a union of DOM and DOW, and instead skip to the code that
                # does an intersect instead
                pass
            else:
                bak = expanded[DOW_FIELD]
                expanded[DOW_FIELD] = ["*"]
                t1 = self._calc(current, expanded, nth_weekday_of_month, is_prev)
                expanded[DOW_FIELD] = bak
                expanded[DAY_FIELD] = ["*"]

                t2 = self._calc(current, expanded, nth_weekday_of_month, is_prev)
                if is_prev:
                    return t1 if t1 > t2 else t2
                return t1 if t1 < t2 else t2

        return self._calc(current, expanded, nth_weekday_of_month, is_prev)

    def _calc(
        self,
        now: datetime.datetime,
        expanded: list[ExpandedExpression],
        nth_weekday_of_month: dict[int, set[int]],
        is_prev: bool,
    ) -> datetime.datetime:
        if is_prev:
            nearest_diff_method = self._get_prev_nearest_diff
            offset = relativedelta(microseconds=-1)
        else:
            nearest_diff_method = self._get_next_nearest_diff
            if len(expanded) > UNIX_CRON_LEN:
                offset = relativedelta(seconds=1)
            else:
                offset = relativedelta(minutes=1)
        # Calculate the next cron time in local time a.k.a. timezone unaware time.
        unaware_time = now.replace(tzinfo=None) + offset
        if len(expanded) > UNIX_CRON_LEN:
            unaware_time = unaware_time.replace(microsecond=0)
        else:
            unaware_time = unaware_time.replace(second=0, microsecond=0)

        month = unaware_time.month
        year = current_year = unaware_time.year

        def proc_year(d):
            if len(expanded) == YEAR_CRON_LEN:
                try:
                    expanded[YEAR_FIELD].index("*")
                except ValueError:
                    # use None as range_val to indicate no loop
                    diff_year = nearest_diff_method(d.year, expanded[YEAR_FIELD], None)
                    if diff_year is None:
                        return None, d
                    if diff_year != 0:
                        if is_prev:
                            d += relativedelta(
                                years=diff_year, month=12, day=31, hour=23, minute=59, second=59
                            )
                        else:
                            d += relativedelta(
                                years=diff_year, month=1, day=1, hour=0, minute=0, second=0
                            )
                        return True, d
            return False, d

        def proc_month(d):
            try:
                expanded[MONTH_FIELD].index("*")
            except ValueError:
                diff_month = nearest_diff_method(
                    d.month, expanded[MONTH_FIELD], self.MONTHS_IN_YEAR
                )
                reset_day = 1

                if diff_month is not None and diff_month != 0:
                    if is_prev:
                        d += relativedelta(months=diff_month)
                        reset_day = _last_day_of_month(d.year, d.month)
                        d += relativedelta(day=reset_day, hour=23, minute=59, second=59)
                    else:
                        d += relativedelta(
                            months=diff_month, day=reset_day, hour=0, minute=0, second=0
                        )
                    return True, d
            return False, d

        def proc_day_of_month(d):
            try:
                expanded[DAY_FIELD].index("*")
            except ValueError:
                days = _last_day_of_month(year, month)
                if "l" in expanded[DAY_FIELD] and days == d.day:
                    return False, d

                if is_prev:
                    days_in_prev_month = DAYS[(month - 2) % self.MONTHS_IN_YEAR]
                    diff_day = nearest_diff_method(d.day, expanded[DAY_FIELD], days_in_prev_month)
                else:
                    diff_day = nearest_diff_method(d.day, expanded[DAY_FIELD], days)

                if diff_day is not None and diff_day != 0:
                    if is_prev:
                        d += relativedelta(days=diff_day, hour=23, minute=59, second=59)
                    else:
                        d += relativedelta(days=diff_day, hour=0, minute=0, second=0)
                    return True, d
            return False, d

        def proc_day_of_week(d):
            try:
                expanded[DOW_FIELD].index("*")
            except ValueError:
                diff_day_of_week = nearest_diff_method(d.isoweekday() % 7, expanded[DOW_FIELD], 7)
                if diff_day_of_week is not None and diff_day_of_week != 0:
                    if is_prev:
                        d += relativedelta(days=diff_day_of_week, hour=23, minute=59, second=59)
                    else:
                        d += relativedelta(days=diff_day_of_week, hour=0, minute=0, second=0)
                    return True, d
            return False, d

        def proc_day_of_week_nth(d):
            if "*" in nth_weekday_of_month:
                s = nth_weekday_of_month["*"]
                for i in range(0, 7):
                    if i in nth_weekday_of_month:
                        nth_weekday_of_month[i].update(s)
                    else:
                        nth_weekday_of_month[i] = s
                del nth_weekday_of_month["*"]

            candidates = []
            for wday, nth in nth_weekday_of_month.items():
                c = self._get_nth_weekday_of_month(d.year, d.month, wday)
                for n in nth:
                    if n == "l":
                        candidate = c[-1]
                    elif len(c) < n:
                        continue
                    else:
                        candidate = c[n - 1]
                    if (is_prev and candidate <= d.day) or (not is_prev and d.day <= candidate):
                        candidates.append(candidate)

            if not candidates:
                if is_prev:
                    d += relativedelta(days=-d.day, hour=23, minute=59, second=59)
                else:
                    days = _last_day_of_month(year, month)
                    d += relativedelta(days=(days - d.day + 1), hour=0, minute=0, second=0)
                return True, d

            candidates.sort()
            diff_day = (candidates[-1] if is_prev else candidates[0]) - d.day
            if diff_day != 0:
                if is_prev:
                    d += relativedelta(days=diff_day, hour=23, minute=59, second=59)
                else:
                    d += relativedelta(days=diff_day, hour=0, minute=0, second=0)
                return True, d
            return False, d

        def proc_hour(d):
            try:
                expanded[HOUR_FIELD].index("*")
            except ValueError:
                diff_hour = nearest_diff_method(d.hour, expanded[HOUR_FIELD], 24)
                if diff_hour is not None and diff_hour != 0:
                    if is_prev:
                        d += relativedelta(hours=diff_hour, minute=59, second=59)
                    else:
                        d += relativedelta(hours=diff_hour, minute=0, second=0)
                    return True, d
            return False, d

        def proc_minute(d):
            try:
                expanded[MINUTE_FIELD].index("*")
            except ValueError:
                diff_min = nearest_diff_method(d.minute, expanded[MINUTE_FIELD], 60)
                if diff_min is not None and diff_min != 0:
                    if is_prev:
                        d += relativedelta(minutes=diff_min, second=59)
                    else:
                        d += relativedelta(minutes=diff_min, second=0)
                    return True, d
            return False, d

        def proc_second(d):
            if len(expanded) > UNIX_CRON_LEN:
                try:
                    expanded[SECOND_FIELD].index("*")
                except ValueError:
                    diff_sec = nearest_diff_method(d.second, expanded[SECOND_FIELD], 60)
                    if diff_sec is not None and diff_sec != 0:
                        d += relativedelta(seconds=diff_sec)
                        return True, d
            else:
                d += relativedelta(second=0)
            return False, d

        procs = [
            proc_year,
            proc_month,
            proc_day_of_month,
            (proc_day_of_week_nth if nth_weekday_of_month else proc_day_of_week),
            proc_hour,
            proc_minute,
            proc_second,
        ]

        while abs(year - current_year) <= self._max_years_between_matches:
            next = False
            stop = False
            for proc in procs:
                (changed, unaware_time) = proc(unaware_time)
                # `None` can be set mostly for year processing
                # so please see proc_year / _get_prev_nearest_diff / _get_next_nearest_diff
                if changed is None:
                    stop = True
                    break
                if changed:
                    month, year = unaware_time.month, unaware_time.year
                    next = True
                    break
            if stop:
                break
            if next:
                continue

            unaware_time = unaware_time.replace(microsecond=0)
            if now.tzinfo is None:
                return unaware_time

            # Add timezone information back and handle DST changes
            aware_time, exists = _add_tzinfo(unaware_time, now, is_prev)

            if not exists and (
                not _is_successor(aware_time, now, is_prev) or "*" in expanded[HOUR_FIELD]
            ):
                # The calculated local date does not exist and moving the time forward
                # to the next valid time isn't the correct solution. Search for the
                # next matching cron time that exists.
                while not exists:
                    unaware_time = self._calc(
                        unaware_time, expanded, nth_weekday_of_month, is_prev
                    )
                    aware_time, exists = _add_tzinfo(unaware_time, now, is_prev)

            offset_delta = _timezone_delta(now, aware_time)
            if not offset_delta:
                # There was no DST change.
                return aware_time

            # There was a DST change. So check if there is a alternative cron time
            # for the other UTC offset.
            alternative_unaware_time = now.replace(tzinfo=None) + offset_delta
            alternative_unaware_time = self._calc(
                alternative_unaware_time, expanded, nth_weekday_of_month, is_prev
            )
            alternative_aware_time, exists = _add_tzinfo(alternative_unaware_time, now, is_prev)

            if not _is_successor(alternative_aware_time, now, is_prev):
                # The alternative time is an ancestor of now. Thus it is not an alternative.
                return aware_time

            if _is_successor(aware_time, alternative_aware_time, is_prev):
                return alternative_aware_time

            return aware_time

        if is_prev:
            raise CroniterBadDateError("failed to find prev date")
        raise CroniterBadDateError("failed to find next date")

    @staticmethod
    def _get_next_nearest_diff(x, to_check, range_val):
        """
        `range_val` is the range of a field.
        If no available time, we can move to next loop(like next month).
        `range_val` can also be set to `None` to indicate that there is no loop.
        ( Currently, should only used for `year` field )
        """
        for i, d in enumerate(to_check):
            if range_val is not None:
                if d == "l":
                    # if 'l' then it is the last day of month
                    # => its value of range_val
                    d = range_val
                elif d > range_val:
                    continue
            if d >= x:
                return d - x
        # When range_val is None and x not exists in to_check,
        # `None` will be returned to suggest no more available time
        if range_val is None:
            return None
        return to_check[0] - x + range_val

    @staticmethod
    def _get_prev_nearest_diff(x, to_check, range_val):
        """
        `range_val` is the range of a field.
        If no available time, we can move to previous loop(like previous month).
        Range_val can also be set to `None` to indicate that there is no loop.
        ( Currently should only used for `year` field )
        """
        candidates = to_check[:]
        candidates.reverse()
        for d in candidates:
            if d != "l" and d <= x:
                return d - x
        if "l" in candidates:
            return -x
        # When range_val is None and x not exists in to_check,
        # `None` will be returned to suggest no more available time
        if range_val is None:
            return None
        candidate = candidates[0]
        for c in candidates:
            # fixed: c < range_val
            # this code will reject all 31 day of month, 12 month, 59 second,
            # 23 hour and so on.
            # if candidates has just a element, this will not harmful.
            # but candidates have multiple elements, then values equal to
            # range_val will rejected.
            if c <= range_val:
                candidate = c
                break
        # fix crontab "0 6 30 3 *" condidates only a element, then get_prev error
        # return 2021-03-02 06:00:00
        if candidate > range_val:
            return -range_val
        return candidate - x - range_val

    @staticmethod
    def _get_nth_weekday_of_month(year: int, month: int, day_of_week: int) -> tuple[int, ...]:
        """For a given year/month return a list of days in nth-day-of-month order.
        The last weekday of the month is always [-1].
        """
        w = (day_of_week + 6) % 7
        c = calendar.Calendar(w).monthdayscalendar(year, month)
        if c[0][0] == 0:
            c.pop(0)
        return tuple(i[0] for i in c)

    @classmethod
    def value_alias(cls, val, field_index, len_expressions=UNIX_CRON_LEN):
        if isinstance(len_expressions, (list, dict, tuple, set)):
            len_expressions = len(len_expressions)
        if val in cls.LOWMAP[field_index] and not (
            # do not support 0 as a month either for classical 5 fields cron,
            # 6fields second repeat form or 7 fields year form
            # but still let conversion happen if day field is shifted
            (field_index in [DAY_FIELD, MONTH_FIELD] and len_expressions == UNIX_CRON_LEN)
            or (field_index in [MONTH_FIELD, DOW_FIELD] and len_expressions == SECOND_CRON_LEN)
            or (
                field_index in [DAY_FIELD, MONTH_FIELD, DOW_FIELD]
                and len_expressions == YEAR_CRON_LEN
            )
        ):
            val = cls.LOWMAP[field_index][val]
        return val

    @classmethod
    def _expand(cls, expr_format, hash_id=None, second_at_beginning=False, from_timestamp=None):
        # Split the expression in components, and normalize L -> l, MON -> mon,
        # etc. Keep expr_format untouched so we can use it in the exception
        # messages.
        expr_aliases = {
            "@midnight": ("0 0 * * *", "h h(0-2) * * * h"),
            "@hourly": ("0 * * * *", "h * * * * h"),
            "@daily": ("0 0 * * *", "h h * * * h"),
            "@weekly": ("0 0 * * 0", "h h * * h h"),
            "@monthly": ("0 0 1 * *", "h h h * * h"),
            "@yearly": ("0 0 1 1 *", "h h h h * h"),
            "@annually": ("0 0 1 1 *", "h h h h * h"),
        }

        efl = expr_format.lower()
        hash_id_expr = 1 if hash_id is not None else 0
        try:
            efl = expr_aliases[efl][hash_id_expr]
        except KeyError:
            pass

        expressions = efl.split()

        if len(expressions) not in VALID_LEN_EXPRESSION:
            raise CroniterBadCronError(
                "Exactly 5, 6 or 7 columns has to be specified for iterator expression."
            )

        if len(expressions) > UNIX_CRON_LEN and second_at_beginning:
            # move second to it's own(6th) field to process by same logical
            expressions.insert(SECOND_FIELD, expressions.pop(0))

        expanded = []
        nth_weekday_of_month = {}

        for field_index, expr in enumerate(expressions):
            for expanderid, expander in EXPANDERS.items():
                expr = expander(cls).expand(
                    efl, field_index, expr, hash_id=hash_id, from_timestamp=from_timestamp
                )

            if "?" in expr:
                if expr != "?":
                    raise CroniterBadCronError(
                        f"[{expr_format}] is not acceptable."
                        f" Question mark can not used with other characters"
                    )
                if field_index not in [DAY_FIELD, DOW_FIELD]:
                    raise CroniterBadCronError(
                        f"[{expr_format}] is not acceptable. "
                        f"Question mark can only used in day_of_month or day_of_week"
                    )
                # currently just trade `?` as `*`
                expr = "*"

            e_list = expr.split(",")
            res = []

            while len(e_list) > 0:
                e = e_list.pop()
                nth = None

                if field_index == DOW_FIELD:
                    # Handle special case in the dow expression: 2#3, l3
                    special_dow_rem = special_dow_re.match(str(e))
                    if special_dow_rem:
                        g = special_dow_rem.groupdict()
                        he, last = g.get("he", ""), g.get("last", "")
                        if he:
                            e = he
                            try:
                                nth = int(last)
                                assert 5 >= nth >= 1
                            except (KeyError, ValueError, AssertionError):
                                raise CroniterBadCronError(
                                    f"[{expr_format}] is not acceptable."
                                    f" Invalid day_of_week value: '{nth}'"
                                )
                        elif last:
                            e = last
                            nth = g["pre"]  # 'l'

                # Before matching step_search_re, normalize "*" to "{min}-{max}".
                # Example: in the minute field, "*/5" normalizes to "0-59/5"
                t = re.sub(
                    r"^\*(\/.+)$",
                    r"%d-%d\1" % (cls.RANGES[field_index][0], cls.RANGES[field_index][1]),
                    str(e),
                )
                m = step_search_re.search(t)

                if not m:
                    # Before matching step_search_re,
                    # normalize "{start}/{step}" to "{start}-{max}/{step}".
                    # Example: in the minute field, "10/5" normalizes to "10-59/5"
                    t = re.sub(r"^(.+)\/(.+)$", r"\1-%d/\2" % (cls.RANGES[field_index][1]), str(e))
                    m = step_search_re.search(t)

                if m:
                    # early abort if low/high are out of bounds
                    (low, high, step) = m.group(1), m.group(2), m.group(4) or 1
                    if field_index == DAY_FIELD and high == "l":
                        high = "31"

                    if not only_int_re.search(low):
                        low = str(cls._alphaconv(field_index, low, expressions))

                    if not only_int_re.search(high):
                        high = str(cls._alphaconv(field_index, high, expressions))

                    # normally, it's already guarded by the RE that should not accept
                    # not-int values.
                    if not only_int_re.search(str(step)):
                        raise CroniterBadCronError(
                            f"[{expr_format}] step '{step}'"
                            f" in field {field_index} is not acceptable"
                        )
                    step = int(step)

                    for band in low, high:
                        if not only_int_re.search(str(band)):
                            raise CroniterBadCronError(
                                f"[{expr_format}] bands '{low}-{high}'"
                                f" in field {field_index} are not acceptable"
                            )

                    low, high = (
                        cls.value_alias(int(_val), field_index, expressions)
                        for _val in (low, high)
                    )

                    if max(low, high) > max(
                        cls.RANGES[field_index][0], cls.RANGES[field_index][1]
                    ):
                        raise CroniterBadCronError(f"{expr_format} is out of bands")

                    if from_timestamp:
                        low = cls._get_low_from_current_date_number(
                            field_index, int(step), int(from_timestamp)
                        )

                    # Handle when the second bound of the range is in backtracking order:
                    # eg: X-Sun or X-7 (Sat-Sun) in DOW, or X-Jan (Apr-Jan) in MONTH
                    if low > high:
                        whole_field_range = list(
                            range(cls.RANGES[field_index][0], cls.RANGES[field_index][1] + 1, 1)
                        )
                        # Add FirstBound -> ENDRANGE, respecting step
                        rng = list(range(low, cls.RANGES[field_index][1] + 1, step))
                        # Then 0 -> SecondBound, but skipping n first occurences according to step
                        # EG to respect such expressions : Apr-Jan/3
                        to_skip = 0
                        if rng:
                            already_skipped = list(reversed(whole_field_range)).index(rng[-1])
                            curpos = whole_field_range.index(rng[-1])
                            if ((curpos + step) > len(whole_field_range)) and (
                                already_skipped < step
                            ):
                                to_skip = step - already_skipped
                        rng += list(range(cls.RANGES[field_index][0] + to_skip, high + 1, step))
                    # if we include a range type: Jan-Jan, or Sun-Sun,
                    #  it means the whole cycle (all days of week, # all monthes of year, etc)
                    elif low == high:
                        rng = list(
                            range(cls.RANGES[field_index][0], cls.RANGES[field_index][1] + 1, step)
                        )
                    else:
                        try:
                            rng = list(range(low, high + 1, step))
                        except ValueError as exc:
                            raise CroniterBadCronError(f"invalid range: {exc}")

                    if field_index == DOW_FIELD and nth and nth != "l":
                        rng = [f"{item}#{nth}" for item in rng]
                    e_list += [a for a in rng if a not in e_list]
                else:
                    if t.startswith("-"):
                        raise CroniterBadCronError(
                            f"[{expr_format}] is not acceptable, negative numbers not allowed"
                        )
                    if not star_or_int_re.search(t):
                        t = cls._alphaconv(field_index, t, expressions)

                    try:
                        t = int(t)
                    except ValueError:
                        pass

                    t = cls.value_alias(t, field_index, expressions)

                    if t not in ["*", "l"] and (
                        int(t) < cls.RANGES[field_index][0] or int(t) > cls.RANGES[field_index][1]
                    ):
                        raise CroniterBadCronError(
                            f"[{expr_format}] is not acceptable, out of range"
                        )

                    res.append(t)

                    if field_index == DOW_FIELD and nth:
                        if t not in nth_weekday_of_month:
                            nth_weekday_of_month[t] = set()
                        nth_weekday_of_month[t].add(nth)

            res = set(res)
            res = sorted(res, key=lambda i: f"{i:02}" if isinstance(i, int) else i)
            if len(res) == cls.LEN_MEANS_ALL[field_index]:
                # Make sure the wildcard is used in the correct way (avoid over-optimization)
                if (field_index == DAY_FIELD and "*" not in expressions[DOW_FIELD]) or (
                    field_index == DOW_FIELD and "*" not in expressions[DAY_FIELD]
                ):
                    pass
                else:
                    res = ["*"]

            expanded.append(["*"] if (len(res) == 1 and res[0] == "*") else res)

        # Check to make sure the dow combo in use is supported
        if nth_weekday_of_month:
            dow_expanded_set = set(expanded[DOW_FIELD])
            dow_expanded_set = dow_expanded_set.difference(nth_weekday_of_month.keys())
            dow_expanded_set.discard("*")
            # Skip: if it's all weeks instead of wildcard
            if dow_expanded_set and len(set(expanded[DOW_FIELD])) != cls.LEN_MEANS_ALL[DOW_FIELD]:
                raise CroniterUnsupportedSyntaxError(
                    f"day-of-week field does not support mixing literal values and nth"
                    f" day of week syntax.  Cron: '{expr_format}'"
                    f"    dow={dow_expanded_set} vs nth={nth_weekday_of_month}"
                )

        EXPRESSIONS[(expr_format, hash_id, second_at_beginning)] = expressions
        return expanded, nth_weekday_of_month

    @classmethod
    def expand(
        cls,
        expr_format: str,
        hash_id: Optional[Union[bytes, str]] = None,
        second_at_beginning: bool = False,
        from_timestamp: Optional[float] = None,
    ) -> tuple[list[ExpandedExpression], dict[int, set[int]]]:
        """
        Expand a cron expression format into a noramlized format of
        list[list[int | 'l' | '*']]. The first list representing each element
        of the epxression, and each sub-list representing the allowed values
        for that expression component.

        A tuple is returned, the first value being the expanded epxression
        list, and the second being a `nth_weekday_of_month` mapping.

        Examples:

        # Every minute
        >>> croniter.expand('* * * * *')
        ([['*'], ['*'], ['*'], ['*'], ['*']], {})

        # On the hour
        >>> croniter.expand('0 0 * * *')
        ([[0], [0], ['*'], ['*'], ['*']], {})

        # Hours 0-5 and 10 monday through friday
        >>> croniter.expand('0-5,10 * * * mon-fri')
        ([[0, 1, 2, 3, 4, 5, 10], ['*'], ['*'], ['*'], [1, 2, 3, 4, 5]], {})

        Note that some special values such as nth day of week are expanded to a
        special mapping format for later processing:

        # Every minute on the 3rd tuesday of the month
        >>> croniter.expand('* * * * 2#3')
        ([['*'], ['*'], ['*'], ['*'], [2]], {2: {3}})

        # Every hour on the last day of the month
        >>> croniter.expand('0 * l * *')
        ([[0], ['*'], ['l'], ['*'], ['*']], {})

        # On the hour every 15 seconds
        >>> croniter.expand('0 0 * * * */15')
        ([[0], [0], ['*'], ['*'], ['*'], [0, 15, 30, 45]], {})
        """
        try:
            return cls._expand(
                expr_format,
                hash_id=hash_id,
                second_at_beginning=second_at_beginning,
                from_timestamp=from_timestamp,
            )
        except (ValueError,) as exc:
            if isinstance(exc, CroniterError):
                raise
            trace = _traceback.format_exc()
            raise CroniterBadCronError(trace)

    @classmethod
    def _get_low_from_current_date_number(cls, field_index, step, from_timestamp):
        dt = datetime.datetime.fromtimestamp(from_timestamp, tz=UTC_DT)
        if field_index == MINUTE_FIELD:
            return dt.minute % step
        if field_index == HOUR_FIELD:
            return dt.hour % step
        if field_index == DAY_FIELD:
            return ((dt.day - 1) % step) + 1
        if field_index == MONTH_FIELD:
            return dt.month % step
        if field_index == DOW_FIELD:
            return (dt.weekday() + 1) % step

        raise ValueError("Can't get current date number for index larger than 4")

    @classmethod
    def is_valid(cls, expression, hash_id=None, encoding="UTF-8", second_at_beginning=False):
        if hash_id:
            if not isinstance(hash_id, (bytes, str)):
                raise TypeError("hash_id must be bytes or UTF-8 string")
            if not isinstance(hash_id, bytes):
                hash_id = hash_id.encode(encoding)
        try:
            cls.expand(expression, hash_id=hash_id, second_at_beginning=second_at_beginning)
        except CroniterError:
            return False
        return True

    @classmethod
    def match(cls, cron_expression, testdate, day_or=True, second_at_beginning=False):
        return cls.match_range(cron_expression, testdate, testdate, day_or, second_at_beginning)

    @classmethod
    def match_range(
        cls, cron_expression, from_datetime, to_datetime, day_or=True, second_at_beginning=False
    ):
        cron = cls(
            cron_expression,
            to_datetime,
            ret_type=datetime.datetime,
            day_or=day_or,
            second_at_beginning=second_at_beginning,
        )
        tdp = cron.get_current(datetime.datetime)
        if not tdp.microsecond:
            tdp += relativedelta(microseconds=1)
        cron.set_current(tdp, force=True)
        try:
            tdt = cron.get_prev()
        except CroniterBadDateError:
            return False
        precision_in_seconds = 1 if len(cron.expanded) > UNIX_CRON_LEN else 60
        duration_in_second = (to_datetime - from_datetime).total_seconds() + precision_in_seconds
        return (max(tdp, tdt) - min(tdp, tdt)).total_seconds() < duration_in_second


def croniter_range(
    start,
    stop,
    expr_format,
    ret_type=None,
    day_or=True,
    exclude_ends=False,
    _croniter=None,
    second_at_beginning=False,
    expand_from_start_time=False,
):
    """
    Generator that provides all times from start to stop matching the given cron expression.
    If the cron expression matches either 'start' and/or 'stop', those times will be returned as
    well unless 'exclude_ends=True' is passed.

    You can think of this function as sibling to the builtin range function for datetime objects.
    Like range(start,stop,step), except that here 'step' is a cron expression.
    """
    _croniter = _croniter or croniter
    auto_rt = datetime.datetime
    # type is used in first if branch for perfs reasons
    if type(start) is not type(stop) and not (
        isinstance(start, type(stop)) or isinstance(stop, type(start))
    ):
        raise CroniterBadTypeRangeError(
            f"The start and stop must be same type.  {type(start)} != {type(stop)}"
        )
    if isinstance(start, (float, int)):
        start, stop = (
            datetime.datetime.fromtimestamp(t, tzutc()).replace(tzinfo=None) for t in (start, stop)
        )
        auto_rt = float
    if ret_type is None:
        ret_type = auto_rt
    if not exclude_ends:
        ms1 = relativedelta(microseconds=1)
        if start < stop:  # Forward (normal) time order
            start -= ms1
            stop += ms1
        else:  # Reverse time order
            start += ms1
            stop -= ms1
    year_span = math.floor(abs(stop.year - start.year)) + 1
    ic = _croniter(
        expr_format,
        start,
        ret_type=datetime.datetime,
        day_or=day_or,
        max_years_between_matches=year_span,
        second_at_beginning=second_at_beginning,
        expand_from_start_time=expand_from_start_time,
    )
    # define a continue (cont) condition function and step function for the main while loop
    if start < stop:  # Forward

        def cont(v):
            return v < stop

        step = ic.get_next
    else:  # Reverse

        def cont(v):
            return v > stop

        step = ic.get_prev
    try:
        dt = step()
        while cont(dt):
            if ret_type is float:
                yield ic.get_current(float)
            else:
                yield dt
            dt = step()
    except CroniterBadDateError:
        # Stop iteration when this exception is raised; no match found within the given year range
        return


class HashExpander:
    def __init__(self, cronit):
        self.cron = cronit

    def do(self, idx, hash_type="h", hash_id=None, range_end=None, range_begin=None):
        """Return a hashed/random integer given range/hash information"""
        if range_end is None:
            range_end = self.cron.RANGES[idx][1]
        if range_begin is None:
            range_begin = self.cron.RANGES[idx][0]
        if hash_type == "r":
            crc = random.randint(0, 0xFFFFFFFF)
        else:
            crc = binascii.crc32(hash_id) & 0xFFFFFFFF
        return ((crc >> idx) % (range_end - range_begin + 1)) + range_begin

    def match(self, efl, idx, expr, hash_id=None, **kw):
        return hash_expression_re.match(expr)

    def expand(self, efl, idx, expr, hash_id=None, match="", **kw):
        """Expand a hashed/random expression to its normal representation"""
        if match == "":
            match = self.match(efl, idx, expr, hash_id, **kw)
        if not match:
            return expr
        m = match.groupdict()

        if m["hash_type"] == "h" and hash_id is None:
            raise CroniterBadCronError("Hashed definitions must include hash_id")

        if m["range_begin"] and m["range_end"]:
            if int(m["range_begin"]) >= int(m["range_end"]):
                raise CroniterBadCronError("Range end must be greater than range begin")

        if m["range_begin"] and m["range_end"] and m["divisor"]:
            # Example: H(30-59)/10 -> 34-59/10 (i.e. 34,44,54)
            if int(m["divisor"]) == 0:
                raise CroniterBadCronError(f"Bad expression: {expr}")

            x = self.do(
                idx,
                hash_type=m["hash_type"],
                hash_id=hash_id,
                range_begin=int(m["range_begin"]),
                range_end=int(m["divisor"]) - 1 + int(m["range_begin"]),
            )
            return f"{x}-{int(m['range_end'])}/{int(m['divisor'])}"
        if m["range_begin"] and m["range_end"]:
            # Example: H(0-29) -> 12
            return str(
                self.do(
                    idx,
                    hash_type=m["hash_type"],
                    hash_id=hash_id,
                    range_end=int(m["range_end"]),
                    range_begin=int(m["range_begin"]),
                )
            )
        if m["divisor"]:
            # Example: H/15 -> 7-59/15 (i.e. 7,22,37,52)
            if int(m["divisor"]) == 0:
                raise CroniterBadCronError(f"Bad expression: {expr}")

            x = self.do(
                idx,
                hash_type=m["hash_type"],
                hash_id=hash_id,
                range_begin=self.cron.RANGES[idx][0],
                range_end=int(m["divisor"]) - 1 + self.cron.RANGES[idx][0],
            )
            return f"{x}-{self.cron.RANGES[idx][1]}/{int(m['divisor'])}"

        # Example: H -> 32
        return str(self.do(idx, hash_type=m["hash_type"], hash_id=hash_id))


EXPANDERS = {"hash": HashExpander}
croniter-6.1.0rc1/src/croniter/tests/000077500000000000000000000000001507066033300175325ustar00rootroot00000000000000croniter-6.1.0rc1/src/croniter/tests/__init__.py000066400000000000000000000000001507066033300216310ustar00rootroot00000000000000croniter-6.1.0rc1/src/croniter/tests/base.py000066400000000000000000000004001507066033300210100ustar00rootroot00000000000000import typing
import unittest


class TestCase(unittest.TestCase):
    """
    We use this base class for all the tests in this package.
    If necessary, we can put common utility or setup code in here.
    """

    maxDiff: typing.Optional[int] = 10**10
croniter-6.1.0rc1/src/croniter/tests/test_croniter.py000077500000000000000000003123061507066033300230000ustar00rootroot00000000000000#!/usr/bin/env python

import unittest
import zoneinfo
from datetime import datetime, timedelta
from functools import partial
from time import sleep

import dateutil.tz
import pytz

from croniter import (
    CroniterBadCronError,
    CroniterBadDateError,
    CroniterNotAlphaError,
    CroniterUnsupportedSyntaxError,
    croniter,
    datetime_to_timestamp,
)
from croniter.croniter import VALID_LEN_EXPRESSION
from croniter.tests import base


class CroniterTest(base.TestCase):
    def test_second_sec(self):
        base = datetime(2012, 4, 6, 13, 26, 10)
        itr = croniter("* * * * * 15,25", base)
        n = itr.get_next(datetime)
        self.assertEqual(15, n.second)
        n = itr.get_next(datetime)
        self.assertEqual(25, n.second)
        n = itr.get_next(datetime)
        self.assertEqual(15, n.second)
        self.assertEqual(27, n.minute)

    def test_second(self):
        base = datetime(2012, 4, 6, 13, 26, 10)
        itr = croniter("*/1 * * * * *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(base.year, n1.year)
        self.assertEqual(base.month, n1.month)
        self.assertEqual(base.day, n1.day)
        self.assertEqual(base.hour, n1.hour)
        self.assertEqual(base.minute, n1.minute)
        self.assertEqual(base.second + 1, n1.second)

    def test_second_repeat(self):
        base = datetime(2012, 4, 6, 13, 26, 36)
        itr = croniter("* * * * * */15", base)
        n1 = itr.get_next(datetime)
        n2 = itr.get_next(datetime)
        n3 = itr.get_next(datetime)
        self.assertEqual(base.year, n1.year)
        self.assertEqual(base.month, n1.month)
        self.assertEqual(base.day, n1.day)
        self.assertEqual(base.hour, n1.hour)
        self.assertEqual(base.minute, n1.minute)
        self.assertEqual(45, n1.second)
        self.assertEqual(base.year, n2.year)
        self.assertEqual(base.month, n2.month)
        self.assertEqual(base.day, n2.day)
        self.assertEqual(base.hour, n2.hour)
        self.assertEqual(base.minute + 1, n2.minute)
        self.assertEqual(0, n2.second)
        self.assertEqual(base.year, n3.year)
        self.assertEqual(base.month, n3.month)
        self.assertEqual(base.day, n3.day)
        self.assertEqual(base.hour, n3.hour)
        self.assertEqual(base.minute + 1, n3.minute)
        self.assertEqual(15, n3.second)

    def test_minute(self):
        # minute asterisk
        base = datetime(2010, 1, 23, 12, 18)
        itr = croniter("*/1 * * * *", base)
        n1 = itr.get_next(datetime)  # 19
        self.assertEqual(base.year, n1.year)
        self.assertEqual(base.month, n1.month)
        self.assertEqual(base.day, n1.day)
        self.assertEqual(base.hour, n1.hour)
        self.assertEqual(base.minute, n1.minute - 1)
        for i in range(39):  # ~ 58
            itr.get_next()
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.minute, 59)
        n3 = itr.get_next(datetime)
        self.assertEqual(n3.minute, 0)
        self.assertEqual(n3.hour, 13)

        itr = croniter("*/5 * * * *", base)
        n4 = itr.get_next(datetime)
        self.assertEqual(n4.minute, 20)
        for i in range(6):
            itr.get_next()
        n5 = itr.get_next(datetime)
        self.assertEqual(n5.minute, 55)
        n6 = itr.get_next(datetime)
        self.assertEqual(n6.minute, 0)
        self.assertEqual(n6.hour, 13)

    def test_hour(self):
        base = datetime(2010, 1, 24, 12, 2)
        itr = croniter("0 */3 * * *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.hour, 15)
        self.assertEqual(n1.minute, 0)
        for i in range(2):
            itr.get_next()
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.hour, 0)
        self.assertEqual(n2.day, 25)

    def test_day(self):
        base = datetime(2010, 2, 24, 12, 9)
        itr = croniter("0 0 */3 * *", base)
        n1 = itr.get_next(datetime)
        # 1 4 7 10 13 16 19 22 25 28
        self.assertEqual(n1.day, 25)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.day, 28)
        n3 = itr.get_next(datetime)
        self.assertEqual(n3.day, 1)
        self.assertEqual(n3.month, 3)

        # test leap year
        base = datetime(1996, 2, 27)
        itr = croniter("0 0 * * *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.day, 28)
        self.assertEqual(n1.month, 2)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.day, 29)
        self.assertEqual(n2.month, 2)

        base2 = datetime(2000, 2, 27)
        itr2 = croniter("0 0 * * *", base2)
        n3 = itr2.get_next(datetime)
        self.assertEqual(n3.day, 28)
        self.assertEqual(n3.month, 2)
        n4 = itr2.get_next(datetime)
        self.assertEqual(n4.day, 29)
        self.assertEqual(n4.month, 2)

    def test_day2(self):
        base3 = datetime(2024, 2, 28)
        itr2 = croniter("* * 29 2 *", base3)
        n3 = itr2.get_prev(datetime)
        self.assertEqual(n3.year, 2020)
        self.assertEqual(n3.month, 2)
        self.assertEqual(n3.day, 29)

    def test_weekday(self):
        base = datetime(2010, 2, 25)
        itr = croniter("0 0 * * sat", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.isoweekday(), 6)
        self.assertEqual(n1.day, 27)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.isoweekday(), 6)
        self.assertEqual(n2.day, 6)
        self.assertEqual(n2.month, 3)

        base = datetime(2010, 1, 25)
        itr = croniter("0 0 1 * wed", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.month, 1)
        self.assertEqual(n1.day, 27)
        self.assertEqual(n1.year, 2010)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.month, 2)
        self.assertEqual(n2.day, 1)
        self.assertEqual(n2.year, 2010)
        n3 = itr.get_next(datetime)
        self.assertEqual(n3.month, 2)
        self.assertEqual(n3.day, 3)
        self.assertEqual(n3.year, 2010)

    def test_nth_weekday(self):
        base = datetime(2010, 2, 25)
        itr = croniter("0 0 * * sat#1", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.isoweekday(), 6)
        self.assertEqual(n1.day, 6)
        self.assertEqual(n1.month, 3)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.isoweekday(), 6)
        self.assertEqual(n2.day, 3)
        self.assertEqual(n2.month, 4)

        base = datetime(2010, 1, 25)
        itr = croniter("0 0 * * wed#5", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.month, 3)
        self.assertEqual(n1.day, 31)
        self.assertEqual(n1.year, 2010)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.month, 6)
        self.assertEqual(n2.day, 30)
        self.assertEqual(n2.year, 2010)
        n3 = itr.get_next(datetime)
        self.assertEqual(n3.month, 9)
        self.assertEqual(n3.day, 29)
        self.assertEqual(n3.year, 2010)

    def test_weekday_day_and(self):
        base = datetime(2010, 1, 25)
        itr = croniter("0 0 1 * mon", base, day_or=False)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.month, 2)
        self.assertEqual(n1.day, 1)
        self.assertEqual(n1.year, 2010)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.month, 3)
        self.assertEqual(n2.day, 1)
        self.assertEqual(n2.year, 2010)
        n3 = itr.get_next(datetime)
        self.assertEqual(n3.month, 11)
        self.assertEqual(n3.day, 1)
        self.assertEqual(n3.year, 2010)

    def test_dom_dow_vixie_cron_bug(self):
        expr = "0 16 */2 * sat"

        # UNION OF "every odd-numbered day" and "every Saturday"
        itr = croniter(expr, start_time=datetime(2023, 5, 2), ret_type=datetime)
        self.assertEqual(itr.get_next(), datetime(2023, 5, 3, 16, 0, 0))  # Wed May 3 2023
        self.assertEqual(itr.get_next(), datetime(2023, 5, 5, 16, 0, 0))  # Fri May 5 2023
        self.assertEqual(itr.get_next(), datetime(2023, 5, 6, 16, 0, 0))  # Sat May 6 2023
        self.assertEqual(itr.get_next(), datetime(2023, 5, 7, 16, 0, 0))  # Sun May 7 2023

        # INTERSECTION OF "every odd-numbered day" and "every Saturday"
        itr = croniter(
            expr, start_time=datetime(2023, 5, 2), ret_type=datetime, implement_cron_bug=True
        )
        self.assertEqual(itr.get_next(), datetime(2023, 5, 13, 16, 0, 0))  # Sat May  13 2023
        self.assertEqual(itr.get_next(), datetime(2023, 5, 27, 16, 0, 0))  # Sat May  27 2023
        self.assertEqual(itr.get_next(), datetime(2023, 6, 3, 16, 0, 0))  # Sat June  3 2023
        self.assertEqual(itr.get_next(), datetime(2023, 6, 17, 16, 0, 0))  # Sun June 17 2023

    def test_month(self):
        base = datetime(2010, 1, 25)
        itr = croniter("0 0 1 * *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.month, 2)
        self.assertEqual(n1.day, 1)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.month, 3)
        self.assertEqual(n2.day, 1)
        for i in range(8):
            itr.get_next()
        n3 = itr.get_next(datetime)
        self.assertEqual(n3.month, 12)
        self.assertEqual(n3.year, 2010)
        n4 = itr.get_next(datetime)
        self.assertEqual(n4.month, 1)
        self.assertEqual(n4.year, 2011)

    def test_last_day_of_month(self):
        base = datetime(2015, 9, 4)
        itr = croniter("0 0 l * *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.month, 9)
        self.assertEqual(n1.day, 30)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.month, 10)
        self.assertEqual(n2.day, 31)
        n3 = itr.get_next(datetime)
        self.assertEqual(n3.month, 11)
        self.assertEqual(n3.day, 30)
        n4 = itr.get_next(datetime)
        self.assertEqual(n4.month, 12)
        self.assertEqual(n4.day, 31)

    def test_range_with_uppercase_last_day_of_month(self):
        base = datetime(2015, 9, 4)
        itr = croniter("0 0 29-L * *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.month, 9)
        self.assertEqual(n1.day, 29)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.month, 9)
        self.assertEqual(n2.day, 30)

    def test_prev_last_day_of_month(self):
        base = datetime(2009, 12, 31, hour=20)
        itr = croniter("0 0 l * *", base)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.month, 12)
        self.assertEqual(n1.day, 31)

        base = datetime(2009, 12, 31)
        itr = croniter("0 0 l * *", base)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.month, 11)
        self.assertEqual(n1.day, 30)

        base = datetime(2010, 1, 5)
        itr = croniter("0 0 l * *", base)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.month, 12)
        self.assertEqual(n1.day, 31)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.month, 11)
        self.assertEqual(n1.day, 30)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.month, 10)
        self.assertEqual(n1.day, 31)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.month, 9)
        self.assertEqual(n1.day, 30)

        base = datetime(2010, 1, 31, minute=2)
        itr = croniter("* * l * *", base)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.month, 1)
        self.assertEqual(n1.day, 31)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.month, 1)
        self.assertEqual(n1.day, 31)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.month, 12)
        self.assertEqual(n1.day, 31)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.month, 12)
        self.assertEqual(n1.day, 31)

    def test_error(self):
        itr = croniter("* * * * *")
        self.assertRaises(TypeError, itr.get_next, str)
        self.assertRaises(ValueError, croniter, "* * * *")
        self.assertRaises(ValueError, croniter, "-90 * * * *")
        self.assertRaises(ValueError, croniter, "a * * * *")
        self.assertRaises(ValueError, croniter, "* * * janu-jun *")
        self.assertRaises(ValueError, croniter, "1-1_0 * * * *")
        self.assertRaises(ValueError, croniter, "0-10/error * * * *")
        self.assertRaises(ValueError, croniter, "0-10/ * * * *")
        self.assertRaises(CroniterBadCronError, croniter, "0-1& * * * *", datetime.now())
        self.assertRaises(ValueError, croniter, "* * 5-100 * *")

    def test_sunday_to_thursday_with_alpha_conversion(self):
        base = datetime(2010, 8, 25, 15, 56)  # wednesday
        itr = croniter("30 22 * * sun-thu", base)
        next = itr.get_next(datetime)

        self.assertEqual(base.year, next.year)
        self.assertEqual(base.month, next.month)
        self.assertEqual(base.day, next.day)
        self.assertEqual(22, next.hour)
        self.assertEqual(30, next.minute)

    def test_optimize_cron_expressions(self):
        """Non-optimal cron expressions that can be simplified."""
        wildcard = ["*"]
        m, h, d, mon, dow, s = range(6)
        # Test each field individually
        self.assertEqual(croniter("0-59 0 1 1 0").expanded[m], wildcard)
        self.assertEqual(croniter("0 0-23 1 1 0").expanded[h], wildcard)
        self.assertEqual(croniter("0 0 1-31 1 0").expanded[dow], [0])
        self.assertEqual(croniter("0 0 1-31 1 *").expanded[d], wildcard)
        self.assertEqual(croniter("0 0 1 1-12 0").expanded[mon], wildcard)
        self.assertEqual(croniter("0 0 1 1 0-6").expanded[dow], [0, 1, 2, 3, 4, 5, 6])
        self.assertEqual(croniter("0 0 * 1 0-6").expanded[dow], wildcard)
        self.assertEqual(croniter("0 0 1 1 0-6").expanded[dow], [0, 1, 2, 3, 4, 5, 6])
        self.assertEqual(croniter("0 0 1 1 0-6,sat#3").expanded[dow], [0, 1, 2, 3, 4, 5, 6])
        self.assertEqual(croniter("0 0 * 1 0-6").expanded[dow], wildcard)
        self.assertEqual(croniter("0 0 * 1 0-6,sat#3").expanded[dow], wildcard)
        self.assertEqual(croniter("0 0 1 1 0 0-59").expanded[s], wildcard)
        # Real life examples
        self.assertEqual(croniter("30 1-12,0,10-23 15-21 * fri").expanded[h], wildcard)
        self.assertEqual(croniter("30 1-23,0 15-21 * fri").expanded[h], wildcard)

    def test_block_dup_ranges(self):
        """Ensure that duplicate/overlapping ranges are squashed"""
        m, h, d, mon, dow, s = range(6)
        self.assertEqual(croniter("* 5,5,1-6 * * *").expanded[h], [1, 2, 3, 4, 5, 6])
        self.assertEqual(croniter("* * * * 2-3,4-5,3,3,3").expanded[dow], [2, 3, 4, 5])
        self.assertEqual(croniter("* * * * * 1,5,*/20,20,15").expanded[s], [0, 1, 5, 15, 20, 40])
        self.assertEqual(croniter("* 4,1-4,5,4 * * *").expanded[h], [1, 2, 3, 4, 5])
        # Real life example
        self.assertEqual(
            croniter("59 23 * 1 wed,fri,mon-thu,tue,tue").expanded[dow], [1, 2, 3, 4, 5]
        )

    def test_prev_minute(self):
        base = datetime(2010, 8, 25, 15, 56)
        itr = croniter("*/1 * * * *", base)
        prev = itr.get_prev(datetime)
        self.assertEqual(base.year, prev.year)
        self.assertEqual(base.month, prev.month)
        self.assertEqual(base.day, prev.day)
        self.assertEqual(base.hour, prev.hour)
        self.assertEqual(base.minute, prev.minute + 1)

        base = datetime(2010, 8, 25, 15, 0)
        itr = croniter("*/1 * * * *", base)
        prev = itr.get_prev(datetime)
        self.assertEqual(base.year, prev.year)
        self.assertEqual(base.month, prev.month)
        self.assertEqual(base.day, prev.day)
        self.assertEqual(base.hour, prev.hour + 1)
        self.assertEqual(59, prev.minute)

        base = datetime(2010, 8, 25, 0, 0)
        itr = croniter("*/1 * * * *", base)
        prev = itr.get_prev(datetime)
        self.assertEqual(base.year, prev.year)
        self.assertEqual(base.month, prev.month)
        self.assertEqual(base.day, prev.day + 1)
        self.assertEqual(23, prev.hour)
        self.assertEqual(59, prev.minute)

    def test_prev_day_of_month_with_crossing(self):
        """
        Test getting previous occurrence that crosses into previous month.
        """
        base = datetime(2012, 3, 15, 0, 0)
        itr = croniter("0 0 22 * *", base)
        prev = itr.get_prev(datetime)
        self.assertEqual(prev.year, 2012)
        self.assertEqual(prev.month, 2)
        self.assertEqual(prev.day, 22)
        self.assertEqual(prev.hour, 0)
        self.assertEqual(prev.minute, 0)

    def test_prev_weekday(self):
        base = datetime(2010, 8, 25, 15, 56)
        itr = croniter("0 0 * * sat,sun", base)
        prev1 = itr.get_prev(datetime)
        self.assertEqual(prev1.year, base.year)
        self.assertEqual(prev1.month, base.month)
        self.assertEqual(prev1.day, 22)
        self.assertEqual(prev1.hour, 0)
        self.assertEqual(prev1.minute, 0)

        prev2 = itr.get_prev(datetime)
        self.assertEqual(prev2.year, base.year)
        self.assertEqual(prev2.month, base.month)
        self.assertEqual(prev2.day, 21)
        self.assertEqual(prev2.hour, 0)
        self.assertEqual(prev2.minute, 0)

        prev3 = itr.get_prev(datetime)
        self.assertEqual(prev3.year, base.year)
        self.assertEqual(prev3.month, base.month)
        self.assertEqual(prev3.day, 15)
        self.assertEqual(prev3.hour, 0)
        self.assertEqual(prev3.minute, 0)

    def test_prev_nth_weekday(self):
        base = datetime(2010, 8, 25, 15, 56)
        itr = croniter("0 0 * * sat#1,sun#2", base)
        prev1 = itr.get_prev(datetime)
        self.assertEqual(prev1.year, base.year)
        self.assertEqual(prev1.month, base.month)
        self.assertEqual(prev1.day, 8)
        self.assertEqual(prev1.hour, 0)
        self.assertEqual(prev1.minute, 0)

        prev2 = itr.get_prev(datetime)
        self.assertEqual(prev2.year, base.year)
        self.assertEqual(prev2.month, base.month)
        self.assertEqual(prev2.day, 7)
        self.assertEqual(prev2.hour, 0)
        self.assertEqual(prev2.minute, 0)

        prev3 = itr.get_prev(datetime)
        self.assertEqual(prev3.year, base.year)
        self.assertEqual(prev3.month, 7)
        self.assertEqual(prev3.day, 11)
        self.assertEqual(prev3.hour, 0)
        self.assertEqual(prev3.minute, 0)

    def test_prev_weekday2(self):
        base = datetime(2010, 8, 25, 15, 56)
        itr = croniter("10 0 * * 0", base)
        prev = itr.get_prev(datetime)
        self.assertEqual(prev.day, 22)
        self.assertEqual(prev.hour, 0)
        self.assertEqual(prev.minute, 10)

    def test_iso_weekday(self):
        base = datetime(2010, 2, 25)
        itr = croniter("0 0 * * 6", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.isoweekday(), 6)
        self.assertEqual(n1.day, 27)
        n2 = itr.get_next(datetime)
        self.assertEqual(n2.isoweekday(), 6)
        self.assertEqual(n2.day, 6)
        self.assertEqual(n2.month, 3)

    def test_bug1(self):
        base = datetime(2012, 2, 24)
        itr = croniter("5 0 */2 * *", base)
        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.hour, 0)
        self.assertEqual(n1.minute, 5)
        self.assertEqual(n1.month, 2)
        # month starts from 1, 3 .... then 21, 23
        # so correct is not 22  but 23
        self.assertEqual(n1.day, 23)

    def test_bug2(self):
        base = datetime(2012, 1, 1, 0, 0)
        iter = croniter("0 * * 3 *", base)
        n1 = iter.get_next(datetime)
        self.assertEqual(n1.year, base.year)
        self.assertEqual(n1.month, 3)
        self.assertEqual(n1.day, base.day)
        self.assertEqual(n1.hour, base.hour)
        self.assertEqual(n1.minute, base.minute)

        n2 = iter.get_next(datetime)
        self.assertEqual(n2.year, base.year)
        self.assertEqual(n2.month, 3)
        self.assertEqual(n2.day, base.day)
        self.assertEqual(n2.hour, base.hour + 1)
        self.assertEqual(n2.minute, base.minute)

        n3 = iter.get_next(datetime)
        self.assertEqual(n3.year, base.year)
        self.assertEqual(n3.month, 3)
        self.assertEqual(n3.day, base.day)
        self.assertEqual(n3.hour, base.hour + 2)
        self.assertEqual(n3.minute, base.minute)

    def test_bug3(self):
        base = datetime(2013, 3, 1, 12, 17, 34, 257877)
        c = croniter("00 03 16,30 * *", base)

        n1 = c.get_next(datetime)
        self.assertEqual(n1.month, 3)
        self.assertEqual(n1.day, 16)

        n2 = c.get_next(datetime)
        self.assertEqual(n2.month, 3)
        self.assertEqual(n2.day, 30)

        n3 = c.get_next(datetime)
        self.assertEqual(n3.month, 4)
        self.assertEqual(n3.day, 16)

        n4 = c.get_prev(datetime)
        self.assertEqual(n4.month, 3)
        self.assertEqual(n4.day, 30)

        n5 = c.get_prev(datetime)
        self.assertEqual(n5.month, 3)
        self.assertEqual(n5.day, 16)

        n6 = c.get_prev(datetime)
        self.assertEqual(n6.month, 2)
        self.assertEqual(n6.day, 16)

    def test_bug34(self):
        base = datetime(2012, 2, 24, 0, 0, 0)
        itr = croniter("* * 31 2 *", base)
        try:
            itr.get_next(datetime)
        except (CroniterBadDateError,) as ex:
            self.assertEqual(f"{ex}", "failed to find next date")

    def test_bug57(self):
        base = datetime(2012, 2, 24, 0, 0, 0)
        itr = croniter("0 4/6 * * *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.hour, 4)
        self.assertEqual(n1.minute, 0)
        self.assertEqual(n1.month, 2)
        self.assertEqual(n1.day, 24)

        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.hour, 22)
        self.assertEqual(n1.minute, 0)
        self.assertEqual(n1.month, 2)
        self.assertEqual(n1.day, 23)

        itr = croniter("0 0/6 * * *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.hour, 6)
        self.assertEqual(n1.minute, 0)
        self.assertEqual(n1.month, 2)
        self.assertEqual(n1.day, 24)

        n1 = itr.get_prev(datetime)
        self.assertEqual(n1.hour, 0)
        self.assertEqual(n1.minute, 0)
        self.assertEqual(n1.month, 2)
        self.assertEqual(n1.day, 24)

    def test_multiple_months(self):
        base = datetime(2016, 3, 1, 0, 0, 0)
        itr = croniter("0 0 1 3,6,9,12 *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.hour, 0)
        self.assertEqual(n1.month, 6)
        self.assertEqual(n1.day, 1)
        self.assertEqual(n1.year, 2016)

        base = datetime(2016, 2, 15, 0, 0, 0)
        itr = croniter("0 0 1 3,6,9,12 *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.hour, 0)
        self.assertEqual(n1.month, 3)
        self.assertEqual(n1.day, 1)
        self.assertEqual(n1.year, 2016)

        base = datetime(2016, 12, 3, 10, 0, 0)
        itr = croniter("0 0 1 3,6,9,12 *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.hour, 0)
        self.assertEqual(n1.month, 3)
        self.assertEqual(n1.day, 1)
        self.assertEqual(n1.year, 2017)

        # The result with this parameters was incorrect.
        # self.assertEqual(p1.month, 12
        # AssertionError: 9 != 12
        base = datetime(2016, 3, 1, 0, 0, 0)
        itr = croniter("0 0 1 3,6,9,12 *", base)
        p1 = itr.get_prev(datetime)
        self.assertEqual(p1.hour, 0)
        self.assertEqual(p1.month, 12)
        self.assertEqual(p1.day, 1)
        self.assertEqual(p1.year, 2015)

        # check my change resolves another hidden bug.
        base = datetime(2016, 2, 1, 0, 0, 0)
        itr = croniter("0 0 1,15,31 * *", base)
        p1 = itr.get_prev(datetime)
        self.assertEqual(p1.hour, 0)
        self.assertEqual(p1.month, 1)
        self.assertEqual(p1.day, 31)
        self.assertEqual(p1.year, 2016)

        base = datetime(2016, 6, 1, 0, 0, 0)
        itr = croniter("0 0 1 3,6,9,12 *", base)
        p1 = itr.get_prev(datetime)
        self.assertEqual(p1.hour, 0)
        self.assertEqual(p1.month, 3)
        self.assertEqual(p1.day, 1)
        self.assertEqual(p1.year, 2016)

        base = datetime(2016, 3, 1, 0, 0, 0)
        itr = croniter("0 0 1 1,3,6,9,12 *", base)
        p1 = itr.get_prev(datetime)
        self.assertEqual(p1.hour, 0)
        self.assertEqual(p1.month, 1)
        self.assertEqual(p1.day, 1)
        self.assertEqual(p1.year, 2016)

        base = datetime(2016, 3, 1, 0, 0, 0)
        itr = croniter("0 0 1 1,3,6,9,12 *", base)
        p1 = itr.get_prev(datetime)
        self.assertEqual(p1.hour, 0)
        self.assertEqual(p1.month, 1)
        self.assertEqual(p1.day, 1)
        self.assertEqual(p1.year, 2016)

    def test_range_generator(self):
        base = datetime(2013, 3, 4, 0, 0)
        itr = croniter("1-9/2 0 1 * *", base)
        n1 = itr.get_next(datetime)
        n2 = itr.get_next(datetime)
        n3 = itr.get_next(datetime)
        n4 = itr.get_next(datetime)
        n5 = itr.get_next(datetime)
        self.assertEqual(n1.minute, 1)
        self.assertEqual(n2.minute, 3)
        self.assertEqual(n3.minute, 5)
        self.assertEqual(n4.minute, 7)
        self.assertEqual(n5.minute, 9)

    def test_previous_hour(self):
        base = datetime(2012, 6, 23, 17, 41)
        itr = croniter("* 10 * * *", base)
        prev1 = itr.get_prev(datetime)
        self.assertEqual(prev1.year, base.year)
        self.assertEqual(prev1.month, base.month)
        self.assertEqual(prev1.day, base.day)
        self.assertEqual(prev1.hour, 10)
        self.assertEqual(prev1.minute, 59)

    def test_previous_day(self):
        base = datetime(2012, 6, 27, 0, 15)
        itr = croniter("* * 26 * *", base)
        prev1 = itr.get_prev(datetime)
        self.assertEqual(prev1.year, base.year)
        self.assertEqual(prev1.month, base.month)
        self.assertEqual(prev1.day, 26)
        self.assertEqual(prev1.hour, 23)
        self.assertEqual(prev1.minute, 59)

    def test_previous_month(self):
        base = datetime(2012, 6, 18, 0, 15)
        itr = croniter("* * * 5 *", base)
        prev1 = itr.get_prev(datetime)
        self.assertEqual(prev1.year, base.year)
        self.assertEqual(prev1.month, 5)
        self.assertEqual(prev1.day, 31)
        self.assertEqual(prev1.hour, 23)
        self.assertEqual(prev1.minute, 59)

    def test_previous_dow(self):
        base = datetime(2012, 5, 13, 18, 48)
        itr = croniter("* * * * sat", base)
        prev1 = itr.get_prev(datetime)
        self.assertEqual(prev1.year, base.year)
        self.assertEqual(prev1.month, base.month)
        self.assertEqual(prev1.day, 12)
        self.assertEqual(prev1.hour, 23)
        self.assertEqual(prev1.minute, 59)

    def test_get_current(self):
        base = datetime(2012, 9, 25, 11, 24)
        itr = croniter("* * * * *", base)
        res = itr.get_current(datetime)
        self.assertEqual(base.year, res.year)
        self.assertEqual(base.month, res.month)
        self.assertEqual(base.day, res.day)
        self.assertEqual(base.hour, res.hour)
        self.assertEqual(base.minute, res.minute)

    def test_first_of_march(self):
        """Test not skipping first of March.

        This fixes https://github.com/pallets-eco/croniter/issues/1
        """
        it = croniter("0 0 */10 * *", datetime(2025, 2, 22))
        self.assertEqual(it.get_next(datetime).isoformat(), "2025-03-01T00:00:00")

    def test_timezone(self):
        base = datetime(2013, 3, 4, 12, 15)
        itr = croniter("* * * * *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.tzinfo, None)

        tokyo = zoneinfo.ZoneInfo("Asia/Tokyo")
        start = datetime(2013, 3, 4, 12, 15, tzinfo=tokyo)
        itr2 = croniter("* * * * *", start)
        n2 = itr2.get_next(datetime)
        self.assertEqual(n2.tzinfo.key, "Asia/Tokyo")

    def test_timezone_pytz(self):
        tokyo = pytz.timezone("Asia/Tokyo")
        base = datetime(2013, 3, 4, 12, 15)
        itr2 = croniter("* * * * *", tokyo.localize(base))
        n2 = itr2.get_next(datetime)
        self.assertEqual(n2.tzinfo.zone, "Asia/Tokyo")

    def test_timezone_dateutil(self):
        tokyo = dateutil.tz.gettz("Asia/Tokyo")
        base = datetime(2013, 3, 4, 12, 15, tzinfo=tokyo)
        itr = croniter("* * * * *", base)
        n1 = itr.get_next(datetime)
        self.assertEqual(n1.tzinfo.tzname(n1), "JST")

    def test_init_no_start_time(self):
        itr = croniter("* * * * *")
        sleep(0.01)
        itr2 = croniter("* * * * *")
        # Greater does not exists in py26
        self.assertTrue(itr2.cur > itr.cur)

    def test_timezone_winter_time(self):
        """Test Athens jumps backwards: 2013-10-27 04:00 -> 03:00 (UTC+3 -> UTC+2)."""
        tz = zoneinfo.ZoneInfo("Europe/Athens")

        expected_schedule = [
            "2013-10-27T02:30:00+03:00",
            "2013-10-27T03:00:00+03:00",
            "2013-10-27T03:30:00+03:00",
            "2013-10-27T03:00:00+02:00",
            "2013-10-27T03:30:00+02:00",
            "2013-10-27T04:00:00+02:00",
            "2013-10-27T04:30:00+02:00",
        ]

        start = datetime(2013, 10, 27, 2, 0, 0, tzinfo=tz)
        ct = croniter("*/30 * * * *", start)
        schedule = [ct.get_next(datetime).isoformat() for _ in range(7)]
        self.assertEqual(schedule, expected_schedule)

        start = datetime(2013, 10, 27, 5, 0, 0, tzinfo=tz)
        ct = croniter("*/30 * * * *", start)
        schedule = [ct.get_prev(datetime).isoformat() for _ in range(7)]
        self.assertEqual(schedule, list(reversed(expected_schedule)))

    def test_timezone_winter_time_pytz(self):
        """Test Athens jumps backwards: 2013-10-27 04:00 -> 03:00 (UTC+3 -> UTC+2)."""
        tz = pytz.timezone("Europe/Athens")

        expected_schedule = [
            "2013-10-27T02:30:00+03:00",
            "2013-10-27T03:00:00+03:00",
            "2013-10-27T03:30:00+03:00",
            "2013-10-27T03:00:00+02:00",
            "2013-10-27T03:30:00+02:00",
            "2013-10-27T04:00:00+02:00",
            "2013-10-27T04:30:00+02:00",
        ]

        start = datetime(2013, 10, 27, 2, 0, 0)
        ct = croniter("*/30 * * * *", tz.localize(start))
        schedule = [ct.get_next(datetime).isoformat() for _ in range(7)]
        self.assertEqual(schedule, expected_schedule)

        start = datetime(2013, 10, 27, 5, 0, 0)
        ct = croniter("*/30 * * * *", tz.localize(start))
        schedule = [ct.get_prev(datetime).isoformat() for _ in range(7)]
        self.assertEqual(schedule, list(reversed(expected_schedule)))

    def test_timezone_summer_time(self):
        """Test Athens jumps forward: 2013-03-31 03:00 -> 04:00 (UTC+2 -> UTC+3)."""
        tz = zoneinfo.ZoneInfo("Europe/Athens")

        expected_schedule = [
            "2013-03-31T01:30:00+02:00",
            "2013-03-31T02:00:00+02:00",
            "2013-03-31T02:30:00+02:00",
            "2013-03-31T04:00:00+03:00",
            "2013-03-31T04:30:00+03:00",
        ]

        start = datetime(2013, 3, 31, 1, 0, 0, tzinfo=tz)
        ct = croniter("*/30 * * * *", start)
        schedule = [ct.get_next(datetime).isoformat() for _ in range(5)]
        self.assertEqual(schedule, expected_schedule)

        start = datetime(2013, 3, 31, 5, 0, 0, tzinfo=tz)
        ct = croniter("*/30 * * * *", start)
        schedule = [ct.get_prev(datetime).isoformat() for _ in range(5)]
        self.assertEqual(schedule, list(reversed(expected_schedule)))

    def test_timezone_summer_time_pytz(self):
        """Test Athens jumps forward: 2013-03-31 03:00 -> 04:00 (UTC+2 -> UTC+3)."""
        tz = pytz.timezone("Europe/Athens")

        expected_schedule = [
            "2013-03-31T01:30:00+02:00",
            "2013-03-31T02:00:00+02:00",
            "2013-03-31T02:30:00+02:00",
            "2013-03-31T04:00:00+03:00",
            "2013-03-31T04:30:00+03:00",
        ]

        start = datetime(2013, 3, 31, 1, 0, 0)
        ct = croniter("*/30 * * * *", tz.localize(start))
        schedule = [ct.get_next(datetime).isoformat() for _ in range(5)]
        self.assertEqual(schedule, expected_schedule)

        start = datetime(2013, 3, 31, 5, 0, 0)
        ct = croniter("*/30 * * * *", tz.localize(start))
        schedule = [ct.get_prev(datetime).isoformat() for _ in range(5)]
        self.assertEqual(schedule, list(reversed(expected_schedule)))

    def test_std_dst(self):
        """
        DST tests

        This fixes https://github.com/taichino/croniter/issues/82

        """
        tz = zoneinfo.ZoneInfo("Europe/Warsaw")
        # -> 2017-03-26 01:59+1:00 -> 03:00+2:00
        local_date = datetime(2017, 3, 26, tzinfo=tz)
        val = croniter("0 0 * * *", local_date).get_next(datetime)
        self.assertEqual(val.isoformat(), "2017-03-27T00:00:00+02:00")
        #
        local_date = datetime(2017, 3, 26, 1, tzinfo=tz)
        cr = croniter("0 * * * *", local_date)
        val = cr.get_next(datetime)
        self.assertEqual(val.isoformat(), "2017-03-26T03:00:00+02:00")
        val = cr.get_current(datetime)
        self.assertEqual(val.isoformat(), "2017-03-26T03:00:00+02:00")

        # -> 2017-10-29 02:59+2:00 -> 02:00+1:00
        local_date = datetime(2017, 10, 29, tzinfo=tz)
        val = croniter("0 0 * * *", local_date).get_next(datetime)
        self.assertEqual(val.isoformat(), "2017-10-30T00:00:00+01:00")
        local_date = datetime(2017, 10, 29, 1, 59, tzinfo=tz)
        cr = croniter("0 * * * *", local_date)
        schedule = [cr.get_next(datetime).isoformat() for _ in range(4)]
        expected_schedule = [
            "2017-10-29T02:00:00+02:00",
            "2017-10-29T02:00:00+01:00",
            "2017-10-29T03:00:00+01:00",
            "2017-10-29T04:00:00+01:00",
        ]
        self.assertEqual(schedule, expected_schedule)

    def test_std_dst_pytz(self):
        """
        DST tests

        This fixes https://github.com/taichino/croniter/issues/82

        """
        tz = pytz.timezone("Europe/Warsaw")
        # -> 2017-03-26 01:59+1:00 -> 03:00+2:00
        local_date = tz.localize(datetime(2017, 3, 26))
        val = croniter("0 0 * * *", local_date).get_next(datetime)
        self.assertEqual(val.isoformat(), "2017-03-27T00:00:00+02:00")
        #
        local_date = tz.localize(datetime(2017, 3, 26, 1))
        cr = croniter("0 * * * *", local_date)
        val = cr.get_next(datetime)
        self.assertEqual(val.isoformat(), "2017-03-26T03:00:00+02:00")
        val = cr.get_current(datetime)
        self.assertEqual(val.isoformat(), "2017-03-26T03:00:00+02:00")

        # -> 2017-10-29 02:59+2:00 -> 02:00+1:00
        local_date = tz.localize(datetime(2017, 10, 29))
        val = croniter("0 0 * * *", local_date).get_next(datetime)
        self.assertEqual(val.isoformat(), "2017-10-30T00:00:00+01:00")
        local_date = tz.localize(datetime(2017, 10, 29, 1, 59))
        cr = croniter("0 * * * *", local_date)
        schedule = [cr.get_next(datetime).isoformat() for _ in range(4)]
        expected_schedule = [
            "2017-10-29T02:00:00+02:00",
            "2017-10-29T02:00:00+01:00",
            "2017-10-29T03:00:00+01:00",
            "2017-10-29T04:00:00+01:00",
        ]
        self.assertEqual(schedule, expected_schedule)

    def test_std_dst2(self):
        """
        DST tests

        This fixes https://github.com/taichino/croniter/issues/87

        SΓ£o Paulo, Brazil: 18/02/2018 00:00 -> 17/02/2018 23:00

        """
        tz = zoneinfo.ZoneInfo("America/Sao_Paulo")
        local_dates = [
            # 17-22: 00 -> 18-00:00
            (datetime(2018, 2, 17, 21, 0, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
            # 17-23: 00 -> 18-00:00
            (datetime(2018, 2, 17, 22, 0, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
            # 17-23: 00 -> 18-00:00
            (datetime(2018, 2, 17, 23, 0, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
            # 18-00: 00 -> 19-00:00
            (datetime(2018, 2, 18, 0, 0, 0, tzinfo=tz), "2018-02-19 00:00:00-03:00"),
            # 17-22: 00 -> 18-00:00
            (datetime(2018, 2, 17, 21, 5, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
            # 17-23: 00 -> 18-00:00
            (datetime(2018, 2, 17, 22, 5, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
            # 17-23: 00 -> 18-00:00
            (datetime(2018, 2, 17, 23, 5, 0, tzinfo=tz), "2018-02-18 00:00:00-03:00"),
            # 18-00: 00 -> 19-00:00
            (datetime(2018, 2, 18, 0, 5, 0, tzinfo=tz), "2018-02-19 00:00:00-03:00"),
        ]
        ret1 = [croniter("0 0 * * *", d[0]).get_next(datetime) for d in local_dates]
        sret1 = [str(d) for d in ret1]
        lret1 = [str(d[1]) for d in local_dates]
        self.assertEqual(sret1, lret1)

    def test_std_dst2_pytz(self):
        """
        DST tests

        This fixes https://github.com/taichino/croniter/issues/87

        SΓ£o Paulo, Brazil: 18/02/2018 00:00 -> 17/02/2018 23:00

        """
        tz = pytz.timezone("America/Sao_Paulo")
        local_dates = [
            # 17-22: 00 -> 18-00:00
            (tz.localize(datetime(2018, 2, 17, 21, 0, 0)), "2018-02-18 00:00:00-03:00"),
            # 17-23: 00 -> 18-00:00
            (tz.localize(datetime(2018, 2, 17, 22, 0, 0)), "2018-02-18 00:00:00-03:00"),
            # 17-23: 00 -> 18-00:00
            (tz.localize(datetime(2018, 2, 17, 23, 0, 0)), "2018-02-18 00:00:00-03:00"),
            # 18-00: 00 -> 19-00:00
            (tz.localize(datetime(2018, 2, 18, 0, 0, 0)), "2018-02-19 00:00:00-03:00"),
            # 17-22: 00 -> 18-00:00
            (tz.localize(datetime(2018, 2, 17, 21, 5, 0)), "2018-02-18 00:00:00-03:00"),
            # 17-23: 00 -> 18-00:00
            (tz.localize(datetime(2018, 2, 17, 22, 5, 0)), "2018-02-18 00:00:00-03:00"),
            # 17-23: 00 -> 18-00:00
            (tz.localize(datetime(2018, 2, 17, 23, 5, 0)), "2018-02-18 00:00:00-03:00"),
            # 18-00: 00 -> 19-00:00
            (tz.localize(datetime(2018, 2, 18, 0, 5, 0)), "2018-02-19 00:00:00-03:00"),
        ]
        ret1 = [croniter("0 0 * * *", d[0]).get_next(datetime) for d in local_dates]
        sret1 = [str(d) for d in ret1]
        lret1 = [str(d[1]) for d in local_dates]
        self.assertEqual(sret1, lret1)

    def test_std_dst3(self):
        """
        DST tests

        This fixes https://github.com/taichino/croniter/issues/90

        Adelaide, Australia: 15/04/2020 00:00 -> 15/03/2020

        """
        tz = zoneinfo.ZoneInfo("Australia/Adelaide")

        schedule = croniter("0 0 24 * *", datetime(2020, 4, 15, tzinfo=tz))
        val1 = schedule.get_prev(datetime)
        self.assertEqual(val1.isoformat(), "2020-03-24T00:00:00+10:30")

        val2 = schedule.get_next(datetime)
        self.assertEqual(val2.isoformat(), "2020-04-24T00:00:00+09:30")

    def test_std_dst3_pytz(self):
        """
        DST tests

        This fixes https://github.com/taichino/croniter/issues/90

        Adelaide, Australia: 15/04/2020 00:00 -> 15/03/2020

        """
        tz = pytz.timezone("Australia/Adelaide")

        schedule = croniter("0 0 24 * *", tz.localize(datetime(2020, 4, 15)))
        val1 = schedule.get_prev(datetime)
        self.assertEqual(val1.isoformat(), "2020-03-24T00:00:00+10:30")

        val2 = schedule.get_next(datetime)
        self.assertEqual(val2.isoformat(), "2020-04-24T00:00:00+09:30")

    def test_dst_daily(self) -> None:
        """
        DST test for daily schedule

        London jumps forward: 2025-03-30 01:00 -> 02:00 (UTC+0 -> UTC+1).
        """
        london = dateutil.tz.gettz("Europe/London")
        start = datetime(2025, 3, 30, tzinfo=london)
        ct = croniter("7 0 * * *", start)
        schedule = [ct.get_next(datetime).isoformat() for _ in range(3)]
        expected_schedule = [
            "2025-03-30T00:07:00+00:00",
            "2025-03-31T00:07:00+01:00",
            "2025-04-01T00:07:00+01:00",
        ]
        self.assertEqual(schedule, expected_schedule)

    def test_dst_hourly(self) -> None:
        """
        DST test for hourly schedule

        This fixes https://github.com/pallets-eco/croniter/issues/149

        London jumps forward: 2025-03-30 01:00 -> 02:00 (UTC+0 -> UTC+1).
        """
        london = dateutil.tz.gettz("Europe/London")
        start = datetime(2025, 3, 30, tzinfo=london)
        ct = croniter("7 * * * *", start)
        schedule = [ct.get_next(datetime).isoformat() for _ in range(3)]
        expected_schedule = [
            "2025-03-30T00:07:00+00:00",
            "2025-03-30T02:07:00+01:00",
            "2025-03-30T03:07:00+01:00",
        ]
        self.assertEqual(schedule, expected_schedule)

    def test_error_alpha_cron(self):
        self.assertRaises(CroniterNotAlphaError, croniter.expand, "* * * janu-jun *")

    def test_error_bad_cron(self):
        self.assertRaises(CroniterBadCronError, croniter.expand, "* * * *")
        self.assertRaises(
            CroniterBadCronError, croniter.expand, ("* " * (max(VALID_LEN_EXPRESSION) + 1)).strip()
        )

    def test_is_valid(self):
        self.assertTrue(croniter.is_valid("0 * * * *"))
        self.assertFalse(croniter.is_valid("0 * *"))
        self.assertFalse(croniter.is_valid("* * * janu-jun *"))
        self.assertTrue(croniter.is_valid("H 0 * * *", hash_id="abc"))

    def test_exactly_the_same_minute(self):
        base = datetime(2018, 3, 5, 12, 30, 50)
        itr = croniter("30 7,12,17 * * *", base)
        n1 = itr.get_prev(datetime)
        self.assertEqual(12, n1.hour)

        n2 = itr.get_prev(datetime)
        self.assertEqual(7, n2.hour)

        n3 = itr.get_next(datetime)
        self.assertEqual(12, n3.hour)

    def test_next_when_now_satisfies_cron(self):
        ts_a = datetime(2018, 5, 21, 0, 3, 0)
        ts_b = datetime(2018, 5, 21, 0, 4, 20)
        test_cron = "4 * * * *"

        next_a = croniter(test_cron, start_time=ts_a).get_next()
        next_b = croniter(test_cron, start_time=ts_b).get_next()

        self.assertTrue(next_b > next_a)

    def test_milliseconds(self):
        """
        https://github.com/taichino/croniter/issues/107
        """

        _croniter = partial(croniter, "0 10 * * *", ret_type=datetime)

        dt = datetime(2018, 1, 2, 10, 0, 0, 500)
        self.assertEqual(_croniter(start_time=dt).get_prev(), datetime(2018, 1, 2, 10, 0))
        self.assertEqual(_croniter(start_time=dt).get_next(), datetime(2018, 1, 3, 10, 0))

        dt = datetime(2018, 1, 2, 10, 0, 1, 0)
        self.assertEqual(_croniter(start_time=dt).get_prev(), datetime(2018, 1, 2, 10, 0))
        self.assertEqual(_croniter(start_time=dt).get_next(), datetime(2018, 1, 3, 10, 0))

        dt = datetime(2018, 1, 2, 9, 59, 59, 999999)
        self.assertEqual(_croniter(start_time=dt).get_prev(), datetime(2018, 1, 1, 10, 0))
        self.assertEqual(_croniter(start_time=dt).get_next(), datetime(2018, 1, 2, 10, 0))

    def test_invalid_zerorepeat(self):
        self.assertFalse(croniter.is_valid("*/0 * * * *"))

    def test_weekday_range(self):
        ret = []
        # jan 14 is monday
        dt = datetime(2019, 1, 14, 0, 0, 0, 0)
        for i in range(10):
            c = croniter("0 0 * * 2-4 *", start_time=dt)
            dt = datetime.fromtimestamp(c.get_next(), dateutil.tz.tzutc()).replace(tzinfo=None)
            ret.append(dt)
            dt += timedelta(days=1)
        sret = [str(r) for r in ret]
        self.assertEqual(
            sret,
            [
                "2019-01-15 00:00:00",
                "2019-01-16 00:00:01",
                "2019-01-17 00:00:02",
                "2019-01-22 00:00:00",
                "2019-01-23 00:00:01",
                "2019-01-24 00:00:02",
                "2019-01-29 00:00:00",
                "2019-01-30 00:00:01",
                "2019-01-31 00:00:02",
                "2019-02-05 00:00:00",
            ],
        )
        ret = []
        dt = datetime(2019, 1, 14, 0, 0, 0, 0)
        for i in range(10):
            c = croniter("0 0 * * 0-6 *", start_time=dt)
            dt = datetime.fromtimestamp(c.get_next(), dateutil.tz.tzutc()).replace(tzinfo=None)
            ret.append(dt)
            dt += timedelta(days=1)
        sret = [str(r) for r in ret]
        self.assertEqual(
            sret,
            [
                "2019-01-14 00:00:01",
                "2019-01-15 00:00:02",
                "2019-01-16 00:00:03",
                "2019-01-17 00:00:04",
                "2019-01-18 00:00:05",
                "2019-01-19 00:00:06",
                "2019-01-20 00:00:07",
                "2019-01-21 00:00:08",
                "2019-01-22 00:00:09",
                "2019-01-23 00:00:10",
            ],
        )

    def test_issue_monsun_117(self):
        ret = []
        dt = datetime(2019, 1, 14, 0, 0, 0, 0)
        for i in range(12):
            # c = croniter("0 0 * * Mon-Sun *", start_time=dt)
            c = croniter("0 0 * * Wed-Sun *", start_time=dt)
            dt = datetime.fromtimestamp(c.get_next(), tz=dateutil.tz.tzutc()).replace(tzinfo=None)
            ret.append(dt)
            dt += timedelta(days=1)
        sret = [str(r) for r in ret]
        self.assertEqual(
            sret,
            [
                "2019-01-16 00:00:00",
                "2019-01-17 00:00:01",
                "2019-01-18 00:00:02",
                "2019-01-19 00:00:03",
                "2019-01-20 00:00:04",
                "2019-01-23 00:00:00",
                "2019-01-24 00:00:01",
                "2019-01-25 00:00:02",
                "2019-01-26 00:00:03",
                "2019-01-27 00:00:04",
                "2019-01-30 00:00:00",
                "2019-01-31 00:00:01",
            ],
        )

    def test_mixdow(self):
        base = datetime(2018, 10, 1, 0, 0)
        itr = croniter("1 1 7,14,21,L * *", base)
        self.assertTrue(isinstance(itr.get_next(), float))

    def test_match(self):
        self.assertTrue(croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 0, 0, 0)))
        self.assertFalse(croniter.match("0 0 * * *", datetime(2019, 1, 14, 0, 1, 0, 0)))
        self.assertTrue(croniter.match("0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 1, 0)))
        self.assertFalse(croniter.match("0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 2, 0)))
        self.assertTrue(croniter.match("31 * * * *", datetime(2019, 1, 14, 1, 31, 0, 0)))
        self.assertTrue(
            croniter.match("0 0 10 * wed", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=True)
        )
        self.assertTrue(
            croniter.match("0 0 10 * fri", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=True)
        )
        self.assertTrue(
            croniter.match("0 0 10 * fri", datetime(2020, 6, 12, 0, 0, 0, 0), day_or=True)
        )
        self.assertTrue(
            croniter.match("0 0 10 * wed", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=False)
        )
        self.assertFalse(
            croniter.match("0 0 10 * fri", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=False)
        )
        self.assertFalse(
            croniter.match("0 0 10 * fri", datetime(2020, 6, 12, 0, 0, 0, 0), day_or=False)
        )

    def test_match_handle_bad_cron(self):
        # some cron expression can"t get prev value and should not raise exception
        self.assertFalse(croniter.match("0 0 31 1 1#1", datetime(2020, 1, 31), day_or=False))
        self.assertFalse(croniter.match("0 0 31 1 * 0 2024/2", datetime(2020, 1, 31)))

    def test_match_range(self):
        self.assertTrue(
            croniter.match_range(
                "0 0 * * *", datetime(2019, 1, 13, 0, 59, 0, 0), datetime(2019, 1, 14, 0, 1, 0, 0)
            )
        )
        self.assertFalse(
            croniter.match_range(
                "0 0 * * *", datetime(2019, 1, 13, 0, 1, 0, 0), datetime(2019, 1, 13, 0, 59, 0, 0)
            )
        )
        self.assertTrue(
            croniter.match_range(
                "0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 0, 0), datetime(2023, 5, 25, 0, 0, 2, 0)
            )
        )
        self.assertFalse(
            croniter.match_range(
                "0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 2, 0), datetime(2023, 5, 25, 0, 0, 4, 0)
            )
        )
        self.assertTrue(
            croniter.match_range(
                "0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 1, 0), datetime(2023, 5, 25, 0, 0, 4, 0)
            )
        )
        self.assertTrue(
            croniter.match_range(
                "31 * * * *",
                datetime(2019, 1, 14, 1, 30, 0, 0),
                datetime(2019, 1, 14, 1, 31, 0, 0),
            )
        )
        self.assertTrue(
            croniter.match_range(
                "0 0 10 * wed",
                datetime(2020, 6, 9, 0, 0, 0, 0),
                datetime(2020, 6, 11, 0, 0, 0, 0),
                day_or=True,
            )
        )
        self.assertTrue(
            croniter.match_range(
                "0 0 10 * fri",
                datetime(2020, 6, 10, 0, 0, 0, 0),
                datetime(2020, 6, 11, 0, 0, 0, 0),
                day_or=True,
            )
        )
        self.assertTrue(
            croniter.match_range(
                "0 0 10 * fri",
                datetime(2020, 6, 11, 0, 0, 0, 0),
                datetime(2020, 6, 12, 0, 0, 0, 0),
                day_or=True,
            )
        )
        self.assertTrue(
            croniter.match_range(
                "0 0 10 * wed",
                datetime(2020, 6, 9, 0, 0, 0, 0),
                datetime(2020, 6, 12, 0, 0, 0, 0),
                day_or=False,
            )
        )
        self.assertFalse(
            croniter.match_range(
                "0 0 10 * fri",
                datetime(2020, 6, 8, 0, 0, 0, 0),
                datetime(2020, 6, 9, 0, 0, 0, 0),
                day_or=False,
            )
        )
        self.assertFalse(
            croniter.match_range(
                "0 0 10 * fri",
                datetime(2020, 6, 7, 0, 0, 0, 0),
                datetime(2020, 6, 11, 0, 0, 0, 0),
                day_or=False,
            )
        )
        self.assertFalse(
            croniter.match_range(
                "2 4 1 * wed",
                datetime(2019, 1, 1, 3, 2, 0, 0),
                datetime(2019, 1, 1, 5, 2, 0, 0),
                day_or=False,
            )
        )

    def test_dst_issue90_st31ny(self):
        """Test DST gap with cron job every day at 02:01.

        Paris jumps forward: 2020-03-29 02:00 -> 03:00 (UTC+1 -> UTC+2).
        So 2020-03-29 02:01 does not exist in local time.

        This fixes https://github.com/taichino/croniter/issues/90#issuecomment-605615205
        """
        expected_schedule = [
            "2020-03-28T02:01:00+01:00",  # only checked for get_prev
            "2020-03-29T03:00:00+02:00",
            "2020-03-30T02:01:00+02:00",
            "2020-03-31T02:01:00+02:00",  # only checked for get_next
        ]

        tz = zoneinfo.ZoneInfo("Europe/Paris")
        now = datetime(2020, 3, 29, 1, 59, 55, tzinfo=tz)
        it = croniter("1 2 * * *", now)
        schedule = [it.get_next(datetime).isoformat() for _ in range(3)]
        self.assertEqual(schedule, expected_schedule[1:])

        schedule = [it.get_prev(datetime).isoformat() for _ in range(3)]
        self.assertEqual(schedule, list(reversed(expected_schedule[:-1])))

    def test_dst_issue90_st31ny_pytz(self):
        """Test DST gap with cron job every day at 02:01.

        Paris jumps forward: 2020-03-29 02:00 -> 03:00 (UTC+1 -> UTC+2).
        So 2020-03-29 02:01 does not exist in local time.

        This fixes https://github.com/taichino/croniter/issues/90#issuecomment-605615205
        """
        expected_schedule = [
            "2020-03-28T02:01:00+01:00",  # only checked for get_prev
            "2020-03-29T03:00:00+02:00",
            "2020-03-30T02:01:00+02:00",
            "2020-03-31T02:01:00+02:00",  # only checked for get_next
        ]

        tz = pytz.timezone("Europe/Paris")
        now = tz.localize(datetime(2020, 3, 29, 1, 59, 55))
        it = croniter("1 2 * * *", now)
        schedule = [it.get_next(datetime).isoformat() for _ in range(3)]
        self.assertEqual(schedule, expected_schedule[1:])

        schedule = [it.get_prev(datetime).isoformat() for _ in range(3)]
        self.assertEqual(schedule, list(reversed(expected_schedule[:-1])))

    def test_dst_iter(self):
        """Test Hebron jumps one hour forward on 2022-03-27 00:00 (UTC+2 -> UTC+3)."""
        tz = zoneinfo.ZoneInfo("Asia/Hebron")
        now = datetime(2022, 3, 25, 0, 0, 0, tzinfo=tz)
        it = croniter("0 0 * * *", now)
        ret = [
            it.get_next(datetime).isoformat(),
            it.get_next(datetime).isoformat(),
            it.get_next(datetime).isoformat(),
        ]
        self.assertEqual(
            ret,
            [
                "2022-03-26T00:00:00+02:00",
                "2022-03-27T01:00:00+03:00",
                "2022-03-28T00:00:00+03:00",
            ],
        )

    def test_dst_iter_pytz(self):
        """Test Hebron jumps one hour forward on 2022-03-27 00:00 (UTC+2 -> UTC+3)."""
        tz = pytz.timezone("Asia/Hebron")
        now = tz.localize(datetime(2022, 3, 25, 0, 0, 0))
        it = croniter("0 0 * * *", now)
        ret = [
            it.get_next(datetime).isoformat(),
            it.get_next(datetime).isoformat(),
            it.get_next(datetime).isoformat(),
        ]
        self.assertEqual(
            ret,
            [
                "2022-03-26T00:00:00+02:00",
                "2022-03-27T01:00:00+03:00",
                "2022-03-28T00:00:00+03:00",
            ],
        )

    def test_nth_wday_simple(self):
        def f(y, m, w):
            return croniter._get_nth_weekday_of_month(y, m, w)

        sun, mon, tue, wed, thu, fri, sat = range(7)

        self.assertEqual(f(2000, 1, mon), (3, 10, 17, 24, 31))
        self.assertEqual(f(2000, 2, tue), (1, 8, 15, 22, 29))  # Leap year
        self.assertEqual(f(2000, 3, wed), (1, 8, 15, 22, 29))
        self.assertEqual(f(2000, 4, thu), (6, 13, 20, 27))
        self.assertEqual(f(2000, 2, fri), (4, 11, 18, 25))
        self.assertEqual(f(2000, 2, sat), (5, 12, 19, 26))

    def test_nth_as_last_wday_simple(self):
        def f(y, m, w):
            return croniter._get_nth_weekday_of_month(y, m, w)[-1]

        sun, mon, tue, wed, thu, fri, sat = range(7)
        self.assertEqual(f(2000, 2, tue), 29)
        self.assertEqual(f(2000, 2, sun), 27)
        self.assertEqual(f(2000, 2, mon), 28)
        self.assertEqual(f(2000, 2, wed), 23)
        self.assertEqual(f(2000, 2, thu), 24)
        self.assertEqual(f(2000, 2, fri), 25)
        self.assertEqual(f(2000, 2, sat), 26)

    def test_wdom_core_leap_year(self):
        def f(y, m, w):
            return croniter._get_nth_weekday_of_month(y, m, w)[-1]

        sun, mon, tue, wed, thu, fri, sat = range(7)
        self.assertEqual(f(2000, 2, tue), 29)
        self.assertEqual(f(2000, 2, sun), 27)
        self.assertEqual(f(2000, 2, mon), 28)
        self.assertEqual(f(2000, 2, wed), 23)
        self.assertEqual(f(2000, 2, thu), 24)
        self.assertEqual(f(2000, 2, fri), 25)
        self.assertEqual(f(2000, 2, sat), 26)

    def test_lwom_friday(self):
        it = croniter("0 0 * * L5", datetime(1987, 1, 15), ret_type=datetime)
        items = [next(it) for i in range(12)]
        self.assertListEqual(
            items,
            [
                datetime(1987, 1, 30),
                datetime(1987, 2, 27),
                datetime(1987, 3, 27),
                datetime(1987, 4, 24),
                datetime(1987, 5, 29),
                datetime(1987, 6, 26),
                datetime(1987, 7, 31),
                datetime(1987, 8, 28),
                datetime(1987, 9, 25),
                datetime(1987, 10, 30),
                datetime(1987, 11, 27),
                datetime(1987, 12, 25),
            ],
        )

    def test_lwom_friday_2hours(self):
        # This works with +/- "days=1' in proc_day_of_week_last() and I don't know WHY?!?
        it = croniter("0 1,5 * * L5", datetime(1987, 1, 15), ret_type=datetime)
        items = [next(it) for i in range(12)]
        self.assertListEqual(
            items,
            [
                datetime(1987, 1, 30, 1),
                datetime(1987, 1, 30, 5),
                datetime(1987, 2, 27, 1),
                datetime(1987, 2, 27, 5),
                datetime(1987, 3, 27, 1),
                datetime(1987, 3, 27, 5),
                datetime(1987, 4, 24, 1),
                datetime(1987, 4, 24, 5),
                datetime(1987, 5, 29, 1),
                datetime(1987, 5, 29, 5),
                datetime(1987, 6, 26, 1),
                datetime(1987, 6, 26, 5),
            ],
        )

    def test_lwom_friday_2xh_2xm(self):
        it = croniter("0,30 1,5 * * L5", datetime(1987, 1, 15), ret_type=datetime)
        items = [next(it) for i in range(12)]
        self.assertListEqual(
            items,
            [
                datetime(1987, 1, 30, 1, 0),
                datetime(1987, 1, 30, 1, 30),
                datetime(1987, 1, 30, 5, 0),
                datetime(1987, 1, 30, 5, 30),
                datetime(1987, 2, 27, 1, 0),
                datetime(1987, 2, 27, 1, 30),
                datetime(1987, 2, 27, 5, 0),
                datetime(1987, 2, 27, 5, 30),
                datetime(1987, 3, 27, 1, 0),
                datetime(1987, 3, 27, 1, 30),
                datetime(1987, 3, 27, 5, 0),
                datetime(1987, 3, 27, 5, 30),
            ],
        )

    def test_lwom_saturday_rev(self):
        it = croniter("0 0 * * L6", datetime(2017, 12, 31), ret_type=datetime, is_prev=True)
        items = [next(it) for i in range(12)]
        self.assertListEqual(
            items,
            [
                datetime(2017, 12, 30),
                datetime(2017, 11, 25),
                datetime(2017, 10, 28),
                datetime(2017, 9, 30),
                datetime(2017, 8, 26),
                datetime(2017, 7, 29),
                datetime(2017, 6, 24),
                datetime(2017, 5, 27),
                datetime(2017, 4, 29),
                datetime(2017, 3, 25),
                datetime(2017, 2, 25),
                datetime(2017, 1, 28),
            ],
        )

    def test_lwom_tue_thu(self):
        it = croniter("0 0 * * L2,L4", datetime(2016, 6, 1), ret_type=datetime)
        items = [next(it) for i in range(10)]
        self.assertListEqual(
            items,
            [
                datetime(2016, 6, 28),
                datetime(2016, 6, 30),
                datetime(2016, 7, 26),
                datetime(2016, 7, 28),
                datetime(2016, 8, 25),  # last tuesday comes before the last thursday
                datetime(2016, 8, 30),
                datetime(2016, 9, 27),
                datetime(2016, 9, 29),
                datetime(2016, 10, 25),
                datetime(2016, 10, 27),
            ],
        )

    def test_hash_mixup_all_fri_3rd_sat(self):
        # It appears that it's not possible to MIX a literal dow with a `dow#n` format
        cron_a = "0 0 * * 6#3"
        cron_b = "0 0 * * 5"
        cron_c = "0 0 * * 5,6#3"
        start = datetime(2021, 3, 1)
        expect_a = [datetime(2021, 3, 20)]
        expect_b = [
            datetime(2021, 3, 5),
            datetime(2021, 3, 12),
            datetime(2021, 3, 19),
            datetime(2021, 3, 26),
        ]
        expect_c = sorted(set(expect_a) & set(expect_b))

        def getn(expr, n):
            it = croniter(expr, start, ret_type=datetime)
            return [next(it) for i in range(n)]

        self.assertListEqual(getn(cron_a, 1), expect_a)
        self.assertListEqual(getn(cron_b, 4), expect_b)
        with self.assertRaises(CroniterUnsupportedSyntaxError):
            self.assertListEqual(getn(cron_c, 5), expect_c)

    def test_lwom_mixup_all_fri_last_sat(self):
        # Based on the failure of test_hash_mixup_all_fri_3rd_sat, we should expect
        # this to fail too as this implementation simply extends nth_weekday_of_month
        cron_a = "0 0 * * L6"
        cron_b = "0 0 * * 5"
        cron_c = "0 0 * * 5,L6"
        start = datetime(2021, 3, 1)
        expect_a = [datetime(2021, 3, 27)]
        expect_b = [
            datetime(2021, 3, 5),
            datetime(2021, 3, 12),
            datetime(2021, 3, 19),
            datetime(2021, 3, 26),
        ]
        expect_c = sorted(set(expect_a) | set(expect_b))

        def getn(expr, n):
            it = croniter(expr, start, ret_type=datetime)
            return [next(it) for i in range(n)]

        self.assertListEqual(getn(cron_a, 1), expect_a)
        self.assertListEqual(getn(cron_b, 4), expect_b)
        with self.assertRaises(CroniterUnsupportedSyntaxError):
            self.assertListEqual(getn(cron_c, 5), expect_c)

    def test_lwom_mixup_firstlast_sat(self):
        # First saturday, last saturday
        start = datetime(2021, 3, 1)
        cron_a = "0 0 * * 6#1"
        cron_b = "0 0 * * L6"
        cron_c = "0 0 * * L6,6#1"
        expect_a = [datetime(2021, 3, 6), datetime(2021, 4, 3), datetime(2021, 5, 1)]
        expect_b = [datetime(2021, 3, 27), datetime(2021, 4, 24), datetime(2021, 5, 29)]
        expect_c = sorted(expect_a + expect_b)

        def getn(expr, n):
            it = croniter(expr, start, ret_type=datetime)
            return [next(it) for i in range(n)]

        self.assertListEqual(getn(cron_a, 3), expect_a)
        self.assertListEqual(getn(cron_b, 3), expect_b)
        self.assertListEqual(getn(cron_c, 6), expect_c)

    def test_lwom_mixup_4th_and_last(self):
        # 4th and last monday
        start = datetime(2021, 11, 1)
        cron_a = "0 0 * * 1#4"
        cron_b = "0 0 * * L1"
        cron_c = "0 0 * * 1#4,L1"
        expect_a = [datetime(2021, 11, 22), datetime(2021, 12, 27), datetime(2022, 1, 24)]
        expect_b = [datetime(2021, 11, 29), datetime(2021, 12, 27), datetime(2022, 1, 31)]
        expect_c = sorted(set(expect_a) | set(expect_b))

        def getn(expr, n):
            it = croniter(expr, start, ret_type=datetime)
            return [next(it) for i in range(n)]

        self.assertListEqual(getn(cron_a, 3), expect_a)
        self.assertListEqual(getn(cron_b, 3), expect_b)
        self.assertListEqual(getn(cron_c, 5), expect_c)

    def test_configure_second_location(self):
        base = datetime(2010, 8, 25, 0)
        itr = croniter("59 58 1 * * *", base, second_at_beginning=True)
        n = itr.get_next(datetime)
        self.assertEqual(n.year, base.year)
        self.assertEqual(n.month, base.month)
        self.assertEqual(n.day, base.day)
        self.assertEqual(n.hour, 1)
        self.assertEqual(n.minute, 58)
        self.assertEqual(n.second, 59)

    def test_nth_out_of_range(self):
        with self.assertRaises(CroniterBadCronError):
            croniter("0 0 * * 1#7")
        with self.assertRaises(CroniterBadCronError):
            croniter("0 0 * * 1#0")

    def test_last_out_of_range(self):
        with self.assertRaises(CroniterBadCronError):
            croniter("0 0 * * L-1")
        with self.assertRaises(CroniterBadCronError):
            croniter("0 0 * * L8")

    def test_question_mark(self):
        base = datetime(2010, 8, 25, 15, 56)
        itr = croniter("0 0 1 * ?", base)
        n = itr.get_next(datetime)
        self.assertEqual(n.year, base.year)
        self.assertEqual(n.month, 9)
        self.assertEqual(n.day, 1)
        self.assertEqual(n.hour, 0)
        self.assertEqual(n.minute, 0)

    def test_invalid_question_mark(self):
        self.assertRaises(CroniterBadCronError, croniter, "? * * * *")
        self.assertRaises(CroniterBadCronError, croniter, "* ? * * *")
        self.assertRaises(CroniterBadCronError, croniter, "* * ?,* * *")

    def test_year(self):
        itr1 = croniter("0 0 11 * * 0 2060", datetime(2050, 1, 1))
        n1 = itr1.get_next(datetime)
        self.assertEqual(n1.year, 2060)
        self.assertEqual(n1.month, 1)
        self.assertEqual(n1.day, 11)
        n2 = itr1.get_next(datetime)
        self.assertEqual(n2.year, 2060)
        self.assertEqual(n2.month, 2)
        self.assertEqual(n2.day, 11)

        itr2 = croniter("0 0 11 * * 0 2050-2060", datetime(2055, 1, 30))
        n3 = itr2.get_next(datetime)
        self.assertEqual(n3.year, 2055)
        self.assertEqual(n3.month, 2)
        self.assertEqual(n3.day, 11)

        itr3 = croniter("0 0 29 2 * 0 2025,2021-2023,2028", datetime(2020, 1, 1))
        n4 = itr3.get_next(datetime)
        self.assertEqual(n4.year, 2028)
        self.assertEqual(n4.month, 2)
        self.assertEqual(n4.day, 29)

        itr4 = croniter("0 0 29 2 * 0 2025,*", datetime(2020, 1, 1))
        n5 = itr4.get_next(datetime)
        self.assertEqual(n5.year, 2020)
        self.assertEqual(n5.month, 2)
        self.assertEqual(n5.day, 29)

        itr5 = croniter("0 0 29 2 * 0 2022/3", datetime(2020, 1, 1))
        n6 = itr5.get_next(datetime)
        self.assertEqual(n6.year, 2028)
        self.assertEqual(n6.month, 2)
        self.assertEqual(n6.day, 29)

        itr6 = croniter("0 0 29 2 * 0 2023-2035/3", datetime(2020, 1, 1))
        n7 = itr6.get_next(datetime)
        self.assertEqual(n7.year, 2032)
        self.assertEqual(n7.month, 2)
        self.assertEqual(n7.day, 29)

    def test_year_with_other_field(self):
        itr1 = croniter("0 0 31 11-12 * 0 2023", datetime(2000, 1, 30))
        n1 = itr1.get_next(datetime)
        self.assertEqual(n1.year, 2023)
        self.assertEqual(n1.month, 12)
        self.assertEqual(n1.day, 31)

        itr2 = croniter("0 0 31 1-2 * 0 2023-2025", datetime(2024, 12, 30))
        n2 = itr2.get_next(datetime)
        self.assertEqual(n2.year, 2025)
        self.assertEqual(n2.month, 1)
        self.assertEqual(n2.day, 31)

        itr3 = croniter("0 0 1 1 1 0 2020-2030", datetime(2000, 1, 1), day_or=False)
        n3 = itr3.get_next(datetime)
        self.assertEqual(n3.year, 2024)
        self.assertEqual(n3.month, 1)
        self.assertEqual(n3.day, 1)

    def test_year_get_prev(self):
        itr1 = croniter("0 0 11 * * 0 2000", datetime(2010, 1, 1))
        p1 = itr1.get_prev(datetime)
        self.assertEqual(p1.year, 2000)
        self.assertEqual(p1.month, 12)
        self.assertEqual(p1.day, 11)

        itr2 = croniter("0 0 11 * * 0 2000", datetime(2010, 1, 1))
        p2 = itr2.get_prev(datetime)
        self.assertEqual(p2.year, 2000)
        self.assertEqual(p2.month, 12)
        self.assertEqual(p2.day, 11)

        itr2 = croniter("0 0 29 2 * 0 2010-2030", datetime(2020, 1, 1))
        p2 = itr2.get_prev(datetime)
        self.assertEqual(p2.year, 2016)
        self.assertEqual(p2.month, 2)
        self.assertEqual(p2.day, 29)

    def test_year_match(self):
        self.assertTrue(croniter.match("* * * * * * 2024", datetime(2024, 1, 1)))
        self.assertTrue(
            croniter.match(
                "59 58 23 31 12 * 2024",
                datetime(2024, 12, 31, 23, 58, 59),
                second_at_beginning=True,
            )
        )
        self.assertFalse(croniter.match("* * * * * * 2024-2026", datetime(2027, 1, 1)))
        self.assertFalse(croniter.match("* * * * * * 2024/2", datetime(2025, 1, 1)))

    def test_year_bad_date_error(self):
        with self.assertRaises(CroniterBadDateError):
            itr = croniter("* * * * * * 2020", datetime(2030, 1, 1))
            itr.get_next()
        with self.assertRaises(CroniterBadDateError):
            itr = croniter("* * * * * * 2020", datetime(2000, 1, 1))
            itr.get_prev()
        with self.assertRaises(CroniterBadDateError):
            itr = croniter("* * 29 2 * * 2021-2023", datetime(2000, 1, 1))
            itr.get_next()

    def test_year_with_second_at_beginning(self):
        base = datetime(2050, 1, 1)
        itr = croniter("59 58 23 31 12 * 2070", base, second_at_beginning=True)
        n = itr.get_next(datetime)
        self.assertEqual(n.year, 2070)
        self.assertEqual(n.month, 12)
        self.assertEqual(n.day, 31)
        self.assertEqual(n.hour, 23)
        self.assertEqual(n.minute, 58)
        self.assertEqual(n.second, 59)

    def test_invalid_year(self):
        self.assertRaises(CroniterBadCronError, croniter, "0 0 1 * * 0 1000")
        self.assertRaises(CroniterBadCronError, croniter, "0 0 1 * * 0 99999")
        self.assertRaises(CroniterBadCronError, croniter, "0 0 1 * * 0 2070#3")

    def test_issue_47(self):
        base = datetime(2021, 3, 30, 4, 0)
        itr = croniter("0 6 30 3 *", base)
        prev1 = itr.get_prev(datetime)
        self.assertEqual(prev1.year, base.year - 1)
        self.assertEqual(prev1.month, 3)
        self.assertEqual(prev1.day, 30)
        self.assertEqual(prev1.hour, 6)
        self.assertEqual(prev1.minute, 0)

    maxDiff = None

    def test_issue_142_dow(self):
        ret = []
        for i in range(1, 31):
            ret.append(
                (i, croniter("35 * 1-l/8 * *", datetime(2020, 1, i), ret_type=datetime).get_next())
            )
            i += 1
        self.assertEqual(
            ret,
            [
                (1, datetime(2020, 1, 1, 0, 35)),
                (2, datetime(2020, 1, 9, 0, 35)),
                (3, datetime(2020, 1, 9, 0, 35)),
                (4, datetime(2020, 1, 9, 0, 35)),
                (5, datetime(2020, 1, 9, 0, 35)),
                (6, datetime(2020, 1, 9, 0, 35)),
                (7, datetime(2020, 1, 9, 0, 35)),
                (8, datetime(2020, 1, 9, 0, 35)),
                (9, datetime(2020, 1, 9, 0, 35)),
                (10, datetime(2020, 1, 17, 0, 35)),
                (11, datetime(2020, 1, 17, 0, 35)),
                (12, datetime(2020, 1, 17, 0, 35)),
                (13, datetime(2020, 1, 17, 0, 35)),
                (14, datetime(2020, 1, 17, 0, 35)),
                (15, datetime(2020, 1, 17, 0, 35)),
                (16, datetime(2020, 1, 17, 0, 35)),
                (17, datetime(2020, 1, 17, 0, 35)),
                (18, datetime(2020, 1, 25, 0, 35)),
                (19, datetime(2020, 1, 25, 0, 35)),
                (20, datetime(2020, 1, 25, 0, 35)),
                (21, datetime(2020, 1, 25, 0, 35)),
                (22, datetime(2020, 1, 25, 0, 35)),
                (23, datetime(2020, 1, 25, 0, 35)),
                (24, datetime(2020, 1, 25, 0, 35)),
                (25, datetime(2020, 1, 25, 0, 35)),
                (26, datetime(2020, 2, 1, 0, 35)),
                (27, datetime(2020, 2, 1, 0, 35)),
                (28, datetime(2020, 2, 1, 0, 35)),
                (29, datetime(2020, 2, 1, 0, 35)),
                (30, datetime(2020, 2, 1, 0, 35)),
            ],
        )

    def test_issue145_getnext(self):
        # Example of quarterly event cron schedule
        start = datetime(2020, 9, 24)
        cron = "0 13 8 1,4,7,10 wed"
        with self.assertRaises(CroniterBadDateError):
            it = croniter(cron, start, day_or=False, max_years_between_matches=1)
            it.get_next()
        # New functionality (0.3.35) allowing croniter to find spare matches of cron
        # patterns across multiple years
        it = croniter(cron, start, day_or=False, max_years_between_matches=5)
        self.assertEqual(it.get_next(datetime), datetime(2025, 1, 8, 13))

    def test_explicit_year_forward(self):
        start = datetime(2020, 9, 24)
        cron = "0 13 8 1,4,7,10 wed"

        # Expect exception because no explicit range was provided.  Therefore, the
        # caller should be made aware that an implicit limit was hit.
        ccron = croniter(cron, start, day_or=False)
        ccron._max_years_between_matches = 1
        iterable = ccron.all_next()
        with self.assertRaises(CroniterBadDateError):
            next(iterable)

        iterable = croniter(cron, start, day_or=False, max_years_between_matches=5).all_next(
            datetime
        )
        n = next(iterable)
        self.assertEqual(n, datetime(2025, 1, 8, 13))

        # If the explicitly given lookahead isn't enough to reach the next date, that's fine.
        # The caller specified the maximum gap, so no just stop iteration
        iterable = croniter(cron, start, day_or=False, max_years_between_matches=2).all_next(
            datetime
        )
        with self.assertRaises(StopIteration):
            next(iterable)

    def test_issue151(self):
        """."""
        self.assertTrue(croniter.match("* * * * *", datetime(2019, 1, 14, 11, 0, 59, 999999)))

    def test_overflow(self):
        """."""
        self.assertRaises(CroniterBadCronError, croniter, "0-10000000 * * * *", datetime.now())

    def test_issue156(self):
        """."""
        dt = croniter("* * * * *,0", datetime(2019, 1, 14, 11, 0, 59, 999999)).get_next()
        self.assertEqual(1547463660.0, dt)
        self.assertRaises(CroniterBadCronError, croniter, "* * * * *,b")
        dt = croniter("0 0 * * *,sat#3", datetime(2019, 1, 14, 11, 0, 59, 999999)).get_next()
        self.assertEqual(1547856000.0, dt)

    def test_confirm_sort(self):
        m, h, d, mon, dow, s = range(6)
        self.assertListEqual(croniter("0 8,22,10,23 1 1 0").expanded[h], [8, 10, 22, 23])
        self.assertListEqual(croniter("0 0 25-L 1 0").expanded[d], [25, 26, 27, 28, 29, 30, 31])
        self.assertListEqual(croniter("1 1 7,14,21,L * *").expanded[d], [7, 14, 21, "l"])
        self.assertListEqual(croniter("0 0 * * *,sat#3").expanded[dow], ["*", 6])

    def test_issue_k6(self):
        self.assertRaises(CroniterBadCronError, croniter, "0 0 0 0 0")
        self.assertRaises(CroniterBadCronError, croniter, "0 0 0 1 0")

    def test_issue_k11(self):
        now = datetime(2019, 1, 14, 11, 0, 59, tzinfo=zoneinfo.ZoneInfo("America/New_York"))
        nextnow = croniter("* * * * * ").next(datetime, start_time=now)
        nextnow2 = croniter("* * * * * ", now).next(datetime)
        for nt in nextnow, nextnow2:
            self.assertEqual(nt.tzinfo.key, "America/New_York")
            self.assertEqual(int(croniter._datetime_to_timestamp(nt)), 1547481660)

    def test_issue_k12(self):
        tz = zoneinfo.ZoneInfo("Europe/Athens")
        base = datetime(2010, 1, 23, 12, 18, tzinfo=tz)
        itr = croniter("* * * * *")
        itr.set_current(start_time=base)
        n1 = itr.get_next()  # 19

        self.assertEqual(n1, datetime_to_timestamp(base) + 60)

    def test_issue_k34(self):
        # invalid cron, but should throw appropriate exception
        self.assertRaises(CroniterBadCronError, croniter, "4 0 L/2 2 0")

    def test_issue_k33(self):
        y = 2018
        # At 11:30 PM, between day 1 and 7 of the month, Monday through Friday, only in January
        ret = []
        for i in range(10):
            cron = croniter("30 23 1-7 JAN MON-FRI#1", datetime(y + i, 1, 1), ret_type=datetime)
            for j in range(7):
                d = cron.get_next()
                if d.year == y + i:
                    ret.append(d)
        rets = [
            datetime(2018, 1, 1, 23, 30),
            datetime(2018, 1, 2, 23, 30),
            datetime(2018, 1, 3, 23, 30),
            datetime(2018, 1, 4, 23, 30),
            datetime(2018, 1, 5, 23, 30),
            datetime(2019, 1, 1, 23, 30),
            datetime(2019, 1, 2, 23, 30),
            datetime(2019, 1, 3, 23, 30),
            datetime(2019, 1, 4, 23, 30),
            datetime(2019, 1, 7, 23, 30),
            datetime(2020, 1, 1, 23, 30),
            datetime(2020, 1, 2, 23, 30),
            datetime(2020, 1, 3, 23, 30),
            datetime(2020, 1, 6, 23, 30),
            datetime(2020, 1, 7, 23, 30),
            datetime(2021, 1, 1, 23, 30),
            datetime(2021, 1, 4, 23, 30),
            datetime(2021, 1, 5, 23, 30),
            datetime(2021, 1, 6, 23, 30),
            datetime(2021, 1, 7, 23, 30),
            datetime(2022, 1, 3, 23, 30),
            datetime(2022, 1, 4, 23, 30),
            datetime(2022, 1, 5, 23, 30),
            datetime(2022, 1, 6, 23, 30),
            datetime(2022, 1, 7, 23, 30),
            datetime(2023, 1, 2, 23, 30),
            datetime(2023, 1, 3, 23, 30),
            datetime(2023, 1, 4, 23, 30),
            datetime(2023, 1, 5, 23, 30),
            datetime(2023, 1, 6, 23, 30),
            datetime(2024, 1, 1, 23, 30),
            datetime(2024, 1, 2, 23, 30),
            datetime(2024, 1, 3, 23, 30),
            datetime(2024, 1, 4, 23, 30),
            datetime(2024, 1, 5, 23, 30),
            datetime(2025, 1, 1, 23, 30),
            datetime(2025, 1, 2, 23, 30),
            datetime(2025, 1, 3, 23, 30),
            datetime(2025, 1, 6, 23, 30),
            datetime(2025, 1, 7, 23, 30),
            datetime(2026, 1, 1, 23, 30),
            datetime(2026, 1, 2, 23, 30),
            datetime(2026, 1, 5, 23, 30),
            datetime(2026, 1, 6, 23, 30),
            datetime(2026, 1, 7, 23, 30),
            datetime(2027, 1, 1, 23, 30),
            datetime(2027, 1, 4, 23, 30),
            datetime(2027, 1, 5, 23, 30),
            datetime(2027, 1, 6, 23, 30),
            datetime(2027, 1, 7, 23, 30),
        ]
        self.assertEqual(ret, rets)
        croniter.expand("30 6 1-7 MAY MON#1")

    def test_bug_62_leap(self):
        ret = croniter("15 22 29 2 *", datetime(2024, 2, 29)).get_prev(datetime)
        self.assertEqual(ret, datetime(2020, 2, 29, 22, 15))

    def test_expand_from_start_time_minute(self):
        seven_seconds_interval_pattern = "*/7 * * * *"
        ret1 = croniter(
            seven_seconds_interval_pattern,
            start_time=datetime(2024, 7, 11, 10, 11),
            expand_from_start_time=True,
        ).get_next(datetime)
        self.assertEqual(ret1, datetime(2024, 7, 11, 10, 18))

        ret2 = croniter(
            seven_seconds_interval_pattern,
            start_time=datetime(2024, 7, 11, 10, 12),
            expand_from_start_time=True,
        ).get_next(datetime)
        self.assertEqual(ret2, datetime(2024, 7, 11, 10, 19))

        ret3 = croniter(
            seven_seconds_interval_pattern,
            start_time=datetime(2024, 7, 11, 10, 11),
            expand_from_start_time=True,
        ).get_prev(datetime)
        self.assertEqual(ret3, datetime(2024, 7, 11, 10, 4))

        ret4 = croniter(
            seven_seconds_interval_pattern,
            start_time=datetime(2024, 7, 11, 10, 12),
            expand_from_start_time=True,
        ).get_prev(datetime)
        self.assertEqual(ret4, datetime(2024, 7, 11, 10, 5))

    def test_expand_from_start_time_hour(self):
        seven_hours_interval_pattern = "0 */7 * * *"
        ret1 = croniter(
            seven_hours_interval_pattern,
            start_time=datetime(2024, 7, 11, 15, 0),
            expand_from_start_time=True,
        ).get_next(datetime)
        self.assertEqual(ret1, datetime(2024, 7, 11, 22, 0))

        ret2 = croniter(
            seven_hours_interval_pattern,
            start_time=datetime(2024, 7, 11, 16, 0),
            expand_from_start_time=True,
        ).get_next(datetime)
        self.assertEqual(ret2, datetime(2024, 7, 11, 23, 0))

        ret3 = croniter(
            seven_hours_interval_pattern,
            start_time=datetime(2024, 7, 11, 15, 0),
            expand_from_start_time=True,
        ).get_prev(datetime)
        self.assertEqual(ret3, datetime(2024, 7, 11, 8, 0))

        ret4 = croniter(
            seven_hours_interval_pattern,
            start_time=datetime(2024, 7, 11, 16, 0),
            expand_from_start_time=True,
        ).get_prev(datetime)
        self.assertEqual(ret4, datetime(2024, 7, 11, 9, 0))

    def test_expand_from_start_time_date(self):
        five_days_interval_pattern = "0 0 */5 * *"
        ret1 = croniter(
            five_days_interval_pattern,
            start_time=datetime(2024, 7, 12),
            expand_from_start_time=True,
        ).get_next(datetime)
        self.assertEqual(ret1, datetime(2024, 7, 17))

        ret2 = croniter(
            five_days_interval_pattern,
            start_time=datetime(2024, 7, 13),
            expand_from_start_time=True,
        ).get_next(datetime)
        self.assertEqual(ret2, datetime(2024, 7, 18))

        ret3 = croniter(
            five_days_interval_pattern,
            start_time=datetime(2024, 7, 12),
            expand_from_start_time=True,
        ).get_prev(datetime)
        self.assertEqual(ret3, datetime(2024, 7, 7))

        ret4 = croniter(
            five_days_interval_pattern,
            start_time=datetime(2024, 7, 13),
            expand_from_start_time=True,
        ).get_prev(datetime)
        self.assertEqual(ret4, datetime(2024, 7, 8))

    def test_expand_from_start_time_month(self):
        three_monts_interval_pattern = "0 0 1 */3 *"
        ret1 = croniter(
            three_monts_interval_pattern,
            start_time=datetime(2024, 7, 1),
            expand_from_start_time=True,
        ).get_next(datetime)
        self.assertEqual(ret1, datetime(2024, 10, 1))

        ret2 = croniter(
            three_monts_interval_pattern,
            start_time=datetime(2024, 8, 1),
            expand_from_start_time=True,
        ).get_next(datetime)
        self.assertEqual(ret2, datetime(2024, 11, 1))

        ret3 = croniter(
            three_monts_interval_pattern,
            start_time=datetime(2024, 7, 1),
            expand_from_start_time=True,
        ).get_prev(datetime)
        self.assertEqual(ret3, datetime(2024, 4, 1))

        ret4 = croniter(
            three_monts_interval_pattern,
            start_time=datetime(2024, 8, 1),
            expand_from_start_time=True,
        ).get_prev(datetime)
        self.assertEqual(ret4, datetime(2024, 5, 1))

    def test_expand_from_start_time_day_of_week(self):
        three_monts_interval_pattern = "0 0 * * */2"
        ret1 = croniter(
            three_monts_interval_pattern,
            start_time=datetime(2024, 7, 10),
            expand_from_start_time=True,
        ).get_next(datetime)
        self.assertEqual(ret1, datetime(2024, 7, 12))

        ret2 = croniter(
            three_monts_interval_pattern,
            start_time=datetime(2024, 7, 11),
            expand_from_start_time=True,
        ).get_next(datetime)
        self.assertEqual(ret2, datetime(2024, 7, 13))

        ret3 = croniter(
            three_monts_interval_pattern,
            start_time=datetime(2024, 7, 10),
            expand_from_start_time=True,
        ).get_prev(datetime)
        self.assertEqual(ret3, datetime(2024, 7, 8))

        ret4 = croniter(
            three_monts_interval_pattern,
            start_time=datetime(2024, 7, 11),
            expand_from_start_time=True,
        ).get_prev(datetime)
        self.assertEqual(ret4, datetime(2024, 7, 9))

    def test_get_next_fails_with_expand_from_start_time_true(self):
        expanded_croniter = croniter("0 0 */5 * *", expand_from_start_time=True)
        self.assertRaises(
            ValueError, expanded_croniter.get_next, datetime, start_time=datetime(2024, 7, 12)
        )

    def test_get_next_update_current(self):
        cron = croniter("* * * * * *")

        cron.set_current(datetime(2024, 7, 12), force=True)
        retn = [(cron.get_next(datetime), cron.get_current(datetime)) for a in range(3)]
        self.assertEqual(
            retn,
            [
                (datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0, 1)),
                (datetime(2024, 7, 12, 0, 0, 2), datetime(2024, 7, 12, 0, 0, 2)),
                (datetime(2024, 7, 12, 0, 0, 3), datetime(2024, 7, 12, 0, 0, 3)),
            ],
        )

        retns = (
            cron.get_next(datetime, start_time=datetime(2024, 7, 12)),
            cron.get_current(datetime),
        )
        self.assertEqual(retn[0], retns)

        cron.set_current(datetime(2024, 7, 12), force=True)
        retp = [(cron.get_prev(datetime), cron.get_current(datetime)) for a in range(3)]
        self.assertEqual(
            retp,
            [
                (datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 11, 23, 59, 59)),
                (datetime(2024, 7, 11, 23, 59, 58), datetime(2024, 7, 11, 23, 59, 58)),
                (datetime(2024, 7, 11, 23, 59, 57), datetime(2024, 7, 11, 23, 59, 57)),
            ],
        )

        retps = (
            cron.get_prev(datetime, start_time=datetime(2024, 7, 12)),
            cron.get_current(datetime),
        )
        self.assertEqual(retp[0], retps)

        cron.set_current(datetime(2024, 7, 12), force=True)
        r = cron.all_next(datetime)
        retan = [(next(r), cron.get_current(datetime)) for a in range(3)]

        r = cron.all_next(datetime, start_time=datetime(2024, 7, 12))
        retans = [(next(r), cron.get_current(datetime)) for a in range(3)]

        cron.set_current(datetime(2024, 7, 12), force=True)
        r = cron.all_prev(datetime)
        retap = [(next(r), cron.get_current(datetime)) for a in range(3)]

        r = cron.all_prev(datetime, start_time=datetime(2024, 7, 12))
        retaps = [(next(r), cron.get_current(datetime)) for a in range(3)]

        self.assertEqual(retp, retap)
        self.assertEqual(retp, retaps)
        self.assertEqual(retn, retan)
        self.assertEqual(retn, retans)

        cron.set_current(datetime(2024, 7, 12), force=True)
        uretn = [
            (cron.get_next(datetime, update_current=False), cron.get_current(datetime))
            for a in range(3)
        ]
        self.assertEqual(
            uretn,
            [
                (datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0)),
                (datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0)),
                (datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0)),
            ],
        )

        cron.set_current(datetime(2024, 7, 12), force=True)
        uretp = [
            (cron.get_prev(datetime, update_current=False), cron.get_current(datetime))
            for a in range(3)
        ]
        self.assertEqual(
            uretp,
            [
                (datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 12, 0, 0)),
                (datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 12, 0, 0)),
                (datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 12, 0, 0)),
            ],
        )

        cron.set_current(datetime(2024, 7, 12), force=True)
        r = cron.all_next(datetime, update_current=False)
        uretan = [(next(r), cron.get_current(datetime)) for a in range(3)]

        cron.set_current(datetime(2024, 7, 12), force=True)
        r = cron.all_prev(datetime, update_current=False)
        uretap = [(next(r), cron.get_current(datetime)) for a in range(3)]

        self.assertEqual(uretp, uretap)
        self.assertEqual(uretn, uretan)

    def test_issue_2038y(self):
        base = datetime(2040, 1, 1, 0, 0)
        itr = croniter("* * * * *", base)
        try:
            itr.get_next()
        except OverflowError:
            raise Exception("overflow not fixed!")

    def test_revert_issue_90_aka_support_dow7(self):
        self.assertTrue(croniter.is_valid("* * * * 1-7"))
        self.assertTrue(croniter.is_valid("* * * * 7"))

    def test_sunday_ranges_to(self):
        self._test_sunday_ranges(
            "0 0 * * Sun-Sun",
            [
                2,
                3,
                4,
                5,
                6,
                7,
                8,
                9,
                10,
                11,
                12,
                13,
                14,
                15,
                16,
                17,
                18,
                19,
                20,
                21,
                22,
                23,
                24,
                25,
                26,
                27,
                28,
                29,
                30,
                31,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Mon-Sun",
            [
                2,
                3,
                4,
                5,
                6,
                7,
                8,
                9,
                10,
                11,
                12,
                13,
                14,
                15,
                16,
                17,
                18,
                19,
                20,
                21,
                22,
                23,
                24,
                25,
                26,
                27,
                28,
                29,
                30,
                31,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Tue-Sun",
            [
                2,
                3,
                4,
                5,
                6,
                7,
                9,
                10,
                11,
                12,
                13,
                14,
                16,
                17,
                18,
                19,
                20,
                21,
                23,
                24,
                25,
                26,
                27,
                28,
                30,
                31,
                1,
                2,
                3,
                4,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Wed-Sun",
            [
                3,
                4,
                5,
                6,
                7,
                10,
                11,
                12,
                13,
                14,
                17,
                18,
                19,
                20,
                21,
                24,
                25,
                26,
                27,
                28,
                31,
                1,
                2,
                3,
                4,
                7,
                8,
                9,
                10,
                11,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Thu-Sun",
            [
                4,
                5,
                6,
                7,
                11,
                12,
                13,
                14,
                18,
                19,
                20,
                21,
                25,
                26,
                27,
                28,
                1,
                2,
                3,
                4,
                8,
                9,
                10,
                11,
                15,
                16,
                17,
                18,
                22,
                23,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Fri-Sun",
            [
                5,
                6,
                7,
                12,
                13,
                14,
                19,
                20,
                21,
                26,
                27,
                28,
                2,
                3,
                4,
                9,
                10,
                11,
                16,
                17,
                18,
                23,
                24,
                25,
                1,
                2,
                3,
                8,
                9,
                10,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Sat-Sun",
            [
                6,
                7,
                13,
                14,
                20,
                21,
                27,
                28,
                3,
                4,
                10,
                11,
                17,
                18,
                24,
                25,
                2,
                3,
                9,
                10,
                16,
                17,
                23,
                24,
                30,
                31,
                6,
                7,
                13,
                14,
            ],
        )

    def test_sunday_ranges_from(self):
        self._test_sunday_ranges(
            "0 0 * * Sun-Mon",
            [
                7,
                8,
                14,
                15,
                21,
                22,
                28,
                29,
                4,
                5,
                11,
                12,
                18,
                19,
                25,
                26,
                3,
                4,
                10,
                11,
                17,
                18,
                24,
                25,
                31,
                1,
                7,
                8,
                14,
                15,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Sun-Tue",
            [
                2,
                7,
                8,
                9,
                14,
                15,
                16,
                21,
                22,
                23,
                28,
                29,
                30,
                4,
                5,
                6,
                11,
                12,
                13,
                18,
                19,
                20,
                25,
                26,
                27,
                3,
                4,
                5,
                10,
                11,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Sun-Wed",
            [
                2,
                3,
                7,
                8,
                9,
                10,
                14,
                15,
                16,
                17,
                21,
                22,
                23,
                24,
                28,
                29,
                30,
                31,
                4,
                5,
                6,
                7,
                11,
                12,
                13,
                14,
                18,
                19,
                20,
                21,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Sun-Thu",
            [
                2,
                3,
                4,
                7,
                8,
                9,
                10,
                11,
                14,
                15,
                16,
                17,
                18,
                21,
                22,
                23,
                24,
                25,
                28,
                29,
                30,
                31,
                1,
                4,
                5,
                6,
                7,
                8,
                11,
                12,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Sun-Fri",
            [
                2,
                3,
                4,
                5,
                7,
                8,
                9,
                10,
                11,
                12,
                14,
                15,
                16,
                17,
                18,
                19,
                21,
                22,
                23,
                24,
                25,
                26,
                28,
                29,
                30,
                31,
                1,
                2,
                4,
                5,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Sun-Sat",
            [
                2,
                3,
                4,
                5,
                6,
                7,
                8,
                9,
                10,
                11,
                12,
                13,
                14,
                15,
                16,
                17,
                18,
                19,
                20,
                21,
                22,
                23,
                24,
                25,
                26,
                27,
                28,
                29,
                30,
                31,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Thu-Tue/2",
            [
                2,
                4,
                6,
                9,
                11,
                13,
                16,
                18,
                20,
                23,
                25,
                27,
                30,
                1,
                3,
                6,
                8,
                10,
                13,
                15,
                17,
                20,
                22,
                24,
                27,
                29,
                2,
                5,
                7,
                9,
            ],
        )

        self._test_sunday_ranges(
            "0 0 * * Thu-Tue/3",
            [
                4,
                7,
                11,
                14,
                18,
                21,
                25,
                28,
                1,
                4,
                8,
                11,
                15,
                18,
                22,
                25,
                29,
                3,
                7,
                10,
                14,
                17,
                21,
                24,
                28,
                31,
                4,
                7,
                11,
                14,
            ],
        )

    def test_mth_ranges_from(self):
        self._test_mth_cron_ranges(
            "0 0 1 Jan-Dec *",
            [
                "24 2",
                "24 3",
                "24 4",
                "24 5",
                "24 6",
                "24 7",
                "24 8",
                "24 9",
                "24 10",
                "24 11",
                "24 12",
                "25 1",
                "25 2",
                "25 3",
                "25 4",
                "25 5",
            ],
        )
        self._test_mth_cron_ranges(
            "0 0 1 Nov-Mar *",
            [
                "24 2",
                "24 3",
                "24 11",
                "24 12",
                "25 1",
                "25 2",
                "25 3",
                "25 11",
                "25 12",
                "26 1",
                "26 2",
                "26 3",
                "26 11",
                "26 12",
                "27 1",
                "27 2",
            ],
        )
        self._test_mth_cron_ranges(
            "0 0 1 Apr-Feb *",
            [
                "24 2",
                "24 4",
                "24 5",
                "24 6",
                "24 7",
                "24 8",
                "24 9",
                "24 10",
                "24 11",
                "24 12",
                "25 1",
                "25 2",
                "25 4",
                "25 5",
                "25 6",
                "25 7",
            ],
        )
        self._test_mth_cron_ranges(
            "0 0 1 Apr-Mar/3 *",
            [
                "24 4",
                "24 7",
                "24 10",
                "25 1",
                "25 4",
                "25 7",
                "25 10",
                "26 1",
                "26 4",
                "26 7",
                "26 10",
                "27 1",
                "27 4",
                "27 7",
                "27 10",
                "28 1",
            ],
        )
        self._test_mth_cron_ranges(
            "0 0 1 Apr-Mar/2 *",
            [
                "24 3",
                "24 4",
                "24 6",
                "24 8",
                "24 10",
                "24 12",
                "25 3",
                "25 4",
                "25 6",
                "25 8",
                "25 10",
                "25 12",
                "26 3",
                "26 4",
                "26 6",
                "26 8",
            ],
        )
        self._test_mth_cron_ranges(
            "0 0 1 Jan-Aug/2 *",
            [
                "24 3",
                "24 5",
                "24 7",
                "25 1",
                "25 3",
                "25 5",
                "25 7",
                "26 1",
                "26 3",
                "26 5",
                "26 7",
                "27 1",
                "27 3",
                "27 5",
                "27 7",
                "28 1",
            ],
        )
        self._test_mth_cron_ranges(
            "0 0 1 Jan-Aug/4 *",
            [
                "24 5",
                "25 1",
                "25 5",
                "26 1",
                "26 5",
                "27 1",
                "27 5",
                "28 1",
                "28 5",
                "29 1",
                "29 5",
                "30 1",
                "30 5",
                "31 1",
                "31 5",
                "32 1",
            ],
        )

    def _test_cron_ranges(
        self, expr, wanted, generator=None, loops=None, start=None, is_prev=None
    ):
        rets = (generator or gen_x_results)(
            expr, loops=loops or 10, start=start or datetime(2024, 1, 1), is_prev=is_prev
        )
        for ret in rets:
            self.assertEqual(wanted, ret)

    def _test_mth_cron_ranges(self, expr, wanted, loops=None, start=None, is_prev=None):
        return self._test_cron_ranges(
            expr,
            wanted,
            generator=gen_x_mth_results,
            loops=loops or 16,
            start=start,
            is_prev=is_prev,
        )

    def _test_sunday_ranges(self, expr, wanted, loops=None, start=None, is_prev=None):
        return self._test_cron_ranges(
            expr,
            wanted,
            generator=gen_all_sunday_forms,
            loops=loops or 30,
            start=start,
            is_prev=is_prev,
        )


def gen_x_mth_results(expr, loops=None, start=None, is_prev=None):
    start = start or datetime(2024, 1, 1)
    cron = croniter(expr, start_time=start)
    n = cron.get_prev if is_prev else cron.get_next
    return [[f"{str(a.year)[-2:]} {a.month}" for a in [n(datetime) for i in range(loops or 16)]]]


def gen_x_results(expr, loops=None, start=None, is_prev=None):
    start = start or datetime(2024, 1, 1)
    cron = croniter(expr, start_time=start)
    n = cron.get_prev if is_prev else cron.get_next
    return [[a.isoformat() for a in [n(datetime) for i in range(loops or 30)]]]


def gen_all_sunday_forms(expr, loops=None, start=None, is_prev=None):
    start = start or datetime(2024, 1, 1)
    cron = croniter(expr, start_time=start)
    n = cron.get_prev if is_prev else cron.get_next
    ret1 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
    cron = croniter(expr.lower().replace("sun", "7"), start_time=start)
    n = cron.get_prev if is_prev else cron.get_next
    ret2 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
    cron = croniter(expr.lower().replace("sun", "0"), start_time=start)
    n = cron.get_prev if is_prev else cron.get_next
    ret3 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
    return ret1, ret2, ret3


if __name__ == "__main__":
    unittest.main()
croniter-6.1.0rc1/src/croniter/tests/test_croniter_dst_repetition.py000077500000000000000000000032261507066033300261120ustar00rootroot00000000000000#!/usr/bin/env python
"""
All related DST croniter tests are isolated here.
"""
# -*- coding: utf-8 -*-

import os
import time
import unittest
from datetime import datetime

from croniter import cron_m, croniter
from croniter.tests import base

ORIG_OVERFLOW32B_MODE = cron_m.OVERFLOW32B_MODE


class CroniterDST138Test(base.TestCase):
    """
    See https://github.com/kiorky/croniter/issues/138.
    """

    _tz = "UTC"

    def setUp(self):
        self._time = os.environ.setdefault("TZ", "")
        self.base = datetime(2024, 1, 25, 4, 46)
        self.iter = croniter("*/5 * * * *", self.base)
        self.results = [
            datetime(2024, 1, 25, 4, 50),
            datetime(2024, 1, 25, 4, 55),
            datetime(2024, 1, 25, 5, 0),
        ]
        self.tzname, self.timezone = time.tzname, time.timezone

    def tearDown(self):
        cron_m.OVERFLOW32B_MODE = ORIG_OVERFLOW32B_MODE
        if not self._time:
            del os.environ["TZ"]
        else:
            os.environ["TZ"] = self._time
        time.tzset()

    def test_issue_138_dt_to_ts_32b(self):
        """
        test local tz, forcing 32b mode.
        """
        self._test(m32b=True)

    def test_issue_138_dt_to_ts_n(self):
        """
        test local tz, forcing non 32b mode.
        """
        self._test(m32b=False)

    def _test(self, tz="UTC", m32b=True):
        cron_m.OVERFLOW32B_MODE = m32b
        os.environ["TZ"] = tz
        time.tzset()
        res = [self.iter.get_next(datetime) for i in range(3)]
        self.assertEqual(res, self.results)


class CroniterDST138TestLocal(CroniterDST138Test):
    _tz = "UTC-8"


if __name__ == "__main__":
    unittest.main()
croniter-6.1.0rc1/src/croniter/tests/test_croniter_hash.py000077500000000000000000000504321507066033300240020ustar00rootroot00000000000000import random
import unittest
import uuid
from datetime import datetime, timedelta

from croniter import CroniterBadCronError, CroniterNotAlphaError, croniter
from croniter.tests import base


class CroniterHashBase(base.TestCase):
    epoch = datetime(2020, 1, 1, 0, 0)
    hash_id = "hello"

    def _test_iter(
        self, definition, expectations, delta, epoch=None, hash_id=None, next_type=None
    ):
        if epoch is None:
            epoch = self.epoch
        if hash_id is None:
            hash_id = self.hash_id
        if next_type is None:
            next_type = datetime
        if not isinstance(expectations, (list, tuple)):
            expectations = (expectations,)
        obj = croniter(definition, epoch, hash_id=hash_id)
        testval = obj.get_next(next_type)
        self.assertIn(testval, expectations)
        if delta is not None:
            self.assertEqual(obj.get_next(next_type), testval + delta)


class CroniterHashTest(CroniterHashBase):
    def test_hash_hourly(self):
        """Test manually-defined hourly"""
        self._test_iter("H * * * *", datetime(2020, 1, 1, 0, 10), timedelta(hours=1))

    def test_hash_daily(self):
        """Test manually-defined daily"""
        self._test_iter("H H * * *", datetime(2020, 1, 1, 11, 10), timedelta(days=1))

    def test_hash_weekly(self):
        """Test manually-defined weekly"""
        # croniter 1.0.5 changes the defined weekly range from (0, 6)
        # to (0, 7), to match cron's behavior that Sunday is 0 or 7.
        # This changes the hash, so test for either.
        self._test_iter(
            "H H * * H",
            (datetime(2020, 1, 3, 11, 10), datetime(2020, 1, 5, 11, 10)),
            timedelta(weeks=1),
        )

    def test_hash_monthly(self):
        """Test manually-defined monthly"""
        self._test_iter("H H H * *", datetime(2020, 1, 1, 11, 10), timedelta(days=31))

    def test_hash_yearly(self):
        """Test manually-defined yearly"""
        self._test_iter("H H H H *", datetime(2020, 9, 1, 11, 10), timedelta(days=365))

    def test_hash_second(self):
        """Test seconds

        If a sixth field is provided, seconds are included in the datetime()
        """
        self._test_iter("H H * * * H", datetime(2020, 1, 1, 11, 10, 32), timedelta(days=1))

    def test_hash_year(self):
        """Test years

        provide a seventh field as year
        """
        self._test_iter("H H * * * H H", datetime(2066, 1, 1, 11, 10, 32), timedelta(days=1))

    def test_hash_id_change(self):
        """Test a different hash_id returns different results given same definition and epoch"""
        self._test_iter("H H * * *", datetime(2020, 1, 1, 11, 10), timedelta(days=1))
        self._test_iter(
            "H H * * *", datetime(2020, 1, 1, 0, 24), timedelta(days=1), hash_id="different id"
        )

    def test_hash_epoch_change(self):
        """Test a different epoch returns different results given same definition and hash_id"""
        self._test_iter("H H * * *", datetime(2020, 1, 1, 11, 10), timedelta(days=1))
        self._test_iter(
            "H H * * *",
            datetime(2011, 11, 12, 11, 10),
            timedelta(days=1),
            epoch=datetime(2011, 11, 11, 11, 11),
        )

    def test_hash_range(self):
        """Test a hashed range definition"""
        self._test_iter("H H H(3-5) * *", datetime(2020, 1, 5, 11, 10), timedelta(days=31))
        self._test_iter(
            "H H * * * 0 H(2025-2030)", datetime(2029, 1, 1, 11, 10), timedelta(days=1)
        )

    def test_hash_division(self):
        """Test a hashed division definition"""
        self._test_iter("H H/3 * * *", datetime(2020, 1, 1, 2, 10), timedelta(hours=3))
        self._test_iter(
            "H H H H * H H/2", datetime(2020, 9, 1, 11, 10, 32), timedelta(days=365 * 2)
        )

    def test_hash_range_division(self):
        """Test a hashed range + division definition"""
        self._test_iter("H(30-59)/10 H * * *", datetime(2020, 1, 1, 11, 30), timedelta(minutes=10))

    def test_hash_invalid_range(self):
        """Test validation logic for range_begin and range_end values"""
        try:
            self._test_iter(
                "H(11-10) H * * *", datetime(2020, 1, 1, 11, 31), timedelta(minutes=10)
            )
        except CroniterBadCronError as ex:
            self.assertEqual(str(ex), "Range end must be greater than range begin")

    def test_hash_id_bytes(self):
        """Test hash_id as a bytes object"""
        self._test_iter(
            "H H * * *",
            datetime(2020, 1, 1, 14, 53),
            timedelta(days=1),
            hash_id=b"\x01\x02\x03\x04",
        )

    def test_hash_float(self):
        """Test result as a float object"""
        self._test_iter("H H * * *", 1577877000.0, (60 * 60 * 24), next_type=float)

    def test_invalid_definition(self):
        """Test an invalid definition raises CroniterNotAlphaError"""
        with self.assertRaises(CroniterNotAlphaError):
            croniter("X X * * *", self.epoch, hash_id=self.hash_id)

    def test_invalid_hash_id_type(self):
        """Test an invalid hash_id type raises TypeError"""
        with self.assertRaises(TypeError):
            croniter("H H * * *", self.epoch, hash_id={1: 2})

    def test_invalid_divisor(self):
        """Test an invalid divisor type raises CroniterBadCronError"""
        with self.assertRaises(CroniterBadCronError):
            croniter("* * H/0 * *", self.epoch, hash_id=self.hash_id)


class CroniterWordAliasTest(CroniterHashBase):
    def test_hash_word_midnight(self):
        """Test built-in @midnight

        @midnight is actually up to 3 hours after midnight, not exactly midnight
        """
        self._test_iter("@midnight", datetime(2020, 1, 1, 2, 10, 32), timedelta(days=1))

    def test_hash_word_hourly(self):
        """Test built-in @hourly"""
        self._test_iter("@hourly", datetime(2020, 1, 1, 0, 10, 32), timedelta(hours=1))

    def test_hash_word_daily(self):
        """Test built-in @daily"""
        self._test_iter("@daily", datetime(2020, 1, 1, 11, 10, 32), timedelta(days=1))

    def test_hash_word_weekly(self):
        """Test built-in @weekly"""
        # croniter 1.0.5 changes the defined weekly range from (0, 6)
        # to (0, 7), to match cron's behavior that Sunday is 0 or 7.
        # This changes the hash, so test for either.
        self._test_iter(
            "@weekly",
            (datetime(2020, 1, 3, 11, 10, 32), datetime(2020, 1, 5, 11, 10, 32)),
            timedelta(weeks=1),
        )

    def test_hash_word_monthly(self):
        """Test built-in @monthly"""
        self._test_iter("@monthly", datetime(2020, 1, 1, 11, 10, 32), timedelta(days=31))

    def test_hash_word_yearly(self):
        """Test built-in @yearly"""
        self._test_iter("@yearly", datetime(2020, 9, 1, 11, 10, 32), timedelta(days=365))

    def test_hash_word_annually(self):
        """Test built-in @annually

        @annually is the same as @yearly
        """
        obj_annually = croniter("@annually", self.epoch, hash_id=self.hash_id)
        obj_yearly = croniter("@yearly", self.epoch, hash_id=self.hash_id)
        self.assertEqual(obj_annually.get_next(datetime), obj_yearly.get_next(datetime))
        self.assertEqual(obj_annually.get_next(datetime), obj_yearly.get_next(datetime))


class CroniterHashExpanderBase(base.TestCase):
    def setUp(self):
        _rd = random.Random()
        _rd.seed(100)
        self.HASH_IDS = [uuid.UUID(int=_rd.getrandbits(128)).bytes for _ in range(350)]


class CroniterHashExpanderExpandMinutesTest(CroniterHashExpanderBase):
    MIN_VALUE = 0
    MAX_VALUE = 59
    TOTAL = 60

    def test_expand_minutes(self):
        minutes = set()
        expression = "H * * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            minutes.add(expanded[0][0][0])
        assert len(minutes) == self.TOTAL
        assert min(minutes) == self.MIN_VALUE
        assert max(minutes) == self.MAX_VALUE

    def test_expand_minutes_range_2_minutes(self):
        minutes = set()
        expression = "H/2 * * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _minutes = expanded[0][0]
            assert len(_minutes) == 30
            minutes.update(_minutes)
        assert len(minutes) == self.TOTAL
        assert min(minutes) == self.MIN_VALUE
        assert max(minutes) == self.MAX_VALUE

    def test_expand_minutes_range_3_minutes(self):
        minutes = set()
        expression = "H/3 * * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _minutes = expanded[0][0]
            assert len(_minutes) == 20
            minutes.update(_minutes)
        assert len(minutes) == self.TOTAL
        assert min(minutes) == self.MIN_VALUE
        assert max(minutes) == self.MAX_VALUE

    def test_expand_minutes_range_15_minutes(self):
        minutes = set()
        expression = "H/15 * * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _minutes = expanded[0][0]
            assert len(_minutes) == 4
            minutes.update(_minutes)
        assert len(minutes) == self.TOTAL
        assert min(minutes) == self.MIN_VALUE
        assert max(minutes) == self.MAX_VALUE

    def test_expand_minutes_with_full_range(self):
        minutes = set()
        expression = "H(0-59) * * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            minutes.add(expanded[0][0][0])
        assert len(minutes) == self.TOTAL
        assert min(minutes) == self.MIN_VALUE
        assert max(minutes) == self.MAX_VALUE


class CroniterHashExpanderExpandHoursTest(CroniterHashExpanderBase):
    MIN_VALUE = 0
    MAX_VALUE = 23
    TOTAL = 24

    def test_expand_hours(self):
        hours = set()
        expression = "H H * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            hours.add(expanded[0][1][0])
        assert len(hours) == self.TOTAL
        assert min(hours) == self.MIN_VALUE
        assert max(hours) == self.MAX_VALUE

    def test_expand_hours_range_every_2_hours(self):
        hours = set()
        expression = "H H/2 * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _hours = expanded[0][1]
            assert len(_hours) == 12
            hours.update(_hours)
        assert len(hours) == self.TOTAL
        assert min(hours) == self.MIN_VALUE
        assert max(hours) == self.MAX_VALUE

    def test_expand_hours_range_4_hours(self):
        hours = set()
        expression = "H H/4 * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _hours = expanded[0][1]
            assert len(_hours) == 6
            hours.update(_hours)
        assert len(hours) == self.TOTAL
        assert min(hours) == self.MIN_VALUE
        assert max(hours) == self.MAX_VALUE

    def test_expand_hours_range_8_hours(self):
        hours = set()
        expression = "H H/8 * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _hours = expanded[0][1]
            assert len(_hours) == 3
            hours.update(_hours)
        assert len(hours) == self.TOTAL
        assert min(hours) == self.MIN_VALUE
        assert max(hours) == self.MAX_VALUE

    def test_expand_hours_range_10_hours(self):
        hours = set()
        expression = "H H/10 * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _hours = expanded[0][1]
            assert len(_hours) in {2, 3}
            hours.update(_hours)
        assert len(hours) == self.TOTAL
        assert min(hours) == self.MIN_VALUE
        assert max(hours) == self.MAX_VALUE

    def test_expand_hours_range_12_hours(self):
        hours = set()
        expression = "H H/12 * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _hours = expanded[0][1]
            assert len(_hours) == 2
            hours.update(_hours)
        assert len(hours) == self.TOTAL
        assert min(hours) == self.MIN_VALUE
        assert max(hours) == self.MAX_VALUE

    def test_expand_hours_with_full_range(self):
        minutes = set()
        expression = "* H(0-23) * * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            minutes.add(expanded[0][1][0])
        assert len(minutes) == self.TOTAL
        assert min(minutes) == self.MIN_VALUE
        assert max(minutes) == self.MAX_VALUE


class CroniterHashExpanderExpandMonthDaysTest(CroniterHashExpanderBase):
    MIN_VALUE = 1
    MAX_VALUE = 31
    TOTAL = 31

    def test_expand_month_days(self):
        month_days = set()
        expression = "H H H * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            month_days.add(expanded[0][2][0])
        assert len(month_days) == self.TOTAL
        assert min(month_days) == self.MIN_VALUE
        assert max(month_days) == self.MAX_VALUE

    def test_expand_month_days_range_2_days(self):
        month_days = set()
        expression = "0 0 H/2 * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _days = expanded[0][2]
            assert len(_days) in {15, 16}
            month_days.update(_days)
        assert len(month_days) == self.TOTAL
        assert min(month_days) == self.MIN_VALUE
        assert max(month_days) == self.MAX_VALUE

    def test_expand_month_days_range_5_days(self):
        month_days = set()
        expression = "H H H/5 * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _days = expanded[0][2]
            assert len(_days) in {6, 7}
            month_days.update(_days)
        assert len(month_days) == self.TOTAL
        assert min(month_days) == self.MIN_VALUE
        assert max(month_days) == self.MAX_VALUE

    def test_expand_month_days_range_12_days(self):
        month_days = set()
        expression = "H H H/12 * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _days = expanded[0][2]
            assert len(_days) in {2, 3}
            month_days.update(_days)
        assert len(month_days) == self.TOTAL
        assert min(month_days) == self.MIN_VALUE
        assert max(month_days) == self.MAX_VALUE

    def test_expand_month_days_with_full_range(self):
        month_days = set()
        expression = "* * H(1-31) * *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            month_days.add(expanded[0][2][0])
        assert len(month_days) == self.TOTAL
        assert min(month_days) == self.MIN_VALUE
        assert max(month_days) == self.MAX_VALUE


class CroniterHashExpanderExpandMonthTest(CroniterHashExpanderBase):
    MIN_VALUE = 1
    MAX_VALUE = 12
    TOTAL = 12

    def test_expand_month_days(self):
        month_days = set()
        expression = "H H * H *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            month_days.add(expanded[0][3][0])
        assert len(month_days) == self.TOTAL
        assert min(month_days) == self.MIN_VALUE
        assert max(month_days) == self.MAX_VALUE

    def test_expand_month_days_range_2_months(self):
        months = set()
        expression = "H H * H/2 *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _months = expanded[0][3]
            assert len(_months) == 6
            months.update(_months)
        assert len(months) == self.TOTAL
        assert min(months) == self.MIN_VALUE
        assert max(months) == self.MAX_VALUE

    def test_expand_month_days_range_3_months(self):
        months = set()
        expression = "H H * H/3 *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _months = expanded[0][3]
            assert len(_months) == 4
            months.update(_months)
        assert len(months) == self.TOTAL
        assert min(months) == self.MIN_VALUE
        assert max(months) == self.MAX_VALUE

    def test_expand_month_days_range_5_months(self):
        months = set()
        expression = "H H * H/5 *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _months = expanded[0][3]
            assert len(_months) in {2, 3}
            months.update(_months)
        assert len(months) == self.TOTAL
        assert min(months) == self.MIN_VALUE
        assert max(months) == self.MAX_VALUE

    def test_expand_months_with_full_range(self):
        months = set()
        expression = "* * * H(1-12) *"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            months.add(expanded[0][3][0])
        assert len(months) == self.TOTAL
        assert min(months) == self.MIN_VALUE
        assert max(months) == self.MAX_VALUE


class CroniterHashExpanderExpandWeekDays(CroniterHashExpanderBase):
    MIN_VALUE = 0
    MAX_VALUE = 6
    TOTAL = 7

    def test_expand_week_days(self):
        week_days = set()
        expression = "H H * * H"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            week_days.add(expanded[0][4][0])
        assert len(week_days) == self.TOTAL
        assert min(week_days) == self.MIN_VALUE
        assert max(week_days) == self.MAX_VALUE

    def test_expand_week_days_range_2_days(self):
        days = set()
        expression = "H H * * H/2"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _days = expanded[0][4]
            assert len(_days) in {3, 4}
            days.update(_days)
        assert len(days) == self.TOTAL
        assert min(days) == self.MIN_VALUE
        assert max(days) == self.MAX_VALUE

    def test_expand_week_days_range_4_days(self):
        days = set()
        expression = "H H * * H/4"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            _days = expanded[0][4]
            assert len(_days) in {1, 2}
            days.update(_days)
        assert len(days) == self.TOTAL
        assert min(days) == self.MIN_VALUE
        assert max(days) == self.MAX_VALUE

    def test_expand_week_days_with_full_range(self):
        days = set()
        expression = "* * * * H(0-6)"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            days.add(expanded[0][4][0])
        assert len(days) == self.TOTAL
        assert min(days) == self.MIN_VALUE
        assert max(days) == self.MAX_VALUE


class CroniterHashExpanderExpandYearsTest(CroniterHashExpanderBase):
    def test_expand_years_by_division(self):
        years = set()
        year_min, year_max = croniter.RANGES[6]
        expression = "* * * * * * H/10"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            assert len(expanded[0][6]) == 13
            years.update(expanded[0][6])
        assert len(years) == year_max - year_min + 1
        assert min(years) == year_min
        assert max(years) == year_max

    def test_expand_years_by_range(self):
        years = set()
        expression = "* * * * * * H(2020-2030)"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            years.add(expanded[0][6][0])
        assert len(years) == 11
        assert min(years) == 2020
        assert max(years) == 2030

    def test_expand_years_by_range_and_division(self):
        years = set()
        expression = "* * * * * * H(2020-2050)/10"
        for hash_id in self.HASH_IDS:
            expanded = croniter.expand(expression, hash_id=hash_id)
            years.update(expanded[0][6])
        assert len(years) == 31
        assert min(years) == 2020
        assert max(years) == 2050


if __name__ == "__main__":
    unittest.main()
croniter-6.1.0rc1/src/croniter/tests/test_croniter_random.py000077500000000000000000000037321507066033300243400ustar00rootroot00000000000000import unittest
from datetime import datetime, timedelta

from croniter import croniter
from croniter.tests import base


class CroniterRandomTest(base.TestCase):
    epoch = datetime(2020, 1, 1, 0, 0)

    def test_random(self):
        """Test random definition"""
        obj = croniter("R R * * *", self.epoch)
        result_1 = obj.get_next(datetime)
        self.assertGreaterEqual(result_1, datetime(2020, 1, 1, 0, 0))
        self.assertLessEqual(result_1, datetime(2020, 1, 1, 0, 0) + timedelta(days=1))
        result_2 = obj.get_next(datetime)
        self.assertGreaterEqual(result_2, datetime(2020, 1, 2, 0, 0))
        self.assertLessEqual(result_2, datetime(2020, 1, 2, 0, 0) + timedelta(days=1))

    def test_random_range(self):
        """Test random definition within a range"""
        obj = croniter("R R R(10-20) * *", self.epoch)
        result_1 = obj.get_next(datetime)
        self.assertGreaterEqual(result_1, datetime(2020, 1, 10, 0, 0))
        self.assertLessEqual(result_1, datetime(2020, 1, 10, 0, 0) + timedelta(days=11))
        result_2 = obj.get_next(datetime)
        self.assertGreaterEqual(result_2, datetime(2020, 2, 10, 0, 0))
        self.assertLessEqual(result_2, datetime(2020, 2, 10, 0, 0) + timedelta(days=11))

    def test_random_float(self):
        """Test random definition, float result"""
        obj = croniter("R R * * *", self.epoch)
        result_1 = obj.get_next(float)
        self.assertGreaterEqual(result_1, 1577836800.0)
        self.assertLessEqual(result_1, 1577836800.0 + (60 * 60 * 24))
        result_2 = obj.get_next(float)
        self.assertGreaterEqual(result_2, 1577923200.0)
        self.assertLessEqual(result_2, 1577923200.0 + (60 * 60 * 24))

    def test_random_with_year(self):
        obj = croniter("* * * * * * R(2025-2030)", self.epoch)
        result = obj.get_next(datetime)
        self.assertGreaterEqual(result.year, 2025)
        self.assertLessEqual(result.year, 2030)


if __name__ == "__main__":
    unittest.main()
croniter-6.1.0rc1/src/croniter/tests/test_croniter_range.py000077500000000000000000000204461507066033300241550ustar00rootroot00000000000000#!/usr/bin/env python

import unittest
import zoneinfo
from datetime import datetime

import pytz

from croniter import (
    CroniterBadCronError,
    CroniterBadDateError,
    CroniterBadTypeRangeError,
    croniter,
    croniter_range,
)
from croniter.tests import base


class mydatetime(datetime):
    """."""


class CroniterRangeTest(base.TestCase):
    def test_1day_step(self):
        start = datetime(2016, 12, 2)
        stop = datetime(2016, 12, 10)
        fwd = list(croniter_range(start, stop, "0 0 * * *"))
        self.assertEqual(len(fwd), 9)
        self.assertEqual(fwd[0], start)
        self.assertEqual(fwd[-1], stop)
        # Test the same, but in reverse
        rev = list(croniter_range(stop, start, "0 0 * * *"))
        self.assertEqual(len(rev), 9)
        # Ensure forward/reverse are a mirror image
        rev.reverse()
        self.assertEqual(fwd, rev)

    def test_1day_step_no_ends(self):
        # Test without ends (exclusive)
        start = datetime(2016, 12, 2)
        stop = datetime(2016, 12, 10)
        fwd = list(croniter_range(start, stop, "0 0 * * *", exclude_ends=True))
        self.assertEqual(len(fwd), 7)
        self.assertNotEqual(fwd[0], start)
        self.assertNotEqual(fwd[-1], stop)
        # Test the same, but in reverse
        rev = list(croniter_range(stop, start, "0 0 * * *", exclude_ends=True))
        self.assertEqual(len(rev), 7)
        self.assertNotEqual(fwd[0], stop)
        self.assertNotEqual(fwd[-1], start)

    def test_1month_step(self):
        start = datetime(1982, 1, 1)
        stop = datetime(1983, 12, 31)
        res = list(croniter_range(start, stop, "0 0 1 * *"))
        self.assertEqual(len(res), 24)
        self.assertEqual(res[0], start)
        self.assertEqual(res[5].day, 1)
        self.assertEqual(res[-1], datetime(1983, 12, 1))

    def test_1minute_step_float(self):
        start = datetime(2000, 1, 1, 0, 0)
        stop = datetime(2000, 1, 1, 0, 1)
        res = list(croniter_range(start, stop, "* * * * *", ret_type=float))
        self.assertEqual(len(res), 2)
        self.assertEqual(res[0], 946684800.0)
        self.assertEqual(res[-1] - res[0], 60)

    def test_auto_ret_type(self):
        data = [
            (datetime(2019, 1, 1), datetime(2020, 1, 1), datetime),
            (1552252218.0, 1591823311.0, float),
        ]
        for start, stop, rtype in data:
            ret = list(croniter_range(start, stop, "0 0 * * *"))
            self.assertIsInstance(ret[0], rtype)

    def test_input_type_exceptions(self):
        dt_start1 = datetime(2019, 1, 1)
        dt_stop1 = datetime(2020, 1, 1)
        f_start1 = 1552252218.0
        f_stop1 = 1591823311.0
        # Mix start/stop types
        with self.assertRaises(TypeError):
            list(croniter_range(dt_start1, f_stop1, "0 * * * *"), ret_type=datetime)
        with self.assertRaises(TypeError):
            list(croniter_range(f_start1, dt_stop1, "0 * * * *"))

    def test_timezone_dst_pytz(self):
        """Test across DST transition, which technically is a timzone change in pytz."""
        tz = pytz.timezone("America/New_York")
        start = tz.localize(datetime(2020, 10, 30))
        stop = tz.localize(datetime(2020, 11, 10))
        res = list(croniter_range(start, stop, "0 0 * * *"))
        self.assertNotEqual(res[0].tzinfo, res[-1].tzinfo)
        self.assertEqual(len(res), 12)

    def test_extra_hour_day_prio(self):
        """Test New York jumps forward: 2020-03-08 02:00 -> 03:00 (UTC-5 -> UTC-4)."""
        tz = zoneinfo.ZoneInfo("America/New_York")
        cron = "0 3 * * *"
        start = datetime(2020, 3, 7, tzinfo=tz)
        end = datetime(2020, 3, 11, tzinfo=tz)
        ret = [i.isoformat() for i in croniter_range(start, end, cron)]
        self.assertEqual(
            ret,
            [
                "2020-03-07T03:00:00-05:00",
                "2020-03-08T03:00:00-04:00",
                "2020-03-09T03:00:00-04:00",
                "2020-03-10T03:00:00-04:00",
            ],
        )

    def test_extra_hour_day_prio_pytz(self):
        """Test New York jumps forward: 2020-03-08 02:00 -> 03:00 (UTC-5 -> UTC-4)."""

        def datetime_tz(*args, **kw):
            """Defined this in another branch.  single-use-version"""
            tzinfo = kw.pop("tzinfo")
            return tzinfo.localize(datetime(*args))

        tz = pytz.timezone("America/New_York")
        cron = "0 3 * * *"
        start = datetime_tz(2020, 3, 7, tzinfo=tz)
        end = datetime_tz(2020, 3, 11, tzinfo=tz)
        ret = [i.isoformat() for i in croniter_range(start, end, cron)]
        self.assertEqual(
            ret,
            [
                "2020-03-07T03:00:00-05:00",
                "2020-03-08T03:00:00-04:00",
                "2020-03-09T03:00:00-04:00",
                "2020-03-10T03:00:00-04:00",
            ],
        )

    def test_issue145_getnext(self):
        # Example of quarterly event cron schedule
        start = datetime(2020, 9, 24)
        cron = "0 13 8 1,4,7,10 wed"
        with self.assertRaises(CroniterBadDateError):
            it = croniter(cron, start, day_or=False, max_years_between_matches=1)
            it.get_next()
        # New functionality (0.3.35) allowing croniter to find spare matches of cron
        # patterns across multiple years
        it = croniter(cron, start, day_or=False, max_years_between_matches=5)
        self.assertEqual(it.get_next(datetime), datetime(2025, 1, 8, 13))

    def test_issue145_range(self):
        cron = "0 13 8 1,4,7,10 wed"
        matches = list(
            croniter_range(datetime(2020, 1, 1), datetime(2020, 12, 31), cron, day_or=False)
        )
        self.assertEqual(len(matches), 3)
        self.assertEqual(matches[0], datetime(2020, 1, 8, 13))
        self.assertEqual(matches[1], datetime(2020, 4, 8, 13))
        self.assertEqual(matches[2], datetime(2020, 7, 8, 13))

        # No matches within this range; therefore expect empty list
        matches = list(
            croniter_range(datetime(2020, 9, 30), datetime(2020, 10, 30), cron, day_or=False)
        )
        self.assertEqual(len(matches), 0)

    def test_croniter_range_derived_class(self):
        # trivial example extending croniter

        class croniter_nosec(croniter):
            """Like croniter, but it forbids second-level cron expressions."""

            @classmethod
            def expand(cls, expr_format, *args, **kwargs):
                if len(expr_format.split()) == 6:
                    raise CroniterBadCronError("Expected 'min hour day mon dow'")
                return croniter.expand(expr_format, *args, **kwargs)

        cron = "0 13 8 1,4,7,10 wed"
        matches = list(
            croniter_range(
                datetime(2020, 1, 1),
                datetime(2020, 12, 31),
                cron,
                day_or=False,
                _croniter=croniter_nosec,
            )
        )
        self.assertEqual(len(matches), 3)

        cron = "0 1 8 1,15,L wed 15,45"
        with self.assertRaises(CroniterBadCronError):
            # Should fail using the custom class that forbids the seconds expression
            croniter_nosec(cron)

        with self.assertRaises(CroniterBadCronError):
            # Should similarly fail because the custom class rejects seconds expr
            i = croniter_range(
                datetime(2020, 1, 1), datetime(2020, 12, 31), cron, _croniter=croniter_nosec
            )
            next(i)

    def test_dt_types(self):
        start = mydatetime(2020, 9, 24)
        stop = datetime(2020, 9, 28)
        try:
            list(croniter_range(start, stop, "0 0 * * *"))
        except CroniterBadTypeRangeError:
            self.fail("should not be triggered")

    def test_configure_second_location(self):
        start = datetime(2016, 12, 2, 0, 0, 0)
        stop = datetime(2016, 12, 2, 0, 1, 0)
        fwd = list(croniter_range(start, stop, "*/20 * * * * *", second_at_beginning=True))
        self.assertEqual(len(fwd), 4)
        self.assertEqual(fwd[0], start)
        self.assertEqual(fwd[-1], stop)

    def test_year_range(self):
        start = datetime(2010, 1, 1)
        stop = datetime(2030, 1, 1)
        fwd = list(croniter_range(start, stop, "0 0 1 1 ? 0 2020-2024,2028"))
        self.assertEqual(len(fwd), 6)
        self.assertEqual(fwd[0], datetime(2020, 1, 1))
        self.assertEqual(fwd[-1], datetime(2028, 1, 1))


if __name__ == "__main__":
    unittest.main()
croniter-6.1.0rc1/src/croniter/tests/test_croniter_speed.py000077500000000000000000000064161507066033300241620ustar00rootroot00000000000000#!/usr/bin/env python

import os
import unittest
import zoneinfo
from datetime import datetime
from timeit import Timer

from croniter import croniter
from croniter.tests import base


class CroniterSpeedTest(base.TestCase):
    def run_long_test(self, iterations=1):
        dt = datetime(2010, 1, 23, 12, 18)
        itr = croniter("*/1 * * * *", dt)
        for i in range(iterations):  # ~ 58
            itr.get_next()

        itr = croniter("*/5 * * * *", dt)
        for i in range(iterations):
            itr.get_next()

        dt = datetime(2010, 1, 24, 12, 2)
        itr = croniter("0 */3 * * *", dt)
        for i in range(iterations):
            itr.get_next()

        dt = datetime(2010, 2, 24, 12, 9)
        itr = croniter("0 0 */3 * *", dt)
        for i in range(iterations):
            itr.get_next(datetime)

        # test leap year
        dt = datetime(1996, 2, 27)
        itr = croniter("0 0 * * *", dt)
        for i in range(iterations):
            itr.get_next(datetime)

        dt2 = datetime(2000, 2, 27)
        itr2 = croniter("0 0 * * *", dt2)
        for i in range(iterations):
            itr2.get_next(datetime)

        dt = datetime(2010, 2, 25)
        itr = croniter("0 0 * * sat", dt)
        for i in range(iterations):
            itr.get_next(datetime)

        dt = datetime(2010, 1, 25)
        itr = croniter("0 0 1 * wed", dt)
        for i in range(iterations):
            itr.get_next(datetime)

        dt = datetime(2010, 1, 25)
        itr = croniter("0 0 1 * *", dt)
        for i in range(iterations):
            itr.get_next()

        dt = datetime(2010, 8, 25, 15, 56)
        itr = croniter("*/1 * * * *", dt)
        for i in range(iterations):
            itr.get_prev(datetime)

        dt = datetime(2010, 8, 25, 15, 0)
        itr = croniter("*/1 * * * *", dt)
        for i in range(iterations):
            itr.get_prev(datetime)

        dt = datetime(2010, 8, 25, 0, 0)
        itr = croniter("*/1 * * * *", dt)
        for i in range(iterations):
            itr.get_prev(datetime)

        dt = datetime(2010, 8, 25, 15, 56)
        itr = croniter("0 0 * * sat,sun", dt)
        for i in range(iterations):
            itr.get_prev(datetime)

        dt = datetime(2010, 2, 25)
        itr = croniter("0 0 * * 7", dt)
        for i in range(iterations):
            itr.get_prev(datetime)

        # dst regression test
        tz = zoneinfo.ZoneInfo("Europe/Bucharest")
        offsets = set()
        dst_cron = "15 0,3 * 3 *"
        dst_iters = int(2 * 31 * (iterations / 40))
        dt = datetime(2010, 1, 25, tzinfo=tz)
        itr = croniter(dst_cron, dt)
        for i in range(dst_iters):
            d = itr.get_next(datetime)
            offsets.add(d.utcoffset())
        itr = croniter(dst_cron, dt)
        for i in range(dst_iters):
            d = itr.get_prev(datetime)
            offsets.add(d.utcoffset())

    def test_not_long_time(self):
        iterations = int(os.environ.get("CRONITER_TEST_SPEED_ITERATIONS", "40"))
        globs = globals()
        globs.update(locals())
        t = Timer("self.run_long_test(iterations)", globals=globs)
        limit = 80
        ret = t.timeit(limit)
        self.assertTrue(ret < limit, f"Regression in croniter speed detected ({ret} {limit}).")


if __name__ == "__main__":
    unittest.main()
croniter-6.1.0rc1/tox.ini000066400000000000000000000010221507066033300152620ustar00rootroot00000000000000[tox]
minversion = 4
skipsdist = true
envlist = lint,test,cov
skip_missing_interpreters = true

[testenv]
allowlist_externals = uv
usedevelop = true
commands =
    test: uv run --active py.test       -v .
    cov:  uv run --active py.test --cov -v .

[testenv:lint]
allowlist_externals = uv
commands = uv run --group=lint --active ruff check --fix

[testenv:mypy]
allowlist_externals = uv
commands = uv run --group=mypy --active mypy .

[testenv:fmt]
allowlist_externals = uv
commands = uv run --group=format --active ruff format
croniter-6.1.0rc1/uv.lock000066400000000000000000005770521507066033300153000ustar00rootroot00000000000000version = 1
revision = 3
requires-python = ">=3.9"
resolution-markers = [
    "python_full_version >= '3.10'",
    "python_full_version < '3.10'",
]

[[package]]
name = "backports-tarfile"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" },
]

[[package]]
name = "build"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "colorama", marker = "os_name == 'nt'" },
    { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" },
    { name = "packaging" },
    { name = "pyproject-hooks" },
    { name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382, upload-time = "2025-08-01T21:27:07.844Z" },
]

[[package]]
name = "cachetools"
version = "6.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9d/61/e4fad8155db4a04bfb4734c7c8ff0882f078f24294d42798b3568eb63bff/cachetools-6.2.0.tar.gz", hash = "sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32", size = 30988, upload-time = "2025-08-25T18:57:30.924Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl", hash = "sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6", size = 11276, upload-time = "2025-08-25T18:57:29.684Z" },
]

[[package]]
name = "certifi"
version = "2025.10.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
]

[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
    { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
    { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
    { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
    { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
    { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
    { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
    { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
    { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
    { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
    { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
    { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
    { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
    { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
    { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
    { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
    { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
    { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
    { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
    { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
    { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
    { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
    { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
    { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
    { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
    { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
    { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
    { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
    { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
    { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
    { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
    { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
    { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
    { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
    { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
    { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
    { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
    { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
    { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
    { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
    { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
    { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
    { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
    { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
    { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
    { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
    { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
    { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
    { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
    { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
    { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
    { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
    { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
    { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
    { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
    { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
    { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
    { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
    { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
    { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
    { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
    { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
    { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
    { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
    { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
    { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
    { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
    { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
    { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
    { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
    { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
    { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" },
    { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" },
    { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" },
    { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" },
    { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" },
    { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" },
    { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" },
    { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" },
    { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" },
    { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" },
    { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" },
    { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" },
]

[[package]]
name = "chardet"
version = "5.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" },
]

[[package]]
name = "charset-normalizer"
version = "3.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" },
    { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" },
    { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" },
    { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" },
    { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" },
    { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" },
    { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" },
    { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" },
    { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" },
    { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" },
    { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" },
    { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" },
    { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" },
    { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" },
    { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" },
    { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" },
    { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" },
    { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" },
    { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" },
    { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" },
    { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" },
    { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" },
    { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
    { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
    { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
    { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" },
    { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" },
    { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" },
    { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" },
    { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" },
    { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" },
    { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" },
    { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" },
    { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
    { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
    { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
    { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
    { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
    { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
    { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
    { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
    { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
    { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
    { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
    { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
    { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
    { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
    { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
    { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
    { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
    { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
    { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
    { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
    { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
    { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
    { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" },
    { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" },
    { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" },
    { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" },
    { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" },
    { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" },
    { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" },
    { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" },
    { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" },
    { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" },
    { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" },
    { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
]

[[package]]
name = "check-manifest"
version = "0.50"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "build" },
    { name = "setuptools" },
    { name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/ab/7607952f2c8d34c4124309dd3ea17c256fd3420a4ade01322daf9402b0b5/check_manifest-0.50.tar.gz", hash = "sha256:d300f9f292986aa1a30424af44eb45c5644e0a810e392e62d553b24bb3393494", size = 44827, upload-time = "2024-10-09T08:10:01.71Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/71/55/92207fa9b92ac2ade5593b1280f804f2590a680b7fe96775eb26074eec6b/check_manifest-0.50-py3-none-any.whl", hash = "sha256:6ab3e3aa72a008da3314b432f4c768c9647b4d6d8032f9e1a4672a572118e48c", size = 20385, upload-time = "2024-10-09T08:09:59.963Z" },
]

[[package]]
name = "cmarkgfm"
version = "2024.11.20"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/44/df/99c139c587d1fb1633b8adb22a44c8ab6b002453ed4f97ceefb1759eacdf/cmarkgfm-2024.11.20.tar.gz", hash = "sha256:5dd01cf61975a8a57213cdef5ed870e936032f13fe93d60ddf659ffb9cf73c6a", size = 146799, upload-time = "2024-11-20T22:04:51.722Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/28/71/1e0f09be27ce7e087bf91267531080e5640f600502e482bbe0cff2291939/cmarkgfm-2024.11.20-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d74c8bba226751161b7f163fb3e71ee82453759e016df934927ab44aac532eb", size = 439420, upload-time = "2024-11-20T22:04:57.545Z" },
    { url = "https://files.pythonhosted.org/packages/b4/15/53032c238c3289ba3b5f70fb81f60921168234e1379a48752fd4b2b5e52c/cmarkgfm-2024.11.20-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3354d5497820e18eccf0552224cb2cba2cb27ab378ed99371523e2726027c04c", size = 444715, upload-time = "2024-11-20T22:04:59.382Z" },
    { url = "https://files.pythonhosted.org/packages/fa/61/9e339af11a0e21fff2422b4f06283c3153d92ef027726b469b8880f24a4b/cmarkgfm-2024.11.20-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d71ac6281034e0f9babe1e4aa370e1249ce8f253ad85ff60daadea8ef5da4b00", size = 417640, upload-time = "2024-11-20T22:05:00.635Z" },
    { url = "https://files.pythonhosted.org/packages/19/78/25fd7a435213a8027623b89fabd6f0ad4d8bf05caa4a9d2cd09215a2c4dc/cmarkgfm-2024.11.20-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:067bf44b49c23c589eb60ce772f2582259d9818e86a5936b0cd89fd203ff3f21", size = 438233, upload-time = "2024-11-20T22:05:01.996Z" },
    { url = "https://files.pythonhosted.org/packages/16/9f/b7ebe260f4f217d8a16d276cec33114188c6b41ddacbdb69c84015d0c890/cmarkgfm-2024.11.20-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:175ed1f9b76310821140f97c201da4384814cb38875374b83efcd7987651a9e4", size = 417681, upload-time = "2024-11-20T22:05:03.897Z" },
    { url = "https://files.pythonhosted.org/packages/74/e3/aa939d0211bac1cdfeb9c6087c0a23821b8c58c2156bb8cc8c53c45a3733/cmarkgfm-2024.11.20-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:211fad7b509cb1247b6d20a6ec14b0777793b922254b2e28357528001be09f0b", size = 447083, upload-time = "2024-11-20T22:05:06.783Z" },
    { url = "https://files.pythonhosted.org/packages/12/b7/7a63c51c710a2adca3fa36230812c18f240d28bd42f54f6cd34a50803567/cmarkgfm-2024.11.20-cp310-cp310-win32.whl", hash = "sha256:19c5ddef9349c0965931ecabed30d3611dbd88beddefa91565283bd54aa0041c", size = 116828, upload-time = "2024-11-20T21:23:36.969Z" },
    { url = "https://files.pythonhosted.org/packages/22/a8/c232b72be7243be237a4d366e2027a774c65233c350c3c4643e6f8512079/cmarkgfm-2024.11.20-cp310-cp310-win_amd64.whl", hash = "sha256:191ee4ad8073a8a2c2e9d723c7046e0d7f87cca8686ca96f1e7d6e2735a9ebf8", size = 127308, upload-time = "2024-11-20T21:23:38.42Z" },
    { url = "https://files.pythonhosted.org/packages/c1/7b/81c4bb7fd808de5c4f064153d53ae7ed6b8e9726db42c0683cbe4217eb99/cmarkgfm-2024.11.20-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccec2ae525e4435601e75be9d184d4df153bf5b7fc2605a0e1ac256f5c600b5b", size = 439437, upload-time = "2024-11-20T22:05:08.05Z" },
    { url = "https://files.pythonhosted.org/packages/73/1d/0bc9988d53deb52378e967c700e1401461547c39b6afd564364a49ba9222/cmarkgfm-2024.11.20-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9488d4b1049c111300678ff874fde0ddf0f8aaf7ce532d39f942a250e39b56a5", size = 444724, upload-time = "2024-11-20T22:05:09.332Z" },
    { url = "https://files.pythonhosted.org/packages/cf/22/70a4258ace2c8dc46c8e7827c3dc11b56ec34dc4367103b3088e95c5b699/cmarkgfm-2024.11.20-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0eb231d0fa3190ec807f577d60a5bea256fd17968c61c4379c4c1acfd6f61df", size = 417654, upload-time = "2024-11-20T22:05:10.514Z" },
    { url = "https://files.pythonhosted.org/packages/30/a1/4e8be9027776165f30b1e4fe32f5de9823494b6964783a60a6d884dee027/cmarkgfm-2024.11.20-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3ae9dfebf1a9f39bbd3ae7c6150133f603601138b753497ec2deb5ff96c2cb6", size = 438295, upload-time = "2024-11-20T22:05:12.251Z" },
    { url = "https://files.pythonhosted.org/packages/4e/45/c7905704fe5dbbacf0ce613335bc592cecfac76735db4b0bc22fc457aa88/cmarkgfm-2024.11.20-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:35e280f76a73a71c7e4a494b267137741fcd837667bf77a5b30e68b30ab2d3b1", size = 417653, upload-time = "2024-11-20T22:05:13.453Z" },
    { url = "https://files.pythonhosted.org/packages/21/9b/3de2ac0edce26e80f1c94fb82e8c567544d18f006dcfcc4cfe64f8b734bd/cmarkgfm-2024.11.20-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b06c24c2ea5a33eb63b8beeae04869b45ffd015fbd609b4266f11c015f2a7a38", size = 447040, upload-time = "2024-11-20T22:05:14.851Z" },
    { url = "https://files.pythonhosted.org/packages/d9/45/2bbf67af3e18860bc758df5afb440f285e827750d5b4c66c109c102ea92c/cmarkgfm-2024.11.20-cp311-cp311-win32.whl", hash = "sha256:22205f2aa5f7eba8770f8881143b760c5c93904d775f6a8dbe6e30123800d27f", size = 116835, upload-time = "2024-11-20T21:23:39.418Z" },
    { url = "https://files.pythonhosted.org/packages/ca/58/cb0d5ef111f4b14fcffe9f9304e32c5d63ee6b0fcf61589cb992ebcf25b8/cmarkgfm-2024.11.20-cp311-cp311-win_amd64.whl", hash = "sha256:1792cfb4e2f76237753436000cf4433296c869ecabbd714b3c6c27f5b72c49f3", size = 127311, upload-time = "2024-11-20T21:23:40.482Z" },
    { url = "https://files.pythonhosted.org/packages/8f/14/d49a73878f4e65d2796f1e4241454853aef85df3ee0124580af308621918/cmarkgfm-2024.11.20-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eab503e922bd2959c7e6148c69478dd44894201358370876028dd24aea776e24", size = 439973, upload-time = "2024-11-20T22:05:16.702Z" },
    { url = "https://files.pythonhosted.org/packages/16/76/95ee253e61d670fb2fddc8c92cf5da544cb2c6bec081aebece8ceb109822/cmarkgfm-2024.11.20-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c3d66866247eb72bd494db638ab8247f8452fa871ed6e254e5adfc0e2c15cd0", size = 445269, upload-time = "2024-11-20T22:05:18.587Z" },
    { url = "https://files.pythonhosted.org/packages/12/d7/a7a375608dea47c3cca72f9dfc577a070adeee7c0e04a548b61304f5d244/cmarkgfm-2024.11.20-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9347e1f06924bcf207d0b25cb91e51be7eccfa9e91f03528db2a27bfea364365", size = 418261, upload-time = "2024-11-20T22:05:19.786Z" },
    { url = "https://files.pythonhosted.org/packages/cd/22/0306ef1bac3f6ab17732edd9c5c103ae9679c3ee68f82e18038766535c18/cmarkgfm-2024.11.20-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9e8ae37fe7568bf259dcbcb07b95ee3bc3f744bec87162a1391e806c15d9ae", size = 438730, upload-time = "2024-11-20T22:05:21.565Z" },
    { url = "https://files.pythonhosted.org/packages/99/ec/683c69493988035cc12bec14cdebfb8b254e0b03154ebe9a290f53aae34c/cmarkgfm-2024.11.20-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d3eee488ee1bd0ce251d8d5cb853d68234035b2839600319694ca85e585791f0", size = 418035, upload-time = "2024-11-20T22:05:22.703Z" },
    { url = "https://files.pythonhosted.org/packages/8f/46/212e9ae69577ca0982c70c229f75f3d3fc434417fa3d3b5796c74e4939e3/cmarkgfm-2024.11.20-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a01e9d2b6621fba9c833f384d998e6afe0ad858b9e3e22dc71e2aa6caad1e9f3", size = 447528, upload-time = "2024-11-20T22:05:23.841Z" },
    { url = "https://files.pythonhosted.org/packages/cd/ce/e58c4ca1724899bc54128d0a5552f8de1603078c774a46b6f4859e8f068c/cmarkgfm-2024.11.20-cp312-cp312-win32.whl", hash = "sha256:0451df5f02cccc4e76a369e23457cba20e60a99f734ddb126bc3c772018d22dc", size = 116846, upload-time = "2024-11-20T21:23:41.497Z" },
    { url = "https://files.pythonhosted.org/packages/e4/3b/872737e3653a1c2b918f16583d63a9d5ab8101ad1f404e2661b21776f5be/cmarkgfm-2024.11.20-cp312-cp312-win_amd64.whl", hash = "sha256:207d77cc794a1b30ed00e763ee664abc2fe647b0a141067c0962b775bdbc56ce", size = 127350, upload-time = "2024-11-20T21:23:42.622Z" },
    { url = "https://files.pythonhosted.org/packages/25/5a/b7ff55e9bde4a3f00c27aff495e0f7c0e40841616a71e21fa6990c2cc636/cmarkgfm-2024.11.20-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd414cffe2f57ce652cfb1510637760cc761194518b0a712ceceb929a1a512d4", size = 439948, upload-time = "2024-11-20T22:05:24.995Z" },
    { url = "https://files.pythonhosted.org/packages/da/98/47e04e7b63730315e818b9eedb32c1393f264ba7a4a798955ca11e9351a6/cmarkgfm-2024.11.20-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c699e399c7878f5eeef11e51c67d06eb8c4d09d0beb7675b1d6b393c1c5fb9", size = 445250, upload-time = "2024-11-20T22:05:26.166Z" },
    { url = "https://files.pythonhosted.org/packages/aa/a0/1da2ccbce0b2048f12a5a692d2703226dc826beba361ce5f0e7df0420c95/cmarkgfm-2024.11.20-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d9dea0e0a0e9ad436ab11430333b6fa294de0daa00ff675ddac5757a7c628ec", size = 418172, upload-time = "2024-11-20T22:05:27.897Z" },
    { url = "https://files.pythonhosted.org/packages/55/0c/b335f52e4d52a66cadce3eb7662f0392a8036bf1d4349e9149c2d4754e9f/cmarkgfm-2024.11.20-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab2543f2d1070d2dbae37b7098ad3507bedc37f5252883ea879f884dbb7b7610", size = 438704, upload-time = "2024-11-20T22:05:29.02Z" },
    { url = "https://files.pythonhosted.org/packages/1b/34/38da4e22106fac7070d6733c662684deee595070775a4fb6f16f952ac5bd/cmarkgfm-2024.11.20-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ffbb3847e4d71de84e23abd30ff23fb3ce81f9096eb7bdc6569499c8b7a098b8", size = 418016, upload-time = "2024-11-20T22:05:30.446Z" },
    { url = "https://files.pythonhosted.org/packages/ec/ca/0800f288d0db27f65aa792fb534f5abfb672bfa942b649456909700e0f6b/cmarkgfm-2024.11.20-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f987178288a598ddfa8c6fc19dbcc6eb796b7a211266ff4bd1e8cae436fbfd3", size = 447541, upload-time = "2024-11-20T22:05:32.232Z" },
    { url = "https://files.pythonhosted.org/packages/5a/0d/1b3de020df6cac8074955806f38c2ba9bba533a34f4b0104123cdf632b9b/cmarkgfm-2024.11.20-cp313-cp313-win32.whl", hash = "sha256:90b3449bcbf8ab763ca693453ae29e85cf3bbd60b5580ea79e2ae056102b8cdd", size = 116846, upload-time = "2024-11-20T21:23:44.599Z" },
    { url = "https://files.pythonhosted.org/packages/f7/b0/2bb70efd2a10797d2c07262aca576d7c079fd740b2f475bceef79d24837a/cmarkgfm-2024.11.20-cp313-cp313-win_amd64.whl", hash = "sha256:78edaff25d16dc6257d21397f257f43a7fcc8375585c6d51f46896dabacf9b6a", size = 127346, upload-time = "2024-11-20T21:23:46.136Z" },
    { url = "https://files.pythonhosted.org/packages/7d/2a/c2da5a8aa28e0c632ed2fb4050a95a93c16a113d203747a28b92ee8507cb/cmarkgfm-2024.11.20-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2826e9518134ee7d548e1944462cfedf7e7cdf86ffda906d1251c221fae43baf", size = 439412, upload-time = "2024-11-20T22:05:57.236Z" },
    { url = "https://files.pythonhosted.org/packages/b5/77/4ea5b4d844f0f4cf95e2491d2f821dbe772bb276e7831e55c0a178abcb95/cmarkgfm-2024.11.20-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc74387e90d518156d493b52af63faab2d95710e5f582df8dd50952cbf601194", size = 444733, upload-time = "2024-11-20T22:05:58.499Z" },
    { url = "https://files.pythonhosted.org/packages/dd/ef/ff2168448fdb27685149b77700c96dcb7e0504051d72ce35bccd0055b15a/cmarkgfm-2024.11.20-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc07268f5b42ac37b36a8091ddc7b6a7a88c436718d7ac88f5ea0785f2bdc1ec", size = 417622, upload-time = "2024-11-20T22:05:59.859Z" },
    { url = "https://files.pythonhosted.org/packages/5a/db/2457bf396ff07e70db652f46dc248261d8774b07fdcd8d191d1b8dc7a4f2/cmarkgfm-2024.11.20-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3ad48791fdb16372423a43e82774121a5dd85af0ab0532d2c19f56bf46e73f18", size = 438232, upload-time = "2024-11-20T22:06:01.193Z" },
    { url = "https://files.pythonhosted.org/packages/52/55/bd03c6db5e0486b54e118fe433a7aeb87e91419eecac9393c25b38354407/cmarkgfm-2024.11.20-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d58e4a83bc813585f251e3151f55e09915c3c1728da87b47defa0e8c84c85a7c", size = 417673, upload-time = "2024-11-20T22:06:02.585Z" },
    { url = "https://files.pythonhosted.org/packages/64/75/dc5b9a998423ab7acf54f1c3a7430e5a69f96345c2fefe53b7f198684c44/cmarkgfm-2024.11.20-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:169f3bdc8c54ac5d9994b8f47817cf383a8ff960a2a7aa7527fdb180ec2317ce", size = 447075, upload-time = "2024-11-20T22:06:03.905Z" },
    { url = "https://files.pythonhosted.org/packages/19/66/86d57df39b63ed0a45e5ebd62249f2fb5f7331f19d802cdcd6db23dbcacc/cmarkgfm-2024.11.20-cp39-cp39-win32.whl", hash = "sha256:4bc2c0ca1ba3c113f6bd178cfa10e62c257c2ed04d3ef684e2ee8b05342f51bc", size = 116832, upload-time = "2024-11-20T21:23:55.411Z" },
    { url = "https://files.pythonhosted.org/packages/3d/6c/650b6ab2cdd0d4e10465a98d100d064e5042b45fe68a1471636afc6c7090/cmarkgfm-2024.11.20-cp39-cp39-win_amd64.whl", hash = "sha256:5d23226dc67534c6adf5739b7a5150fbcc2c5b9e0d48f8a0bdcfc5636980e19b", size = 127304, upload-time = "2024-11-20T21:23:56.972Z" },
]

[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]

[[package]]
name = "coverage"
version = "7.10.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" },
    { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" },
    { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" },
    { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" },
    { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" },
    { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" },
    { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" },
    { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" },
    { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" },
    { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" },
    { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" },
    { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" },
    { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" },
    { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" },
    { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" },
    { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" },
    { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" },
    { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" },
    { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" },
    { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" },
    { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" },
    { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" },
    { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" },
    { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" },
    { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" },
    { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" },
    { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" },
    { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" },
    { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" },
    { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" },
    { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" },
    { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" },
    { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" },
    { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" },
    { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" },
    { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" },
    { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" },
    { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" },
    { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" },
    { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" },
    { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" },
    { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" },
    { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" },
    { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" },
    { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" },
    { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" },
    { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" },
    { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" },
    { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" },
    { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" },
    { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" },
    { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" },
    { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" },
    { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" },
    { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" },
    { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" },
    { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" },
    { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" },
    { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" },
    { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" },
    { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" },
    { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" },
    { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" },
    { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" },
    { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" },
    { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" },
    { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" },
    { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" },
    { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" },
    { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" },
    { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" },
    { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" },
    { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" },
    { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" },
    { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" },
    { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" },
    { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" },
    { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" },
    { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" },
    { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" },
    { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" },
    { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" },
    { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" },
    { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" },
    { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" },
    { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" },
    { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" },
    { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" },
    { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" },
    { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" },
    { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" },
    { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" },
    { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" },
    { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" },
    { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" },
    { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" },
    { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" },
    { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" },
    { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" },
    { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" },
    { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" },
    { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" },
    { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" },
]

[package.optional-dependencies]
toml = [
    { name = "tomli", marker = "python_full_version <= '3.11'" },
]

[[package]]
name = "croniter"
version = "6.0.1rc1"
source = { editable = "." }
dependencies = [
    { name = "python-dateutil" },
]

[package.dev-dependencies]
dev = [
    { name = "coverage" },
    { name = "pytest" },
    { name = "pytest-cov" },
    { name = "pytz" },
    { name = "setuptools" },
]
format = [
    { name = "ruff" },
]
lint = [
    { name = "ruff" },
]
mypy = [
    { name = "mypy" },
    { name = "types-python-dateutil" },
    { name = "types-pytz" },
]
release = [
    { name = "zest-releaser", extra = ["recommended"] },
]
tox = [
    { name = "tox" },
]

[package.metadata]
requires-dist = [{ name = "python-dateutil" }]

[package.metadata.requires-dev]
dev = [
    { name = "coverage", specifier = ">=4.2" },
    { name = "pytest", specifier = ">=8.3.3" },
    { name = "pytest-cov", specifier = ">=5.0.0" },
    { name = "pytz", specifier = ">2021.1" },
    { name = "setuptools" },
]
format = [{ name = "ruff" }]
lint = [{ name = "ruff" }]
mypy = [
    { name = "mypy" },
    { name = "types-python-dateutil" },
    { name = "types-pytz" },
]
release = [{ name = "zest-releaser", extras = ["recommended"], specifier = ">=6.7" }]
tox = [{ name = "tox", specifier = ">=4" }]

[[package]]
name = "cryptography"
version = "46.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4a/9b/e301418629f7bfdf72db9e80ad6ed9d1b83c487c471803eaa6464c511a01/cryptography-46.0.2.tar.gz", hash = "sha256:21b6fc8c71a3f9a604f028a329e5560009cc4a3a828bfea5fcba8eb7647d88fe", size = 749293, upload-time = "2025-10-01T00:29:11.856Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/c6/38/b2adb2aa1baa6706adc3eb746691edd6f90a656a9a65c3509e274d15a2b8/cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02", size = 4297596, upload-time = "2025-10-01T00:27:25.258Z" },
    { url = "https://files.pythonhosted.org/packages/e4/27/0f190ada240003119488ae66c897b5e97149292988f556aef4a6a2a57595/cryptography-46.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb7fb9cd44c2582aa5990cf61a4183e6f54eea3172e54963787ba47287edd135", size = 4450899, upload-time = "2025-10-01T00:27:27.458Z" },
    { url = "https://files.pythonhosted.org/packages/85/d5/e4744105ab02fdf6bb58ba9a816e23b7a633255987310b4187d6745533db/cryptography-46.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9066cfd7f146f291869a9898b01df1c9b0e314bfa182cef432043f13fc462c92", size = 4300382, upload-time = "2025-10-01T00:27:29.091Z" },
    { url = "https://files.pythonhosted.org/packages/33/fb/bf9571065c18c04818cb07de90c43fc042c7977c68e5de6876049559c72f/cryptography-46.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:97e83bf4f2f2c084d8dd792d13841d0a9b241643151686010866bbd076b19659", size = 4017347, upload-time = "2025-10-01T00:27:30.767Z" },
    { url = "https://files.pythonhosted.org/packages/35/72/fc51856b9b16155ca071080e1a3ad0c3a8e86616daf7eb018d9565b99baa/cryptography-46.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:4a766d2a5d8127364fd936572c6e6757682fc5dfcbdba1632d4554943199f2fa", size = 4983500, upload-time = "2025-10-01T00:27:32.741Z" },
    { url = "https://files.pythonhosted.org/packages/c1/53/0f51e926799025e31746d454ab2e36f8c3f0d41592bc65cb9840368d3275/cryptography-46.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fab8f805e9675e61ed8538f192aad70500fa6afb33a8803932999b1049363a08", size = 4482591, upload-time = "2025-10-01T00:27:34.869Z" },
    { url = "https://files.pythonhosted.org/packages/86/96/4302af40b23ab8aa360862251fb8fc450b2a06ff24bc5e261c2007f27014/cryptography-46.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1e3b6428a3d56043bff0bb85b41c535734204e599c1c0977e1d0f261b02f3ad5", size = 4300019, upload-time = "2025-10-01T00:27:37.029Z" },
    { url = "https://files.pythonhosted.org/packages/9b/59/0be12c7fcc4c5e34fe2b665a75bc20958473047a30d095a7657c218fa9e8/cryptography-46.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:1a88634851d9b8de8bb53726f4300ab191d3b2f42595e2581a54b26aba71b7cc", size = 4950006, upload-time = "2025-10-01T00:27:40.272Z" },
    { url = "https://files.pythonhosted.org/packages/55/1d/42fda47b0111834b49e31590ae14fd020594d5e4dadd639bce89ad790fba/cryptography-46.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:be939b99d4e091eec9a2bcf41aaf8f351f312cd19ff74b5c83480f08a8a43e0b", size = 4482088, upload-time = "2025-10-01T00:27:42.668Z" },
    { url = "https://files.pythonhosted.org/packages/17/50/60f583f69aa1602c2bdc7022dae86a0d2b837276182f8c1ec825feb9b874/cryptography-46.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f13b040649bc18e7eb37936009b24fd31ca095a5c647be8bb6aaf1761142bd1", size = 4425599, upload-time = "2025-10-01T00:27:44.616Z" },
    { url = "https://files.pythonhosted.org/packages/d1/57/d8d4134cd27e6e94cf44adb3f3489f935bde85f3a5508e1b5b43095b917d/cryptography-46.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bdc25e4e01b261a8fda4e98618f1c9515febcecebc9566ddf4a70c63967043b", size = 4697458, upload-time = "2025-10-01T00:27:46.209Z" },
    { url = "https://files.pythonhosted.org/packages/93/22/d66a8591207c28bbe4ac7afa25c4656dc19dc0db29a219f9809205639ede/cryptography-46.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7155c0b004e936d381b15425273aee1cebc94f879c0ce82b0d7fecbf755d53a", size = 4287584, upload-time = "2025-10-01T00:27:57.018Z" },
    { url = "https://files.pythonhosted.org/packages/8c/3e/fac3ab6302b928e0398c269eddab5978e6c1c50b2b77bb5365ffa8633b37/cryptography-46.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a61c154cc5488272a6c4b86e8d5beff4639cdb173d75325ce464d723cda0052b", size = 4433796, upload-time = "2025-10-01T00:27:58.631Z" },
    { url = "https://files.pythonhosted.org/packages/7d/d8/24392e5d3c58e2d83f98fe5a2322ae343360ec5b5b93fe18bc52e47298f5/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9ec3f2e2173f36a9679d3b06d3d01121ab9b57c979de1e6a244b98d51fea1b20", size = 4292126, upload-time = "2025-10-01T00:28:00.643Z" },
    { url = "https://files.pythonhosted.org/packages/ed/38/3d9f9359b84c16c49a5a336ee8be8d322072a09fac17e737f3bb11f1ce64/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2fafb6aa24e702bbf74de4cb23bfa2c3beb7ab7683a299062b69724c92e0fa73", size = 3993056, upload-time = "2025-10-01T00:28:02.8Z" },
    { url = "https://files.pythonhosted.org/packages/d6/a3/4c44fce0d49a4703cc94bfbe705adebf7ab36efe978053742957bc7ec324/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0c7ffe8c9b1fcbb07a26d7c9fa5e857c2fe80d72d7b9e0353dcf1d2180ae60ee", size = 4967604, upload-time = "2025-10-01T00:28:04.783Z" },
    { url = "https://files.pythonhosted.org/packages/eb/c2/49d73218747c8cac16bb8318a5513fde3129e06a018af3bc4dc722aa4a98/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5840f05518caa86b09d23f8b9405a7b6d5400085aa14a72a98fdf5cf1568c0d2", size = 4465367, upload-time = "2025-10-01T00:28:06.864Z" },
    { url = "https://files.pythonhosted.org/packages/1b/64/9afa7d2ee742f55ca6285a54386ed2778556a4ed8871571cb1c1bfd8db9e/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:27c53b4f6a682a1b645fbf1cd5058c72cf2f5aeba7d74314c36838c7cbc06e0f", size = 4291678, upload-time = "2025-10-01T00:28:08.982Z" },
    { url = "https://files.pythonhosted.org/packages/50/48/1696d5ea9623a7b72ace87608f6899ca3c331709ac7ebf80740abb8ac673/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:512c0250065e0a6b286b2db4bbcc2e67d810acd53eb81733e71314340366279e", size = 4931366, upload-time = "2025-10-01T00:28:10.74Z" },
    { url = "https://files.pythonhosted.org/packages/eb/3c/9dfc778401a334db3b24435ee0733dd005aefb74afe036e2d154547cb917/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:07c0eb6657c0e9cca5891f4e35081dbf985c8131825e21d99b4f440a8f496f36", size = 4464738, upload-time = "2025-10-01T00:28:12.491Z" },
    { url = "https://files.pythonhosted.org/packages/dc/b1/abcde62072b8f3fd414e191a6238ce55a0050e9738090dc6cded24c12036/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48b983089378f50cba258f7f7aa28198c3f6e13e607eaf10472c26320332ca9a", size = 4419305, upload-time = "2025-10-01T00:28:14.145Z" },
    { url = "https://files.pythonhosted.org/packages/c7/1f/3d2228492f9391395ca34c677e8f2571fb5370fe13dc48c1014f8c509864/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e6f6775eaaa08c0eec73e301f7592f4367ccde5e4e4df8e58320f2ebf161ea2c", size = 4681201, upload-time = "2025-10-01T00:28:15.951Z" },
    { url = "https://files.pythonhosted.org/packages/b7/66/f42071ce0e3ffbfa80a88feadb209c779fda92a23fbc1e14f74ebf72ef6b/cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b", size = 4293123, upload-time = "2025-10-01T00:28:25.072Z" },
    { url = "https://files.pythonhosted.org/packages/a8/5d/1fdbd2e5c1ba822828d250e5a966622ef00185e476d1cd2726b6dd135e53/cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1", size = 4439524, upload-time = "2025-10-01T00:28:26.808Z" },
    { url = "https://files.pythonhosted.org/packages/c8/c1/5e4989a7d102d4306053770d60f978c7b6b1ea2ff8c06e0265e305b23516/cryptography-46.0.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff798ad7a957a5021dcbab78dfff681f0cf15744d0e6af62bd6746984d9c9e9c", size = 4297264, upload-time = "2025-10-01T00:28:29.327Z" },
    { url = "https://files.pythonhosted.org/packages/28/78/b56f847d220cb1d6d6aef5a390e116ad603ce13a0945a3386a33abc80385/cryptography-46.0.2-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cb5e8daac840e8879407acbe689a174f5ebaf344a062f8918e526824eb5d97af", size = 4011872, upload-time = "2025-10-01T00:28:31.479Z" },
    { url = "https://files.pythonhosted.org/packages/e1/80/2971f214b066b888944f7b57761bf709ee3f2cf805619a18b18cab9b263c/cryptography-46.0.2-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:3f37aa12b2d91e157827d90ce78f6180f0c02319468a0aea86ab5a9566da644b", size = 4978458, upload-time = "2025-10-01T00:28:33.267Z" },
    { url = "https://files.pythonhosted.org/packages/a5/84/0cb0a2beaa4f1cbe63ebec4e97cd7e0e9f835d0ba5ee143ed2523a1e0016/cryptography-46.0.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e38f203160a48b93010b07493c15f2babb4e0f2319bbd001885adb3f3696d21", size = 4472195, upload-time = "2025-10-01T00:28:36.039Z" },
    { url = "https://files.pythonhosted.org/packages/30/8b/2b542ddbf78835c7cd67b6fa79e95560023481213a060b92352a61a10efe/cryptography-46.0.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d19f5f48883752b5ab34cff9e2f7e4a7f216296f33714e77d1beb03d108632b6", size = 4296791, upload-time = "2025-10-01T00:28:37.732Z" },
    { url = "https://files.pythonhosted.org/packages/78/12/9065b40201b4f4876e93b9b94d91feb18de9150d60bd842a16a21565007f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:04911b149eae142ccd8c9a68892a70c21613864afb47aba92d8c7ed9cc001023", size = 4939629, upload-time = "2025-10-01T00:28:39.654Z" },
    { url = "https://files.pythonhosted.org/packages/f6/9e/6507dc048c1b1530d372c483dfd34e7709fc542765015425f0442b08547f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8b16c1ede6a937c291d41176934268e4ccac2c6521c69d3f5961c5a1e11e039e", size = 4471988, upload-time = "2025-10-01T00:28:41.822Z" },
    { url = "https://files.pythonhosted.org/packages/b1/86/d025584a5f7d5c5ec8d3633dbcdce83a0cd579f1141ceada7817a4c26934/cryptography-46.0.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:747b6f4a4a23d5a215aadd1d0b12233b4119c4313df83ab4137631d43672cc90", size = 4422989, upload-time = "2025-10-01T00:28:43.608Z" },
    { url = "https://files.pythonhosted.org/packages/4b/39/536370418b38a15a61bbe413006b79dfc3d2b4b0eafceb5581983f973c15/cryptography-46.0.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b275e398ab3a7905e168c036aad54b5969d63d3d9099a0a66cc147a3cc983be", size = 4685578, upload-time = "2025-10-01T00:28:45.361Z" },
    { url = "https://files.pythonhosted.org/packages/e3/0a/0d10eb970fe3e57da9e9ddcfd9464c76f42baf7b3d0db4a782d6746f788f/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fe245cf4a73c20592f0f48da39748b3513db114465be78f0a36da847221bd1b4", size = 4243379, upload-time = "2025-10-01T00:28:58.989Z" },
    { url = "https://files.pythonhosted.org/packages/7d/60/e274b4d41a9eb82538b39950a74ef06e9e4d723cb998044635d9deb1b435/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2b9cad9cf71d0c45566624ff76654e9bae5f8a25970c250a26ccfc73f8553e2d", size = 4409533, upload-time = "2025-10-01T00:29:00.785Z" },
    { url = "https://files.pythonhosted.org/packages/19/9a/fb8548f762b4749aebd13b57b8f865de80258083fe814957f9b0619cfc56/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9bd26f2f75a925fdf5e0a446c0de2714f17819bf560b44b7480e4dd632ad6c46", size = 4243120, upload-time = "2025-10-01T00:29:02.515Z" },
    { url = "https://files.pythonhosted.org/packages/71/60/883f24147fd4a0c5cab74ac7e36a1ff3094a54ba5c3a6253d2ff4b19255b/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:7282d8f092b5be7172d6472f29b0631f39f18512a3642aefe52c3c0e0ccfad5a", size = 4408940, upload-time = "2025-10-01T00:29:04.42Z" },
]

[[package]]
name = "distlib"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]

[[package]]
name = "docutils"
version = "0.22.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4a/c0/89fe6215b443b919cb98a5002e107cb5026854ed1ccb6b5833e0768419d1/docutils-0.22.2.tar.gz", hash = "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", size = 2289092, upload-time = "2025-09-20T17:55:47.994Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl", hash = "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8", size = 632667, upload-time = "2025-09-20T17:55:43.052Z" },
]

[[package]]
name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
]

[[package]]
name = "filelock"
version = "3.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
]

[[package]]
name = "id"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" },
]

[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]

[[package]]
name = "importlib-metadata"
version = "8.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
]

[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]

[[package]]
name = "jaraco-classes"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "more-itertools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
]

[[package]]
name = "jaraco-context"
version = "6.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "backports-tarfile", marker = "python_full_version < '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" },
]

[[package]]
name = "jaraco-functools"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "more-itertools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" },
]

[[package]]
name = "jeepney"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
]

[[package]]
name = "keyring"
version = "25.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "importlib-metadata", marker = "python_full_version < '3.12'" },
    { name = "jaraco-classes" },
    { name = "jaraco-context" },
    { name = "jaraco-functools" },
    { name = "jeepney", marker = "sys_platform == 'linux'" },
    { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
    { name = "secretstorage", version = "3.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and sys_platform == 'linux'" },
    { name = "secretstorage", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" },
]

[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
    "python_full_version < '3.10'",
]
dependencies = [
    { name = "mdurl", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
]

[[package]]
name = "markdown-it-py"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
    "python_full_version >= '3.10'",
]
dependencies = [
    { name = "mdurl", marker = "python_full_version >= '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]

[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]

[[package]]
name = "more-itertools"
version = "10.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
]

[[package]]
name = "mypy"
version = "1.18.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "mypy-extensions" },
    { name = "pathspec" },
    { name = "tomli", marker = "python_full_version < '3.11'" },
    { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" },
    { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" },
    { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" },
    { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" },
    { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" },
    { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" },
    { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" },
    { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" },
    { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" },
    { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" },
    { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" },
    { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" },
    { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" },
    { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" },
    { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" },
    { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" },
    { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" },
    { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" },
    { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" },
    { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" },
    { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" },
    { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" },
    { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" },
    { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" },
    { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" },
    { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" },
    { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" },
    { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" },
    { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" },
    { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" },
    { url = "https://files.pythonhosted.org/packages/3f/a6/490ff491d8ecddf8ab91762d4f67635040202f76a44171420bcbe38ceee5/mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b", size = 12807230, upload-time = "2025-09-19T00:09:49.471Z" },
    { url = "https://files.pythonhosted.org/packages/eb/2e/60076fc829645d167ece9e80db9e8375648d210dab44cc98beb5b322a826/mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133", size = 11895666, upload-time = "2025-09-19T00:10:53.678Z" },
    { url = "https://files.pythonhosted.org/packages/97/4a/1e2880a2a5dda4dc8d9ecd1a7e7606bc0b0e14813637eeda40c38624e037/mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6", size = 12499608, upload-time = "2025-09-19T00:09:36.204Z" },
    { url = "https://files.pythonhosted.org/packages/00/81/a117f1b73a3015b076b20246b1f341c34a578ebd9662848c6b80ad5c4138/mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac", size = 13244551, upload-time = "2025-09-19T00:10:17.531Z" },
    { url = "https://files.pythonhosted.org/packages/9b/61/b9f48e1714ce87c7bf0358eb93f60663740ebb08f9ea886ffc670cea7933/mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b", size = 13491552, upload-time = "2025-09-19T00:10:13.753Z" },
    { url = "https://files.pythonhosted.org/packages/c9/66/b2c0af3b684fa80d1b27501a8bdd3d2daa467ea3992a8aa612f5ca17c2db/mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0", size = 9765635, upload-time = "2025-09-19T00:10:30.993Z" },
    { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" },
]

[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]

[[package]]
name = "nh3"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/96cff0977357f60f06ec4368c4c7a7a26cccfe7c9fcd54f5378bf0428fd3/nh3-0.3.0.tar.gz", hash = "sha256:d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f", size = 19655, upload-time = "2025-07-17T14:43:37.05Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/b4/11/340b7a551916a4b2b68c54799d710f86cf3838a4abaad8e74d35360343bb/nh3-0.3.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb", size = 1427992, upload-time = "2025-07-17T14:43:06.848Z" },
    { url = "https://files.pythonhosted.org/packages/ad/7f/7c6b8358cf1222921747844ab0eef81129e9970b952fcb814df417159fb9/nh3-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2", size = 798194, upload-time = "2025-07-17T14:43:08.263Z" },
    { url = "https://files.pythonhosted.org/packages/63/da/c5fd472b700ba37d2df630a9e0d8cc156033551ceb8b4c49cc8a5f606b68/nh3-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95", size = 837884, upload-time = "2025-07-17T14:43:09.233Z" },
    { url = "https://files.pythonhosted.org/packages/4c/3c/cba7b26ccc0ef150c81646478aa32f9c9535234f54845603c838a1dc955c/nh3-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d", size = 996365, upload-time = "2025-07-17T14:43:10.243Z" },
    { url = "https://files.pythonhosted.org/packages/f3/ba/59e204d90727c25b253856e456ea61265ca810cda8ee802c35f3fadaab00/nh3-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35", size = 1071042, upload-time = "2025-07-17T14:43:11.57Z" },
    { url = "https://files.pythonhosted.org/packages/10/71/2fb1834c10fab6d9291d62c95192ea2f4c7518bd32ad6c46aab5d095cb87/nh3-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5", size = 995737, upload-time = "2025-07-17T14:43:12.659Z" },
    { url = "https://files.pythonhosted.org/packages/33/c1/8f8ccc2492a000b6156dce68a43253fcff8b4ce70ab4216d08f90a2ac998/nh3-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9", size = 980552, upload-time = "2025-07-17T14:43:13.763Z" },
    { url = "https://files.pythonhosted.org/packages/2f/d6/f1c6e091cbe8700401c736c2bc3980c46dca770a2cf6a3b48a175114058e/nh3-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5", size = 593618, upload-time = "2025-07-17T14:43:15.098Z" },
    { url = "https://files.pythonhosted.org/packages/23/1e/80a8c517655dd40bb13363fc4d9e66b2f13245763faab1a20f1df67165a7/nh3-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e", size = 598948, upload-time = "2025-07-17T14:43:16.064Z" },
    { url = "https://files.pythonhosted.org/packages/9a/e0/af86d2a974c87a4ba7f19bc3b44a8eaa3da480de264138fec82fe17b340b/nh3-0.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f", size = 580479, upload-time = "2025-07-17T14:43:17.038Z" },
    { url = "https://files.pythonhosted.org/packages/0c/e0/cf1543e798ba86d838952e8be4cb8d18e22999be2a24b112a671f1c04fd6/nh3-0.3.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a", size = 1442218, upload-time = "2025-07-17T14:43:18.087Z" },
    { url = "https://files.pythonhosted.org/packages/5c/86/a96b1453c107b815f9ab8fac5412407c33cc5c7580a4daf57aabeb41b774/nh3-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1", size = 823791, upload-time = "2025-07-17T14:43:19.721Z" },
    { url = "https://files.pythonhosted.org/packages/97/33/11e7273b663839626f714cb68f6eb49899da5a0d9b6bc47b41fe870259c2/nh3-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392", size = 811143, upload-time = "2025-07-17T14:43:20.779Z" },
    { url = "https://files.pythonhosted.org/packages/6a/1b/b15bd1ce201a1a610aeb44afd478d55ac018b4475920a3118ffd806e2483/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a", size = 1064661, upload-time = "2025-07-17T14:43:21.839Z" },
    { url = "https://files.pythonhosted.org/packages/8f/14/079670fb2e848c4ba2476c5a7a2d1319826053f4f0368f61fca9bb4227ae/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49", size = 997061, upload-time = "2025-07-17T14:43:23.179Z" },
    { url = "https://files.pythonhosted.org/packages/a3/e5/ac7fc565f5d8bce7f979d1afd68e8cb415020d62fa6507133281c7d49f91/nh3-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb", size = 924761, upload-time = "2025-07-17T14:43:24.23Z" },
    { url = "https://files.pythonhosted.org/packages/39/2c/6394301428b2017a9d5644af25f487fa557d06bc8a491769accec7524d9a/nh3-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1", size = 803959, upload-time = "2025-07-17T14:43:26.377Z" },
    { url = "https://files.pythonhosted.org/packages/4e/9a/344b9f9c4bd1c2413a397f38ee6a3d5db30f1a507d4976e046226f12b297/nh3-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9", size = 844073, upload-time = "2025-07-17T14:43:27.375Z" },
    { url = "https://files.pythonhosted.org/packages/66/3f/cd37f76c8ca277b02a84aa20d7bd60fbac85b4e2cbdae77cb759b22de58b/nh3-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62", size = 1000680, upload-time = "2025-07-17T14:43:28.452Z" },
    { url = "https://files.pythonhosted.org/packages/ee/db/7aa11b44bae4e7474feb1201d8dee04fabe5651c7cb51409ebda94a4ed67/nh3-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23", size = 1076613, upload-time = "2025-07-17T14:43:30.031Z" },
    { url = "https://files.pythonhosted.org/packages/97/03/03f79f7e5178eb1ad5083af84faff471e866801beb980cc72943a4397368/nh3-0.3.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450", size = 1001418, upload-time = "2025-07-17T14:43:31.429Z" },
    { url = "https://files.pythonhosted.org/packages/ce/55/1974bcc16884a397ee699cebd3914e1f59be64ab305533347ca2d983756f/nh3-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518", size = 986499, upload-time = "2025-07-17T14:43:32.459Z" },
    { url = "https://files.pythonhosted.org/packages/c9/50/76936ec021fe1f3270c03278b8af5f2079038116b5d0bfe8538ffe699d69/nh3-0.3.0-cp38-abi3-win32.whl", hash = "sha256:6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d", size = 599000, upload-time = "2025-07-17T14:43:33.852Z" },
    { url = "https://files.pythonhosted.org/packages/8c/ae/324b165d904dc1672eee5f5661c0a68d4bab5b59fbb07afb6d8d19a30b45/nh3-0.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95", size = 604530, upload-time = "2025-07-17T14:43:34.95Z" },
    { url = "https://files.pythonhosted.org/packages/5b/76/3165e84e5266d146d967a6cc784ff2fbf6ddd00985a55ec006b72bc39d5d/nh3-0.3.0-cp38-abi3-win_arm64.whl", hash = "sha256:d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2", size = 585971, upload-time = "2025-07-17T14:43:35.936Z" },
]

[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]

[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]

[[package]]
name = "pep440"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/9f/6934fbe88a573c91e4715ec2e6e346cf994460154dbc0d66db6a4096fa5d/pep440-0.1.2.tar.gz", hash = "sha256:58b37246cc2b13fee1ca2a3c092cb3704d21ecf621a5bdbb168e44e697f6d04d", size = 4378, upload-time = "2022-09-13T16:41:44.023Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/f6/eb/430277c5edc3442ddf969c7ef7de75aa65f9c59bf07d413ed1b706fd8abc/pep440-0.1.2-py3-none-any.whl", hash = "sha256:36d6ad73f2b5d07769294cafe183500ac89d848c922a3d3f521b968481880d51", size = 4555, upload-time = "2022-09-13T16:41:41.902Z" },
]

[[package]]
name = "platformdirs"
version = "4.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
]

[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]

[[package]]
name = "pycparser"
version = "2.23"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]

[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]

[[package]]
name = "pyproject-api"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "packaging" },
    { name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" },
]

[[package]]
name = "pyproject-hooks"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
]

[[package]]
name = "pyroma"
version = "5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "build" },
    { name = "docutils" },
    { name = "packaging" },
    { name = "pygments" },
    { name = "requests" },
    { name = "setuptools" },
    { name = "trove-classifiers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4b/b8/0b3e91b9967470f69b63379058180c3a00c4a1df424e8a73c3ba93ae32e2/pyroma-5.0.tar.gz", hash = "sha256:20b28f5ad08d2588618ac3a2f0a7ba7a4febe6aa514963e4211cfa6d2ad81eb5", size = 67034, upload-time = "2025-07-15T06:40:34.572Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/e0/67/b7d968fc38cecea90ad0cbb9fa281a5ad02d1ecf8dd9c9fd104a3967f149/pyroma-5.0-py3-none-any.whl", hash = "sha256:dabe6ad75e362b56ba189982510fd928ca8aecb8cb21ee18e927c2acf6dbf167", size = 22821, upload-time = "2025-07-15T06:40:32.721Z" },
]

[[package]]
name = "pytest"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "colorama", marker = "sys_platform == 'win32'" },
    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
    { name = "iniconfig" },
    { name = "packaging" },
    { name = "pluggy" },
    { name = "pygments" },
    { name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]

[[package]]
name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "coverage", extra = ["toml"] },
    { name = "pluggy" },
    { name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]

[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]

[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]

[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]

[[package]]
name = "readme-renderer"
version = "44.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "docutils" },
    { name = "nh3" },
    { name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" },
]

[package.optional-dependencies]
md = [
    { name = "cmarkgfm" },
]

[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "certifi" },
    { name = "charset-normalizer" },
    { name = "idna" },
    { name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]

[[package]]
name = "requests-toolbelt"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
]

[[package]]
name = "rfc3986"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" },
]

[[package]]
name = "rich"
version = "14.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
    { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
    { name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" },
]

[[package]]
name = "ruff"
version = "0.13.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/8e/f9f9ca747fea8e3ac954e3690d4698c9737c23b51731d02df999c150b1c9/ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e", size = 5438533, upload-time = "2025-10-02T19:29:31.582Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/d2/33/8f7163553481466a92656d35dea9331095122bb84cf98210bef597dd2ecd/ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c", size = 12484040, upload-time = "2025-10-02T19:28:49.199Z" },
    { url = "https://files.pythonhosted.org/packages/b0/b5/4a21a4922e5dd6845e91896b0d9ef493574cbe061ef7d00a73c61db531af/ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2", size = 13122975, upload-time = "2025-10-02T19:28:52.446Z" },
    { url = "https://files.pythonhosted.org/packages/40/90/15649af836d88c9f154e5be87e64ae7d2b1baa5a3ef317cb0c8fafcd882d/ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286", size = 12346621, upload-time = "2025-10-02T19:28:54.712Z" },
    { url = "https://files.pythonhosted.org/packages/a5/42/bcbccb8141305f9a6d3f72549dd82d1134299177cc7eaf832599700f95a7/ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838", size = 12574408, upload-time = "2025-10-02T19:28:56.679Z" },
    { url = "https://files.pythonhosted.org/packages/ce/19/0f3681c941cdcfa2d110ce4515624c07a964dc315d3100d889fcad3bfc9e/ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822", size = 12285330, upload-time = "2025-10-02T19:28:58.79Z" },
    { url = "https://files.pythonhosted.org/packages/10/f8/387976bf00d126b907bbd7725219257feea58650e6b055b29b224d8cb731/ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60", size = 13980815, upload-time = "2025-10-02T19:29:01.577Z" },
    { url = "https://files.pythonhosted.org/packages/0c/a6/7c8ec09d62d5a406e2b17d159e4817b63c945a8b9188a771193b7e1cc0b5/ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3", size = 14987733, upload-time = "2025-10-02T19:29:04.036Z" },
    { url = "https://files.pythonhosted.org/packages/97/e5/f403a60a12258e0fd0c2195341cfa170726f254c788673495d86ab5a9a9d/ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3", size = 14439848, upload-time = "2025-10-02T19:29:06.684Z" },
    { url = "https://files.pythonhosted.org/packages/39/49/3de381343e89364c2334c9f3268b0349dc734fc18b2d99a302d0935c8345/ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662", size = 13421890, upload-time = "2025-10-02T19:29:08.767Z" },
    { url = "https://files.pythonhosted.org/packages/ab/b5/c0feca27d45ae74185a6bacc399f5d8920ab82df2d732a17213fb86a2c4c/ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af", size = 13444870, upload-time = "2025-10-02T19:29:11.234Z" },
    { url = "https://files.pythonhosted.org/packages/50/a1/b655298a1f3fda4fdc7340c3f671a4b260b009068fbeb3e4e151e9e3e1bf/ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d", size = 13691599, upload-time = "2025-10-02T19:29:13.353Z" },
    { url = "https://files.pythonhosted.org/packages/32/b0/a8705065b2dafae007bcae21354e6e2e832e03eb077bb6c8e523c2becb92/ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0", size = 12421893, upload-time = "2025-10-02T19:29:15.668Z" },
    { url = "https://files.pythonhosted.org/packages/0d/1e/cbe7082588d025cddbb2f23e6dfef08b1a2ef6d6f8328584ad3015b5cebd/ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c", size = 12267220, upload-time = "2025-10-02T19:29:17.583Z" },
    { url = "https://files.pythonhosted.org/packages/a5/99/4086f9c43f85e0755996d09bdcb334b6fee9b1eabdf34e7d8b877fadf964/ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e", size = 13177818, upload-time = "2025-10-02T19:29:19.943Z" },
    { url = "https://files.pythonhosted.org/packages/9b/de/7b5db7e39947d9dc1c5f9f17b838ad6e680527d45288eeb568e860467010/ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989", size = 13618715, upload-time = "2025-10-02T19:29:22.527Z" },
    { url = "https://files.pythonhosted.org/packages/28/d3/bb25ee567ce2f61ac52430cf99f446b0e6d49bdfa4188699ad005fdd16aa/ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3", size = 12334488, upload-time = "2025-10-02T19:29:24.782Z" },
    { url = "https://files.pythonhosted.org/packages/cf/49/12f5955818a1139eed288753479ba9d996f6ea0b101784bb1fe6977ec128/ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2", size = 13455262, upload-time = "2025-10-02T19:29:26.882Z" },
    { url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" },
]

[[package]]
name = "secretstorage"
version = "3.3.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
    "python_full_version < '3.10'",
]
dependencies = [
    { name = "cryptography", marker = "python_full_version < '3.10'" },
    { name = "jeepney", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" },
]

[[package]]
name = "secretstorage"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
    "python_full_version >= '3.10'",
]
dependencies = [
    { name = "cryptography", marker = "python_full_version >= '3.10'" },
    { name = "jeepney", marker = "python_full_version >= '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/31/9f/11ef35cf1027c1339552ea7bfe6aaa74a8516d8b5caf6e7d338daf54fd80/secretstorage-3.4.0.tar.gz", hash = "sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c", size = 19748, upload-time = "2025-09-09T16:42:13.859Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" },
]

[[package]]
name = "setuptools"
version = "80.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
]

[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]

[[package]]
name = "tomli"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
    { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
    { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
    { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
    { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
    { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
    { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
    { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
    { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
    { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
    { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
    { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
    { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
    { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
    { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
    { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
    { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
    { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
    { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
    { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
    { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
    { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
    { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
    { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
    { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
    { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
    { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
    { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
    { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
    { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
    { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
]

[[package]]
name = "tox"
version = "4.30.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "cachetools" },
    { name = "chardet" },
    { name = "colorama" },
    { name = "filelock" },
    { name = "packaging" },
    { name = "platformdirs" },
    { name = "pluggy" },
    { name = "pyproject-api" },
    { name = "tomli", marker = "python_full_version < '3.11'" },
    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
    { name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/51/b2/cee55172e5e10ce030b087cd3ac06641e47d08a3dc8d76c17b157dba7558/tox-4.30.3.tar.gz", hash = "sha256:f3dd0735f1cd4e8fbea5a3661b77f517456b5f0031a6256432533900e34b90bf", size = 202799, upload-time = "2025-10-02T16:24:39.974Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/e2/e4/8bb9ce952820df4165eb34610af347665d6cb436898a234db9d84d093ce6/tox-4.30.3-py3-none-any.whl", hash = "sha256:a9f17b4b2d0f74fe0d76207236925a119095011e5c2e661a133115a8061178c9", size = 175512, upload-time = "2025-10-02T16:24:38.209Z" },
]

[[package]]
name = "trove-classifiers"
version = "2025.9.11.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/9a/778622bc06632529817c3c524c82749a112603ae2bbcf72ee3eb33a2c4f1/trove_classifiers-2025.9.11.17.tar.gz", hash = "sha256:931ca9841a5e9c9408bc2ae67b50d28acf85bef56219b56860876dd1f2d024dd", size = 16975, upload-time = "2025-09-11T17:07:50.97Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/e1/85/a4ff8758c66f1fc32aa5e9a145908394bf9cf1c79ffd1113cfdeb77e74e4/trove_classifiers-2025.9.11.17-py3-none-any.whl", hash = "sha256:5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd", size = 14158, upload-time = "2025-09-11T17:07:49.886Z" },
]

[[package]]
name = "twine"
version = "6.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "id" },
    { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
    { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" },
    { name = "packaging" },
    { name = "readme-renderer" },
    { name = "requests" },
    { name = "requests-toolbelt" },
    { name = "rfc3986" },
    { name = "rich" },
    { name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" },
]

[[package]]
name = "types-python-dateutil"
version = "2.9.0.20250822"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0c/0a/775f8551665992204c756be326f3575abba58c4a3a52eef9909ef4536428/types_python_dateutil-2.9.0.20250822.tar.gz", hash = "sha256:84c92c34bd8e68b117bff742bc00b692a1e8531262d4507b33afcc9f7716cd53", size = 16084, upload-time = "2025-08-22T03:02:00.613Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/ab/d9/a29dfa84363e88b053bf85a8b7f212a04f0d7343a4d24933baa45c06e08b/types_python_dateutil-2.9.0.20250822-py3-none-any.whl", hash = "sha256:849d52b737e10a6dc6621d2bd7940ec7c65fcb69e6aa2882acf4e56b2b508ddc", size = 17892, upload-time = "2025-08-22T03:01:59.436Z" },
]

[[package]]
name = "types-pytz"
version = "2025.2.0.20250809"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/07/e2/c774f754de26848f53f05defff5bb21dd9375a059d1ba5b5ea943cf8206e/types_pytz-2025.2.0.20250809.tar.gz", hash = "sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5", size = 10876, upload-time = "2025-08-09T03:14:17.453Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/db/d0/91c24fe54e565f2344d7a6821e6c6bb099841ef09007ea6321a0bac0f808/types_pytz-2025.2.0.20250809-py3-none-any.whl", hash = "sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db", size = 10095, upload-time = "2025-08-09T03:14:16.674Z" },
]

[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]

[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]

[[package]]
name = "virtualenv"
version = "20.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "distlib" },
    { name = "filelock" },
    { name = "platformdirs" },
    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" },
]

[[package]]
name = "wheel"
version = "0.45.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" },
]

[[package]]
name = "zest-releaser"
version = "9.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "build" },
    { name = "colorama" },
    { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
    { name = "packaging" },
    { name = "readme-renderer", extra = ["md"] },
    { name = "requests" },
    { name = "setuptools" },
    { name = "tomli", marker = "python_full_version < '3.11'" },
    { name = "twine" },
    { name = "wheel" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/9a/b697026d16206ea3f7562eb719d04e9ad3c0d18a9b3c940c02c1809980ce/zest_releaser-9.6.2.tar.gz", hash = "sha256:b13aa5f474a3f5a83ea0f1d168277ddefd0d8c8374f844490a7ce5436f072492", size = 122976, upload-time = "2025-04-11T09:58:00Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/48/fb/927c5b13ce1b6078e5ba4b2ba61c444074f007c729cf42c8c62cdcd44bfb/zest_releaser-9.6.2-py3-none-any.whl", hash = "sha256:642c98746cab45019c365038a6c997bf67e58b9073ee42f251d9a320279dfb91", size = 101388, upload-time = "2025-04-11T09:57:58.049Z" },
]

[package.optional-dependencies]
recommended = [
    { name = "check-manifest" },
    { name = "pep440" },
    { name = "pyroma" },
]

[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
]