pax_global_header00006660000000000000000000000064152351555370014525gustar00rootroot0000000000000052 comment=8984d6cd0788a8ac8d039512b37af454d2423dc5 caio-0.12.3/000077500000000000000000000000001523515553700125235ustar00rootroot00000000000000caio-0.12.3/.editorconfig000066400000000000000000000004351523515553700152020ustar00rootroot00000000000000root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true [*.{py,yml}] indent_style = space [*.py] indent_size = 4 [docs/**.py] max_line_length = 80 [*.rst] indent_size = 3 [Makefile] indent_style = tab [*.yml] indent_size = 2 caio-0.12.3/.gitattributes000066400000000000000000000002571523515553700154220ustar00rootroot00000000000000benchmark/*.png filter=lfs diff=lfs merge=lfs -text benchmark/results/*.png filter=lfs diff=lfs merge=lfs -text benchmark/results/*.csv.gz filter=lfs diff=lfs merge=lfs -text caio-0.12.3/.github/000077500000000000000000000000001523515553700140635ustar00rootroot00000000000000caio-0.12.3/.github/ISSUE_TEMPLATE.md000066400000000000000000000017201523515553700165700ustar00rootroot00000000000000## Long story short ## Expected behavior ## Actual behavior ## Steps to reproduce ## Environment info Kernel version: `replace here to "uname -a" output` File system: `your file system` I have been produced this problem with implementations: * [] `export CAIO_IMPL=linux` - Native linux implementation * [] `export CAIO_IMPL=thread` - Thread implementation * [] `export CAIO_IMPL=python` - Pure Python implementation ## Additional info caio-0.12.3/.github/workflows/000077500000000000000000000000001523515553700161205ustar00rootroot00000000000000caio-0.12.3/.github/workflows/ci.yml000066400000000000000000000102211523515553700172320ustar00rootroot00000000000000name: CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: lint: runs-on: ubuntu-latest strategy: matrix: include: - check: lint - check: mypy steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 with: python-version: "3.12" - run: uv sync - name: Run lint if: matrix.check == 'lint' run: uv run --with ruff ruff check caio tests - name: Run mypy if: matrix.check == 'mypy' run: uv run --with mypy mypy --allow-untyped-calls --allow-untyped-defs --allow-untyped-decorators caio tests: needs: lint runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - python: "3.10" os: ubuntu-latest - python: "3.11" os: ubuntu-latest - python: "3.12" os: ubuntu-latest - python: "3.13" os: ubuntu-latest - python: "3.14" os: ubuntu-latest # Fresh build under a free-threaded interpreter. None of the C # extensions declare Py_mod_gil yet, so importing one re-enables # the GIL by default - PYTHON_GIL=0 below keeps it off anyway so # the suite actually exercises real no-GIL concurrency (their # thread-safety is what tests/test_free_threading.py checks). - python: "3.13t" os: ubuntu-latest free_threaded: true - python: "3.14t" os: ubuntu-latest free_threaded: true - python: "3.10" os: windows-latest - python: "3.11" os: windows-latest - python: "3.13" os: windows-latest - python: "3.14" os: windows-latest - python: "3.10" os: macos-latest - python: "3.11" os: macos-latest - python: "3.12" os: macos-latest - python: "3.13" os: macos-latest - python: "3.14" os: macos-latest steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 with: python-version: "${{ matrix.python }}" - run: uv sync --extra develop - name: Run tests run: uv run pytest --cov=caio --cov-report=term-missing -sv tests env: FORCE_COLOR: 1 COVERALLS_PARALLEL: 'true' COVERALLS_SERVICE_NAME: github GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PYTHON_GIL: ${{ matrix.free_threaded && '0' || '' }} - name: Report coverage run: uv run coveralls continue-on-error: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Builds normally under a regular interpreter, then runs the suite through # a free-threaded one of the same X.Y against that same install. The # compiled extensions' SOABI won't match the free-threaded interpreter, so # they simply fail to import (same ImportError path as any unsupported # platform) - only python_aio ends up available, and unlike the `tests` # matrix's own free-threaded entries above, nothing gets the chance to # re-enable the GIL first. free-threaded-python-only: needs: lint runs-on: ubuntu-latest strategy: fail-fast: false matrix: python: ["3.13", "3.14"] steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 with: python-version: "${{ matrix.python }}" - run: uv sync --extra develop - run: uv python install "${{ matrix.python }}t" - name: Run tests under the free-threaded interpreter against this install run: | FREE_THREADED_PYTHON="$(uv python find "${{ matrix.python }}t")" PYTHONPATH="$(pwd)/.venv/lib/python${{ matrix.python }}/site-packages" "$FREE_THREADED_PYTHON" -m pytest -sv tests env: FORCE_COLOR: 1 finish: needs: - tests runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} parallel-finished: true caio-0.12.3/.github/workflows/publish.yml000066400000000000000000000172621523515553700203210ustar00rootroot00000000000000name: publish on: release: types: [published] jobs: sdist: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 - name: Set version from release tag run: uv version --frozen "${GITHUB_REF_NAME#v}" - uses: actions/setup-python@v5 with: python-version: '3.12' - run: pip install build - run: python -m build --sdist - uses: actions/upload-artifact@v4 with: name: sdist path: dist/*.tar.gz - name: Upload to GitHub Release uses: softprops/action-gh-release@v2 with: files: dist/*.tar.gz wheels-linux-x86_64: runs-on: ubuntu-latest container: quay.io/pypa/manylinux_2_34_x86_64 permissions: contents: write strategy: fail-fast: false matrix: python: [cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313, cp314-cp314, cp314-cp314t] steps: - uses: actions/checkout@v4 - name: Set version from release tag run: uv version --frozen "${GITHUB_REF_NAME#v}" - name: Build wheel run: | /opt/python/${{ matrix.python }}/bin/pip install -U setuptools wheel /opt/python/${{ matrix.python }}/bin/pip wheel . --no-build-isolation --no-deps -w /tmp/raw/ auditwheel repair /tmp/raw/caio-*.whl -w dist/ && rm /tmp/raw/caio-*.whl - uses: actions/upload-artifact@v4 with: name: wheel-linux-x86_64-${{ matrix.python }} path: dist/*.whl - name: Upload to GitHub Release uses: softprops/action-gh-release@v2 with: files: dist/*.whl wheels-linux-aarch64: runs-on: ubuntu-24.04-arm container: quay.io/pypa/manylinux_2_34_aarch64 permissions: contents: write strategy: fail-fast: false matrix: python: [cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313, cp314-cp314, cp314-cp314t] steps: - uses: actions/checkout@v4 - name: Set version from release tag run: uv version --frozen "${GITHUB_REF_NAME#v}" - name: Build wheel run: | /opt/python/${{ matrix.python }}/bin/pip install -U setuptools wheel /opt/python/${{ matrix.python }}/bin/pip wheel . --no-build-isolation --no-deps -w /tmp/raw/ auditwheel repair /tmp/raw/caio-*.whl -w dist/ && rm /tmp/raw/caio-*.whl - uses: actions/upload-artifact@v4 with: name: wheel-linux-aarch64-${{ matrix.python }} path: dist/*.whl - name: Upload to GitHub Release uses: softprops/action-gh-release@v2 with: files: dist/*.whl wheels-musllinux-x86_64: runs-on: ubuntu-latest container: quay.io/pypa/musllinux_1_2_x86_64 permissions: contents: write strategy: fail-fast: false matrix: python: [cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313, cp314-cp314, cp314-cp314t] steps: - uses: actions/checkout@v4 - name: Set version from release tag run: uv version --frozen "${GITHUB_REF_NAME#v}" - name: Build wheel run: | /opt/python/${{ matrix.python }}/bin/pip install -U setuptools wheel /opt/python/${{ matrix.python }}/bin/pip wheel . --no-build-isolation --no-deps -w /tmp/raw/ auditwheel repair /tmp/raw/caio-*.whl -w dist/ && rm /tmp/raw/caio-*.whl - uses: actions/upload-artifact@v4 with: name: wheel-musllinux-x86_64-${{ matrix.python }} path: dist/*.whl - name: Upload to GitHub Release uses: softprops/action-gh-release@v2 with: files: dist/*.whl wheels-musllinux-aarch64: # Deliberately NOT run inside `container:` (unlike every other wheels-* # job) - GitHub Actions cannot run JavaScript-based actions (checkout, # upload-artifact, the gh-release upload) inside an Alpine container on # an ARM64 runner ("JavaScript Actions in Alpine containers are only # supported on x64 Linux runners" - a known GitHub Actions limitation, # architecture-specific, which is why wheels-musllinux-x86_64 doesn't # need this workaround). Instead: checkout/upload/release run directly # on the plain Ubuntu ARM64 host, and only the actual musl-linked build # step below runs inside the musllinux container, invoked explicitly via # `docker run` against the checked-out workspace bind-mounted in - the # GitHub-hosted ARM64 runner image ships Docker already (proven by every # other container:-based job on this same runner type relying on it). runs-on: ubuntu-24.04-arm permissions: contents: write strategy: fail-fast: false matrix: python: [cp310-cp310, cp311-cp311, cp312-cp312, cp313-cp313, cp314-cp314, cp314-cp314t] steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 - name: Set version from release tag run: uv version --frozen "${GITHUB_REF_NAME#v}" - name: Build wheel run: | docker run --rm -v "${{ github.workspace }}:/work" -w /work \ quay.io/pypa/musllinux_1_2_aarch64 bash -c ' /opt/python/${{ matrix.python }}/bin/pip install -U setuptools wheel && /opt/python/${{ matrix.python }}/bin/pip wheel . --no-build-isolation --no-deps -w /tmp/raw/ && auditwheel repair /tmp/raw/caio-*.whl -w dist/ && rm /tmp/raw/caio-*.whl ' - uses: actions/upload-artifact@v4 with: name: wheel-musllinux-aarch64-${{ matrix.python }} path: dist/*.whl - name: Upload to GitHub Release uses: softprops/action-gh-release@v2 with: files: dist/*.whl wheels-macos: runs-on: macos-latest permissions: contents: write strategy: fail-fast: false matrix: python: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.13t", "3.14t"] steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 with: python-version: "${{ matrix.python }}" - name: Set version from release tag run: uv version --frozen "${GITHUB_REF_NAME#v}" - run: uv build --wheel - uses: actions/upload-artifact@v4 with: name: wheel-macos-${{ matrix.python }} path: dist/*.whl - name: Upload to GitHub Release uses: softprops/action-gh-release@v2 with: files: dist/*.whl wheels-windows: runs-on: windows-latest permissions: contents: write steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v5 - name: Set version from release tag run: uv version --frozen "${GITHUB_REF_NAME#v}" shell: bash - uses: actions/setup-python@v5 with: python-version: "3.12" - run: pip install build - run: python -m build --wheel - uses: actions/upload-artifact@v4 with: name: wheel-windows path: dist/*.whl - name: Upload to GitHub Release uses: softprops/action-gh-release@v2 with: files: dist/*.whl pypi: needs: [sdist, wheels-linux-x86_64, wheels-linux-aarch64, wheels-musllinux-x86_64, wheels-musllinux-aarch64, wheels-macos, wheels-windows] runs-on: ubuntu-latest environment: pypi permissions: id-token: write steps: - uses: actions/download-artifact@v4 with: pattern: 'wheel-*' path: dist merge-multiple: true - uses: actions/download-artifact@v4 with: name: sdist path: dist - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist skip-existing: true caio-0.12.3/.gitignore000066400000000000000000000034241523515553700145160ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ .venv* # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ *.bin caio-0.12.3/COPYING000066400000000000000000000261371523515553700135670ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2025 Dmitry Orlov Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. caio-0.12.3/MANIFEST.in000066400000000000000000000001171523515553700142600ustar00rootroot00000000000000recursive-include caio/src/threadpool *.* graft tests global-exclude *.py[cod] caio-0.12.3/Makefile000066400000000000000000000030311523515553700141600ustar00rootroot00000000000000build: sdist mac_wheel linux_wheel .PHONY: sdist mac_wheel linux_wheel sdist: python3 setup.py sdist .venvs: mkdir -p $@ .venvs/3.10: .venvs python3.10 -m venv $@ $@/bin/python -m pip install -U pip setuptools build wheel .venvs/3.11: .venvs python3.11 -m venv $@ $@/bin/python -m pip install -U pip setuptools build wheel .venvs/3.12: .venvs python3.12 -m venv $@ $@/bin/python -m pip install -U pip setuptools build wheel .venvs/3.13: .venvs python3.13 -m venv $@ $@/bin/python -m pip install -U pip setuptools build wheel .venvs/3.14: .venvs python3.14 -m venv $@ $@/bin/python -m pip install -U pip setuptools build wheel .venvs/3.13t: .venvs python3.13t -m venv $@ $@/bin/python -m pip install -U pip setuptools build wheel .venvs/3.14t: .venvs python3.14t -m venv $@ $@/bin/python -m pip install -U pip setuptools build wheel mac_wheel: .venvs/3.10 .venvs/3.11 .venvs/3.12 .venvs/3.13 .venvs/3.14 .venvs/3.13t .venvs/3.14t .venvs/3.10/bin/python -m build .venvs/3.11/bin/python -m build .venvs/3.12/bin/python -m build .venvs/3.13/bin/python -m build .venvs/3.14/bin/python -m build .venvs/3.13t/bin/python -m build .venvs/3.14t/bin/python -m build linux_wheel: docker run -it --rm \ -v `pwd`:/mnt \ --entrypoint /bin/bash \ --workdir /mnt \ --platform linux/amd64 \ quay.io/pypa/manylinux_2_34_x86_64 \ scripts/make-wheels.sh docker run -it --rm \ -v `pwd`:/mnt \ --entrypoint /bin/bash \ --platform linux/arm64 \ --workdir /mnt \ quay.io/pypa/manylinux_2_34_aarch64 \ scripts/make-wheels.sh caio-0.12.3/README.md000066400000000000000000000060221523515553700140020ustar00rootroot00000000000000Python wrapper for AIO ====================== Python bindings for async file I/O on Linux and a pure-Python/thread fallback for other platforms. Three backends are available: | Backend | Kernel | Notes | |-----------------|--------|------------------------------------------------------------| | `linux_uring` | ≥ 5.6 | io_uring - shared ring buffers, zero-syscall completions | | `linux_aio` | ≥ 4.18 | kernel AIO (`io_submit` / `io_getevents`), `O_DIRECT` only | | `thread_aio` | any | pthreads pool, portable | | `python_aio` | any | pure Python, no C extension required | Example ------- ```python import asyncio from caio import AsyncioContext loop = asyncio.get_event_loop() async def main(): # max_requests=128 by default ctx = AsyncioContext(max_requests=128) with open("test.file", "wb+") as fp: fd = fp.fileno() # Execute one write operation await ctx.write(b"Hello world", fd, offset=0) # Execute one read operation print(await ctx.read(32, fd, offset=0)) # Execute one fdsync operation await ctx.fdsync(fd) op1 = ctx.write(b"Hello from ", fd, offset=0) op2 = ctx.write(b"async world", fd, offset=11) await asyncio.gather(op1, op2) print(await ctx.read(32, fd, offset=0)) # Hello from async world loop.run_until_complete(main()) ``` Selecting a backend ------------------- `from caio import AsyncioContext` picks the best available backend automatically (`linux_uring` → `linux_aio` → `thread_aio` → `python_aio`). To force a specific backend use the `CAIO_IMPL` environment variable: ```bash CAIO_IMPL=uring python my_app.py # linux_uring CAIO_IMPL=linux python my_app.py # linux_aio CAIO_IMPL=thread python my_app.py # thread_aio CAIO_IMPL=python python my_app.py # python_aio ``` Or import a backend directly: ```python # io_uring (Linux ≥ 5.6) from caio.linux_uring_asyncio import AsyncioContext # kernel AIO (Linux ≥ 4.18) from caio.linux_aio_asyncio import AsyncioContext # thread pool from caio.thread_aio_asyncio import AsyncioContext ``` A `default_implementation` file placed next to `caio/__init__.py` (useful for distro package maintainers) may contain one of `uring`, `linux`, `thread`, or `python` on its first non-comment line. Troubleshooting --------------- ### io_uring blocked by seccomp Containers (Docker, Podman, Kubernetes) often block io_uring via a seccomp filter. The import will raise `ImportError` with a diagnostic message if `io_uring_setup(2)` returns `ENOSYS`. Fix: ```bash # Docker / Podman docker run --security-opt seccomp=unconfined ... # Kubernetes securityContext: seccompProfile: type: Unconfined ``` ### linux_aio compatibility `linux_aio` requires kernel ≥ 4.18 and a compatible filesystem. If it does not work in your environment you can fall back to `thread` or `python`: ```bash CAIO_IMPL=thread python my_app.py ``` caio-0.12.3/benchmark/000077500000000000000000000000001523515553700144555ustar00rootroot00000000000000caio-0.12.3/benchmark/.gitignore000066400000000000000000000000111523515553700164350ustar00rootroot00000000000000results/ caio-0.12.3/benchmark/bench.py000066400000000000000000000357501523515553700161200ustar00rootroot00000000000000#!/usr/bin/env python3 """ caio benchmark — latency distribution & throughput. Two orthogonal sweeps for each backend: 1. concurrency sweep — fix chunk=16 KB, vary in-flight ops: 1…512 2. chunk-size sweep — fix concurrency=64, vary chunk: 4 KB…1 MB Metrics per cell: ops/s · MB/s · mean · p50 · p95 · p99 · p999 (ms) Usage: CAIO_BENCH_DATA=/tmp/caio-bench CAIO_RESULTS=/tmp/results uv run python bench.py """ import asyncio import importlib import itertools import os import pathlib import random import sys import tempfile import time from typing import Callable, Iterator, List, Tuple DATA_DIR = pathlib.Path(os.environ.get("CAIO_BENCH_DATA", "data")) RESULTS_DIR = pathlib.Path(os.environ.get("CAIO_RESULTS", "/tmp/results")) TOTAL_OPS = 10_000 WARMUP_OPS = 500 MAX_REQUESTS = 4096 # sweep 1 — vary concurrency, fixed chunk CONC_SWEEP_CHUNK = 16 * 1024 CONC_SWEEP = [1, 4, 16, 64, 256, 512, 1024, 2048, 4096] # sweep 2 — vary chunk size, fixed concurrency CHUNK_SWEEP_CONC = 64 CHUNK_SWEEP_SIZES = [4 * 1024, 16 * 1024, 64 * 1024, 256 * 1024, 1024 * 1024, 4 * 1024 * 1024] BACKENDS = [ ("linux_uring", "caio.linux_uring_asyncio", "AsyncioContext"), ("linux_aio", "caio.linux_aio_asyncio", "AsyncioContext"), ("thread_aio", "caio.thread_aio_asyncio", "AsyncioContext"), ("python_aio", "caio.python_aio_asyncio", "AsyncioContext"), ] # ── stats ───────────────────────────────────────────────────────────────────── def pct(data: List[float], p: float) -> float: s = sorted(data) return s[min(int(len(s) * p), len(s) - 1)] def calc_stats(lats: List[float], wall: float, chunk: int, n_total: int) -> dict: ops = n_total / wall return dict( n_ops = n_total, wall_s = wall, ops_s = ops, mb_s = ops * chunk / 1e6, mean = sum(lats) / len(lats) * 1e3, p50 = pct(lats, 0.500) * 1e3, p95 = pct(lats, 0.950) * 1e3, p99 = pct(lats, 0.990) * 1e3, p999 = pct(lats, 0.999) * 1e3, ) # ── I/O sources ─────────────────────────────────────────────────────────────── class ReadPool: def __init__(self): self._fps = [] self._fds: List[Tuple[int, int]] = [] def open(self): paths = sorted(DATA_DIR.glob("*.bin")) if not paths: sys.exit(f"No .bin files in {DATA_DIR}. Run gen_data.py first.") for p in paths: fp = open(p, "rb") self._fps.append(fp) self._fds.append((fp.fileno(), p.stat().st_size)) def args(self, chunk: int) -> Tuple[int, int, int]: fd, size = random.choice(self._fds) hi = max(0, size - chunk) offset = (random.randint(0, hi) >> 12) << 12 # 4 KB aligned return chunk, fd, offset def pregenerate(self, n: int, chunk: int) -> Iterator[Tuple[int, int, int]]: return itertools.cycle([self.args(chunk) for _ in range(n)]) def pregenerate_seq(self, chunk: int) -> Iterator[Tuple[int, int, int]]: """Sequential read offsets cycling through all open files.""" args = [] for fd, size in self._fds: offset = 0 while offset + chunk <= size: args.append((chunk, fd, offset)) offset += chunk if not args: return self.pregenerate(1, chunk) return itertools.cycle(args) def close(self): for fp in self._fps: fp.close() class WriteTarget: # big enough for any chunk × (ops + warmup) SIZE = 1024 * 1024 * 1024 # 1 GB preallocated sparse file def __init__(self): self._fp = tempfile.NamedTemporaryFile(delete=False) self._fp.seek(self.SIZE - 1) self._fp.write(b"\x00") self._fp.flush() self.fd = self._fp.fileno() self._path = pathlib.Path(self._fp.name) self._bufs: dict[int, bytes] = {} def args(self, chunk: int) -> Tuple[bytes, int, int]: if chunk not in self._bufs: self._bufs[chunk] = os.urandom(chunk) hi = self.SIZE - chunk offset = (random.randint(0, hi) >> 12) << 12 return self._bufs[chunk], self.fd, offset def pregenerate(self, n: int, chunk: int) -> Iterator[Tuple[bytes, int, int]]: return itertools.cycle([self.args(chunk) for _ in range(n)]) def pregenerate_seq(self, chunk: int) -> Iterator[Tuple[bytes, int, int]]: """Sequential write offsets from 0 to SIZE.""" if chunk not in self._bufs: self._bufs[chunk] = os.urandom(chunk) n_slots = self.SIZE // chunk args = [(self._bufs[chunk], self.fd, i * chunk) for i in range(n_slots)] return itertools.cycle(args) def close(self): self._fp.close() self._path.unlink(missing_ok=True) # ── engine ──────────────────────────────────────────────────────────────────── SAMPLE_RATE = 0.1 # time 10% of ops, rest run without perf_counter overhead async def run( ctx, concurrency: int, n: int, op_fn: Callable, warmup: int, ) -> Tuple[List[float], float]: """ Run op_fn(ctx) exactly n times with `concurrency` ops in-flight at most. Uses a sliding window: exactly `concurrency` tasks exist at any time, a new one is spawned as soon as any completes. No semaphore needed. Only a fraction (SAMPLE_RATE) of ops are individually timed to avoid polluting the measurement with clock overhead. Throughput is always derived from wall time. Returns (sampled_latencies_in_seconds, wall_clock_seconds). """ async def timed() -> float: t = time.perf_counter() await op_fn(ctx) return time.perf_counter() - t async def untimed() -> float: await op_fn(ctx) return 0.0 sampled: set = set() # tasks whose result is a real latency _perf_counter = time.perf_counter async def _drain(pending, collect=None): done, pending = await asyncio.wait( pending, return_when=asyncio.FIRST_COMPLETED, ) if collect is not None: for task in done: if task in sampled: collect.append(task.result()) sampled.discard(task) else: sampled.difference_update(done) return pending # Warmup — sliding window, discard results if warmup: pending: set = set() left = warmup while left > 0 or pending: while len(pending) < concurrency and left > 0: pending.add(asyncio.create_task(untimed())) left -= 1 if pending: pending = await _drain(pending) # Decide which ops to sample (pre-generate for zero per-op overhead) sample_mask = bytearray(1 if random.random() < SAMPLE_RATE else 0 for _ in range(n)) # Measurement — sliding window, sampled latencies latencies: List[float] = [] pending = set() idx = 0 t0 = _perf_counter() while idx < n or pending: while len(pending) < concurrency and idx < n: if sample_mask[idx]: task = asyncio.create_task(timed()) sampled.add(task) else: task = asyncio.create_task(untimed()) pending.add(task) idx += 1 if pending: pending = await _drain(pending, latencies) wall = _perf_counter() - t0 return latencies, wall # ── formatting ──────────────────────────────────────────────────────────────── def _hdr(pivot_label: str, pivot_width: int) -> str: return ( f"{'backend':<14} {'op':<5} {pivot_label:>{pivot_width}}" f" {'ops/s':>8} {'MB/s':>6} {'wall':>6}" f" {'mean':>7} {'p50':>7} {'p95':>7} {'p99':>7} {'p999':>8} (ms)" ) def _row(backend: str, op: str, pivot: str, pivot_w: int, s: dict) -> str: return ( f"{backend:<14} {op:<5} {pivot:>{pivot_w}}" f" {s['ops_s']:>8.0f} {s['mb_s']:>6.1f} {s['wall_s']:>5.2f}s" f" {s['mean']:>7.3f} {s['p50']:>7.3f}" f" {s['p95']:>7.3f} {s['p99']:>7.3f} {s['p999']:>8.3f}" ) # ── sweeps ──────────────────────────────────────────────────────────────────── async def sweep_concurrency( name: str, CtxCls, pool: ReadPool, write: WriteTarget, access: str = "rand", ) -> List[str]: """Vary concurrency at fixed chunk size.""" csv: List[str] = [] chunk = CONC_SWEEP_CHUNK total = TOTAL_OPS + WARMUP_OPS sweep_tag = f"conc_sweep_{access}" for conc in CONC_SWEEP: ctx = CtxCls(max_requests=MAX_REQUESTS) pivot = str(conc) if access == "seq": read_it = pool.pregenerate_seq(chunk) write_it = write.pregenerate_seq(chunk) else: read_it = pool.pregenerate(total, chunk) write_it = write.pregenerate(total, chunk) for op_name, op_fn in [ ("read", lambda c: c.read(*next(read_it))), ("write", lambda c: c.write(*next(write_it))), ]: lats, wall = await run(ctx, conc, TOTAL_OPS, op_fn, WARMUP_OPS) s = calc_stats(lats, wall, chunk, TOTAL_OPS) print(_row(name, op_name, pivot, 5, s), flush=True) for lat in lats: csv.append( f"{name},{sweep_tag},{op_name},{conc},{chunk}," f"{lat * 1e6:.1f},{s['wall_s']:.4f},{s['n_ops']}" ) ctx.close() return csv async def sweep_chunk( name: str, CtxCls, pool: ReadPool, write: WriteTarget, access: str = "rand", ) -> List[str]: """Vary chunk size at fixed concurrency.""" csv: List[str] = [] conc = CHUNK_SWEEP_CONC ctx = CtxCls(max_requests=MAX_REQUESTS) total = TOTAL_OPS + WARMUP_OPS sweep_tag = f"chunk_sweep_{access}" for chunk in CHUNK_SWEEP_SIZES: pivot = f"{chunk // 1024}K" if access == "seq": read_it = pool.pregenerate_seq(chunk) write_it = write.pregenerate_seq(chunk) else: read_it = pool.pregenerate(total, chunk) write_it = write.pregenerate(total, chunk) for op_name, op_fn in [ ("read", lambda c: c.read(*next(read_it))), ("write", lambda c: c.write(*next(write_it))), ]: lats, wall = await run(ctx, conc, TOTAL_OPS, op_fn, WARMUP_OPS) s = calc_stats(lats, wall, chunk, TOTAL_OPS) print(_row(name, op_name, pivot, 5, s), flush=True) for lat in lats: csv.append( f"{name},{sweep_tag},{op_name},{conc},{chunk}," f"{lat * 1e6:.1f},{s['wall_s']:.4f},{s['n_ops']}" ) ctx.close() return csv # ── main ────────────────────────────────────────────────────────────────────── async def main(): RESULTS_DIR.mkdir(parents=True, exist_ok=True) pool = ReadPool() pool.open() write = WriteTarget() available = [] for label, mod_name, cls_name in BACKENDS: try: m = importlib.import_module(mod_name) available.append((label, getattr(m, cls_name))) except ImportError as e: print(f"skip {label}: {e}", file=sys.stderr) if not available: sys.exit("No backends available.") CSV_HEADER = "backend,sweep,op,concurrency,chunk_bytes,latency_us,wall_s,n_ops\n" per_backend: dict[str, List[str]] = {} for name, Cls in available: per_backend.setdefault(name, []) def _run_sweep(title: str, hdr: str, coro_fn): sep = "─" * len(hdr) print(f"\n{'━'*len(hdr)}") print(title) print(hdr) print(sep) return sep # ── sweep 1: concurrency, random ───────────────────────────────────────── hdr1 = _hdr("conc", 5) sep1 = _run_sweep( f"SWEEP 1 — concurrency / random (chunk={CONC_SWEEP_CHUNK//1024} KB, ops={TOTAL_OPS})", hdr1, None, ) for name, Cls in available: rows = await sweep_concurrency(name, Cls, pool, write, access="rand") per_backend[name].extend(rows) print(sep1) # ── sweep 2: chunk size, random ─────────────────────────────────────────── hdr2 = _hdr("chunk", 5) sep2 = _run_sweep( f"SWEEP 2 — chunk size / random (concurrency={CHUNK_SWEEP_CONC}, ops={TOTAL_OPS})", hdr2, None, ) for name, Cls in available: rows = await sweep_chunk(name, Cls, pool, write, access="rand") per_backend[name].extend(rows) print(sep2) # ── sweep 3: concurrency, sequential ───────────────────────────────────── sep3 = _run_sweep( f"SWEEP 3 — concurrency / sequential (chunk={CONC_SWEEP_CHUNK//1024} KB, ops={TOTAL_OPS})", hdr1, None, ) for name, Cls in available: rows = await sweep_concurrency(name, Cls, pool, write, access="seq") per_backend[name].extend(rows) print(sep3) # ── sweep 4: chunk size, sequential ────────────────────────────────────── sep4 = _run_sweep( f"SWEEP 4 — chunk size / sequential (concurrency={CHUNK_SWEEP_CONC}, ops={TOTAL_OPS})", hdr2, None, ) for name, Cls in available: rows = await sweep_chunk(name, Cls, pool, write, access="seq") per_backend[name].extend(rows) print(sep4) # ── save CSVs — always one file per backend ─────────────────────────────── for name, rows in per_backend.items(): out = RESULTS_DIR / f"bench_{name}.csv" out.write_text(CSV_HEADER + "\n".join(rows) + "\n") print(f"CSV → {out}") pool.close() write.close() if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument( "--backend", choices=[b[0] for b in BACKENDS], default=None, help="Run a single backend (default: all)", ) args = parser.parse_args() if args.backend: BACKENDS[:] = [b for b in BACKENDS if b[0] == args.backend] asyncio.run(main()) caio-0.12.3/benchmark/bench_runner.py000066400000000000000000000047011523515553700175010ustar00rootroot00000000000000#!/usr/bin/env python3 """ Run each caio backend in a separate subprocess so thread pools from one backend cannot influence measurements of the next. Every per-backend CSV uses the same schema. The merge step validates that schema instead of silently attaching an outdated header to wider rows. Usage: CAIO_BENCH_DATA=/tmp/caio-bench CAIO_RESULTS=/tmp/results uv run python bench_runner.py """ import csv import os import pathlib import subprocess import sys BACKENDS = ["linux_uring", "linux_aio", "thread_aio", "python_aio"] RESULTS_DIR = pathlib.Path(os.environ.get("CAIO_RESULTS", "/tmp/results")) DATA_DIR = pathlib.Path(os.environ.get("CAIO_BENCH_DATA", "/tmp/caio-bench")) BENCH = pathlib.Path(__file__).parent / "bench.py" env = os.environ.copy() CSV_COLUMNS = [ "backend", "sweep", "op", "concurrency", "chunk_bytes", "latency_us", "wall_s", "n_ops", ] def run_backend(name: str): print(f"\n{'━' * 80}", flush=True) print(f" backend: {name}", flush=True) print(f"{'━' * 80}", flush=True) proc = subprocess.run( [sys.executable, str(BENCH), "--backend", name], env=env, cwd=BENCH.parent, ) if proc.returncode != 0: print(f"[!] {name} exited with code {proc.returncode}", flush=True) def merge_results(): # Merge all per-backend CSVs into one, rejecting incompatible files # rather than producing a syntactically valid but column-shifted result. rows: list[dict[str, str]] = [] def collect(path: pathlib.Path): if not path.exists(): return with path.open(newline="") as fp: reader = csv.DictReader(fp) if reader.fieldnames != CSV_COLUMNS: print( f"[!] {path}: incompatible CSV header " f"{reader.fieldnames!r}; expected {CSV_COLUMNS!r}", flush=True, ) return rows.extend(reader) for name in BACKENDS: collect(RESULTS_DIR / f"bench_{name}.csv") merged = RESULTS_DIR / "bench_all.csv" with merged.open("w", newline="") as fp: writer = csv.DictWriter(fp, fieldnames=CSV_COLUMNS) writer.writeheader() writer.writerows(rows) print(f"\nMerged CSV → {merged}") def main(): RESULTS_DIR.mkdir(parents=True, exist_ok=True) for name in BACKENDS: run_backend(name) merge_results() if __name__ == "__main__": main() caio-0.12.3/benchmark/gen_data.py000066400000000000000000000014241523515553700165720ustar00rootroot00000000000000import hashlib import os import pathlib from multiprocessing.pool import ThreadPool from rich.progress import Progress POOL = ThreadPool(32) DATA_DIR = pathlib.Path(os.environ.get("CAIO_BENCH_DATA", "data")) def gen_data(file_id): seed = os.urandom(64) hasher = hashlib.sha512() with open(DATA_DIR / f"{file_id}.bin", "wb+") as fp: for _ in range(100000): hasher.update(seed) seed = hasher.digest() fp.write(seed) def main(): files = 128 DATA_DIR.mkdir(parents=True, exist_ok=True) with Progress() as progress: task = progress.add_task("Generating...", total=files) for _ in POOL.imap_unordered(gen_data, range(files)): progress.advance(task) if __name__ == "__main__": main() caio-0.12.3/benchmark/plot_results.py000066400000000000000000000470301523515553700175720ustar00rootroot00000000000000""" Plot and compare caio benchmark results with and without the GIL. Produces figures saved to CAIO_RESULTS dir (once per access pattern: rand/seq): concurrency_sweep_throughput_{rand,seq}.png concurrency_sweep_latency_{rand,seq}.png chunk_sweep_throughput_{rand,seq}.png chunk_sweep_latency_{rand,seq}.png latency_histograms.png (rand only) Usage: CAIO_RESULTS_GIL=/tmp/results-gil \ CAIO_RESULTS_NOGIL=/tmp/results-nogil \ PLOT_RESULTS=/tmp/results \ uv run --with matplotlib --with numpy python plot_results.py """ import csv import gzip import os import pathlib from collections import defaultdict from typing import Any import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np from matplotlib import ticker from PIL import Image SCRIPT_DIR = pathlib.Path(__file__).parent RESULTS_DIR = SCRIPT_DIR / "results" PLOT_RESULTS_DIR = pathlib.Path(os.environ.get("PLOT_RESULTS", RESULTS_DIR)) CSV_GIL_PATH = ( pathlib.Path(os.environ["CAIO_RESULTS_GIL"]) / "bench_all.csv" if "CAIO_RESULTS_GIL" in os.environ else RESULTS_DIR / "bench_all.csv.gz" ) CSV_NOGIL_PATH = ( pathlib.Path(os.environ["CAIO_RESULTS_NOGIL"]) / "bench_all.csv" if "CAIO_RESULTS_NOGIL" in os.environ else RESULTS_DIR / "bench_all_nogil.csv.gz" ) OUTPUT_SUFFIX = "" MODE_LABEL = "" BACKENDS = [ "linux_uring", "linux_aio", "thread_aio", "python_aio", ] OP_COLORS = {"read": "#3498db", "write": "#e74c3c"} OP_MARKERS = {"read": "o", "write": "s"} # ── data loading ────────────────────────────────────────────────────────────── Row = dict[str, str] def load_csv(path: pathlib.Path) -> list[Row]: if path.suffix == ".gz": with gzip.open(path, mode="rt", newline="") as fp: return list(csv.DictReader(fp)) with path.open(mode="r", newline="") as fp: return list(csv.DictReader(fp)) def pct(values: list[float], p: float) -> float: s = sorted(values) return s[min(int(len(s) * p), len(s) - 1)] CellData = dict[str, Any] # keys: lats (List[float]), wall_s (float), n_ops (int) def group( rows: list[Row], sweep: str, op: str, ) -> defaultdict[str, defaultdict[int, CellData]]: """Returns {backend: {pivot_value: {lats: [...], wall_s: float, n_ops: int}}}. `sweep` is the full sweep tag, e.g. 'conc_sweep_rand' or 'chunk_sweep_seq'. The pivot key is concurrency for conc sweeps, chunk_bytes for chunk sweeps. """ is_conc = sweep.startswith("conc_sweep") result: defaultdict[str, defaultdict[int, CellData]] = defaultdict( lambda: defaultdict(lambda: {"lats": [], "wall_s": 0.0, "n_ops": 0}), ) for r in rows: if r["sweep"] != sweep or r["op"] != op: continue if not r.get("latency_us"): continue pivot = int(r["concurrency"] if is_conc else r["chunk_bytes"]) cell = result[r["backend"]][pivot] cell["lats"].append(float(r["latency_us"])) if r.get("wall_s"): cell["wall_s"] = float(r["wall_s"]) if r.get("n_ops"): cell["n_ops"] = int(r["n_ops"]) return result # ── plot helpers ────────────────────────────────────────────────────────────── def apply_style(ax, xlabel: str, ylabel: str, title: str, xscale="linear", yscale="linear"): ax.set_xlabel(xlabel, fontsize=10) ax.set_ylabel(ylabel, fontsize=10) ax.set_title(title, fontsize=11, fontweight="bold") ax.set_xscale(xscale) ax.set_yscale(yscale) ax.grid(True, which="both", linestyle="--", alpha=0.4) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) def _mode_title(title: str) -> str: if not MODE_LABEL: return title return f"{title} — {MODE_LABEL}" def _output_path(name: str) -> pathlib.Path: stem, suffix = os.path.splitext(name) return PLOT_RESULTS_DIR / f"{stem}{OUTPUT_SUFFIX}{suffix}" def _percentile_footer( ax, rows: list[Row], sweep: str, backend: str, ) -> None: """Compact aggregate latency summary below every subplot.""" lines = [] for op in ("read", "write"): values = [ float(row["latency_us"]) / 1000 for row in rows if row["sweep"] == sweep and row["backend"] == backend and row["op"] == op and row.get("latency_us") ] if not values: continue stats = " ".join( f"p{int(quantile * 100)} {pct(values, quantile):.3f}" for quantile in (0.25, 0.50, 0.95, 0.99) ) lines.append(f"{op}: {stats} ms") if lines: ax.text( 0.5, -0.30, "\n".join(lines), transform=ax.transAxes, ha="center", va="top", fontsize=6.2, color="#555555", linespacing=1.35, ) def _combine_mode_rows( gil_path: pathlib.Path, nogil_path: pathlib.Path, output_path: pathlib.Path, ) -> None: """Stack GIL above no-GIL while preserving each row's exact layout.""" with Image.open(gil_path) as gil_image, Image.open(nogil_path) as nogil_image: width = max(gil_image.width, nogil_image.width) height = gil_image.height + nogil_image.height combined = Image.new("RGB", (width, height), "white") combined.paste(gil_image.convert("RGB"), (0, 0)) combined.paste(nogil_image.convert("RGB"), (0, gil_image.height)) combined.save(output_path) def fmt_chunk(b: int) -> str: if b >= 1024 * 1024: return f"{b // (1024*1024)}M" return f"{b // 1024}K" def _op_legend(ax): """Legend for read/write ops + band explanation.""" from matplotlib.lines import Line2D from matplotlib.patches import Patch elems = [ Line2D([0], [0], color=OP_COLORS[op], linewidth=2, marker=OP_MARKERS[op], markersize=5, label=op) for op in ("read", "write") ] + [ Patch(facecolor="gray", alpha=0.25, label="p25–p75"), Patch(facecolor="gray", alpha=0.10, label="p5–p95"), ] ax.legend(handles=elems, fontsize=7.5, framealpha=0.7) def _chunk_xticks(ax, chunks: list[int]): """Set numeric x-positions with human-readable tick labels.""" ax.set_xticks(range(len(chunks))) ax.set_xticklabels([fmt_chunk(c) for c in chunks]) def _conc_xticks(ax, values: list[int]): """Keep logarithmic concurrency labels readable in wide backend grids.""" ticks = values if len(ticks) > 6: ticks = ticks[::2] if ticks[-1] != values[-1]: ticks.append(values[-1]) ax.set_xticks(ticks) ax.tick_params(axis="x", labelsize=8) def _available(rows: list[Row]) -> list[str]: """Backends that actually appear in the CSV data.""" present = {r["backend"] for r in rows} return [b for b in BACKENDS if b in present] # ── figure 1: concurrency sweep — throughput ────────────────────────────────── def plot_conc_throughput(rows: list[Row], access: str = "rand"): backends = _available(rows) n = len(backends) if not n: return fig, axes = plt.subplots(1, n, figsize=(4.5 * n, 5), sharey=True) fig.suptitle( _mode_title( f"Throughput vs Concurrency (chunk=16 KB, {access})", ), fontsize=13, fontweight="bold", ) sweep = f"conc_sweep_{access}" if n == 1: axes = [axes] for col, backend in enumerate(backends): ax = axes[col] xs_all: list[int] = [] for op in ("read", "write"): data = group(rows, sweep, op) if backend not in data: continue d = data[backend] xs = sorted(d) xs_all = xs ys = [] for x in xs: cell = d[x] if cell["wall_s"] > 0 and cell["n_ops"] > 0: ys.append(cell["n_ops"] / cell["wall_s"] / 1000) else: mean_s = np.mean(cell["lats"]) / 1e6 ys.append(x / mean_s / 1000 if mean_s > 0 else 0) ax.plot(xs, ys, marker=OP_MARKERS[op], color=OP_COLORS[op], label=op, linewidth=1.8, markersize=6) apply_style(ax, "Concurrency", "kops/s", backend, xscale="log") ax.xaxis.set_major_formatter(ticker.ScalarFormatter()) _conc_xticks(ax, xs_all) ax.legend(fontsize=8, framealpha=0.7) _percentile_footer(ax, rows, sweep, backend) fig.tight_layout(rect=(0, 0.07, 1, 0.95), h_pad=3) out = _output_path(f"concurrency_sweep_throughput_{access}.png") fig.savefig(out, dpi=150, bbox_inches="tight") print(f"saved {out}") plt.close(fig) # ── figure 2: concurrency sweep — latency ──────────────────────────────────── def plot_conc_latency(rows: list[Row], access: str = "rand"): backends = _available(rows) n = len(backends) if not n: return fig, axes = plt.subplots(1, n, figsize=(4.5 * n, 5), sharey=True) fig.suptitle( _mode_title(f"Latency vs Concurrency (chunk=16 KB, {access})"), fontsize=13, fontweight="bold", ) sweep = f"conc_sweep_{access}" if n == 1: axes = [axes] for col, backend in enumerate(backends): ax = axes[col] pivots: list[int] = [] for op in ("read", "write"): data = group(rows, sweep, op) if backend not in data: continue d = data[backend] pivots = sorted(d) lats_list = [d[p]["lats"] for p in pivots] xs = list(pivots) p5s = [pct(l, 0.05) / 1000 for l in lats_list] p25s = [pct(l, 0.25) / 1000 for l in lats_list] p50s = [pct(l, 0.50) / 1000 for l in lats_list] p75s = [pct(l, 0.75) / 1000 for l in lats_list] p95s = [pct(l, 0.95) / 1000 for l in lats_list] color = OP_COLORS[op] ax.fill_between(xs, p5s, p95s, color=color, alpha=0.10) ax.fill_between(xs, p25s, p75s, color=color, alpha=0.25) ax.plot(xs, p50s, marker=OP_MARKERS[op], color=color, label=op, linewidth=1.8, markersize=5) apply_style(ax, "Concurrency", "Latency (ms)", backend, xscale="log", yscale="log") ax.xaxis.set_major_formatter(ticker.ScalarFormatter()) if pivots: _conc_xticks(ax, pivots) _op_legend(ax) _percentile_footer(ax, rows, sweep, backend) fig.tight_layout(rect=(0, 0.07, 1, 0.95), h_pad=3) out = _output_path(f"concurrency_sweep_latency_{access}.png") fig.savefig(out, dpi=150, bbox_inches="tight") print(f"saved {out}") plt.close(fig) # ── figure 3: chunk sweep — throughput (MB/s) ───────────────────────────────── def plot_chunk_throughput(rows: list[Row], access: str = "rand"): backends = _available(rows) n = len(backends) if not n: return fig, axes = plt.subplots(1, n, figsize=(4.5 * n, 5), sharey=True) fig.suptitle( _mode_title( f"Throughput vs Chunk Size (concurrency=64, {access})", ), fontsize=13, fontweight="bold", ) sweep = f"chunk_sweep_{access}" if n == 1: axes = [axes] for col, backend in enumerate(backends): ax = axes[col] chunks: list[int] = [] for op in ("read", "write"): data = group(rows, sweep, op) if backend not in data: continue d = data[backend] chunks = sorted(d) xpos = list(range(len(chunks))) ys = [] for chunk in chunks: cell = d[chunk] if cell["wall_s"] > 0 and cell["n_ops"] > 0: ops_s = cell["n_ops"] / cell["wall_s"] else: mean_s = np.mean(cell["lats"]) / 1e6 ops_s = 64 / mean_s if mean_s > 0 else 0 ys.append(ops_s * chunk / 1e6) # MB/s ax.plot(xpos, ys, marker=OP_MARKERS[op], color=OP_COLORS[op], label=op, linewidth=1.8, markersize=6) apply_style(ax, "Chunk size", "MB/s", backend) _chunk_xticks(ax, chunks) ax.legend(fontsize=8, framealpha=0.7) _percentile_footer(ax, rows, sweep, backend) fig.tight_layout(rect=(0, 0.07, 1, 0.95), h_pad=3) out = _output_path(f"chunk_sweep_throughput_{access}.png") fig.savefig(out, dpi=150, bbox_inches="tight") print(f"saved {out}") plt.close(fig) # ── figure 4: chunk sweep — latency ────────────────────────────────────────── def plot_chunk_latency(rows: list[Row], access: str = "rand"): backends = _available(rows) n = len(backends) if not n: return fig, axes = plt.subplots(1, n, figsize=(4.5 * n, 5), sharey=True) fig.suptitle( _mode_title(f"Latency vs Chunk Size (concurrency=64, {access})"), fontsize=13, fontweight="bold", ) sweep = f"chunk_sweep_{access}" if n == 1: axes = [axes] for col, backend in enumerate(backends): ax = axes[col] chunks: list[int] = [] for op in ("read", "write"): data = group(rows, sweep, op) if backend not in data: continue d = data[backend] chunks = sorted(d) xpos = list(range(len(chunks))) lats_list = [d[c]["lats"] for c in chunks] p5s = [pct(l, 0.05) / 1000 for l in lats_list] p25s = [pct(l, 0.25) / 1000 for l in lats_list] p50s = [pct(l, 0.50) / 1000 for l in lats_list] p75s = [pct(l, 0.75) / 1000 for l in lats_list] p95s = [pct(l, 0.95) / 1000 for l in lats_list] color = OP_COLORS[op] ax.fill_between(xpos, p5s, p95s, color=color, alpha=0.10) ax.fill_between(xpos, p25s, p75s, color=color, alpha=0.25) ax.plot(xpos, p50s, marker=OP_MARKERS[op], color=color, label=op, linewidth=1.8, markersize=5) apply_style(ax, "Chunk size", "Latency (ms)", backend, yscale="log") _chunk_xticks(ax, chunks) _op_legend(ax) _percentile_footer(ax, rows, sweep, backend) fig.tight_layout(rect=(0, 0.07, 1, 0.95), h_pad=3) out = _output_path(f"chunk_sweep_latency_{access}.png") fig.savefig(out, dpi=150, bbox_inches="tight") print(f"saved {out}") plt.close(fig) # ── figure 5: latency distribution histograms (concurrency=64, chunk=16K) ──── _HIST_COLORS = ["#3498db", "#e74c3c", "#2ecc71", "#f39c12", "#9b59b6"] def plot_histograms(rows: list[Row]): """Per-backend histogram of read latency at concurrency=64, chunk=16K.""" backends = _available(rows) if not backends: return target_conc = 64 ncols = min(len(backends), 4) nrows = (len(backends) + ncols - 1) // ncols fig, axes = plt.subplots(nrows, ncols, figsize=(6.5 * ncols, 4.5 * nrows), squeeze=False) fig.suptitle( _mode_title( f"Read latency distribution " f"(concurrency={target_conc}, chunk=16 KB)", ), fontsize=13, fontweight="bold", ) for idx, backend in enumerate(backends): ax = axes[idx // ncols][idx % ncols] lats = [ float(r["latency_us"]) / 1000 # us -> ms for r in rows if r["sweep"] == "conc_sweep_rand" and r["op"] == "read" and r["backend"] == backend and r.get("latency_us") and int(r["concurrency"]) == target_conc ] if not lats: ax.set_visible(False) continue p25 = pct(lats, 0.25) p50 = pct(lats, 0.50) p95 = pct(lats, 0.95) p99 = pct(lats, 0.99) clip = pct(lats, 0.999) ax.hist( [min(x, clip) for x in lats], bins=80, color=_HIST_COLORS[idx % len(_HIST_COLORS)], alpha=0.75, edgecolor="none", ) for val, label, ls in [(p50, "p50", "-"), (p95, "p95", "--"), (p99, "p99", ":")]: ax.axvline(val, color="black", linestyle=ls, linewidth=1.2, label=f"{label}={val:.3f} ms") ax.set_title(backend, fontweight="bold") ax.set_xlabel("Latency (ms)") ax.set_ylabel("Count") ax.legend(fontsize=8) ax.grid(True, linestyle="--", alpha=0.4) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) ax.text( 0.5, -0.24, ( f"read: p25 {p25:.3f} p50 {p50:.3f} " f"p95 {p95:.3f} p99 {p99:.3f} ms" ), transform=ax.transAxes, ha="center", va="top", fontsize=6.5, color="#555555", ) # Hide unused subplot cells for idx in range(len(backends), nrows * ncols): axes[idx // ncols][idx % ncols].set_visible(False) fig.tight_layout(rect=(0, 0.07, 1, 0.95), h_pad=3) out = _output_path("latency_histograms.png") fig.savefig(out, dpi=150, bbox_inches="tight") print(f"saved {out}") plt.close(fig) # ── main ────────────────────────────────────────────────────────────────────── def _render_mode(rows: list[Row], label: str, suffix: str) -> None: global MODE_LABEL, OUTPUT_SUFFIX MODE_LABEL = label OUTPUT_SUFFIX = suffix for access in ("rand", "seq"): plot_conc_throughput(rows, access) plot_conc_latency(rows, access) plot_chunk_throughput(rows, access) plot_chunk_latency(rows, access) plot_histograms(rows) def main(): missing = [ path for path in (CSV_GIL_PATH, CSV_NOGIL_PATH) if not path.exists() ] if missing: paths = "\n".join(str(path) for path in missing) raise SystemExit(f"Benchmark CSV not found:\n{paths}") PLOT_RESULTS_DIR.mkdir(parents=True, exist_ok=True) gil_rows = load_csv(CSV_GIL_PATH) nogil_rows = load_csv(CSV_NOGIL_PATH) print(f"loaded {len(gil_rows):,} GIL rows from {CSV_GIL_PATH}") print(f"loaded {len(nogil_rows):,} no-GIL rows from {CSV_NOGIL_PATH}") _render_mode(gil_rows, "GIL", "_gil") _render_mode(nogil_rows, "free-threaded / no GIL", "_nogil") names = [ f"{prefix}_{access}.png" for access in ("rand", "seq") for prefix in ( "concurrency_sweep_throughput", "concurrency_sweep_latency", "chunk_sweep_throughput", "chunk_sweep_latency", ) ] + ["latency_histograms.png"] for name in names: stem, suffix = os.path.splitext(name) gil_path = PLOT_RESULTS_DIR / f"{stem}_gil{suffix}" nogil_path = PLOT_RESULTS_DIR / f"{stem}_nogil{suffix}" output_path = PLOT_RESULTS_DIR / name _combine_mode_rows(gil_path, nogil_path, output_path) gil_path.unlink() nogil_path.unlink() print(f"combined {output_path}") print(f"\nAll plots saved to {PLOT_RESULTS_DIR.resolve()}") if __name__ == "__main__": main() caio-0.12.3/benchmark/pyproject.toml000066400000000000000000000003071523515553700173710ustar00rootroot00000000000000[project] name = "caio-benchmark" version = "0.1.0" requires-python = ">=3.10" dependencies = [ "caio", "matplotlib", "rich", ] [tool.uv.sources] caio = { path = "..", editable = true } caio-0.12.3/benchmark/results/000077500000000000000000000000001523515553700161565ustar00rootroot00000000000000caio-0.12.3/benchmark/results/bench_all.csv.gz000066400000000000000000000002031523515553700212140ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:7659a5411926479d07cae526e0746c2636b4a8a5b3989fce986f95fe402ccb78 size 886698 caio-0.12.3/benchmark/results/bench_all_nogil.csv.gz000066400000000000000000000002031523515553700224040ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:4d01e61d6f5479f545cc3c61aa28dba94fe0189a11964bbfdb89ff55c4b7442d size 858860 caio-0.12.3/benchmark/results/chunk_sweep_latency_rand.png000066400000000000000000000002031523515553700237150ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:b33ccbc3adaa2d0d7b97c060b22add3897e532e7ede2ec4467d752c91ba7f6ec size 448718 caio-0.12.3/benchmark/results/chunk_sweep_latency_seq.png000066400000000000000000000002031523515553700235610ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:060062635b0a698aa39230402f611325fdef3c86997a13915b15a32354bb105a size 443957 caio-0.12.3/benchmark/results/chunk_sweep_throughput_rand.png000066400000000000000000000002031523515553700244670ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:d047c271d3be2ab593f3c0915a30547d50278a1209b81e9f371f89697b5f7b3a size 238270 caio-0.12.3/benchmark/results/chunk_sweep_throughput_seq.png000066400000000000000000000002031523515553700243330ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:b80d399806a7e74f3356da70a026e333ccea49804a3ffa1a1ae931a946aa0e22 size 241990 caio-0.12.3/benchmark/results/concurrency_sweep_latency_rand.png000066400000000000000000000002031523515553700251370ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:25be813cf6daecf1e3bb4729dbb676ad6e677c4012e5f09174b406840c7f3a08 size 462932 caio-0.12.3/benchmark/results/concurrency_sweep_latency_seq.png000066400000000000000000000002031523515553700250030ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:54746e1a4eccb8ba870b1e4b532bbb97e2a35885cd3540be1a16892e3b5b5288 size 452030 caio-0.12.3/benchmark/results/concurrency_sweep_throughput_rand.png000066400000000000000000000002031523515553700257110ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:1d64ef620f11786da668608984885988640d56891c06e9a7f6714fd896d3c093 size 259261 caio-0.12.3/benchmark/results/concurrency_sweep_throughput_seq.png000066400000000000000000000002031523515553700255550ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:62236646f3bc2b31215c640f8a45d54d3a54ef47cb28ab248bc21c9be258b927 size 260910 caio-0.12.3/benchmark/results/latency_histograms.png000066400000000000000000000002031523515553700225560ustar00rootroot00000000000000version https://git-lfs.github.com/spec/v1 oid sha256:07fd2c31b690990a38503c72c02bba02aed6cce067f8732bed0e97225452774e size 227561 caio-0.12.3/benchmark/uv.lock000066400000000000000000005520721523515553700157740ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.11'", "python_full_version < '3.11'", ] [[package]] name = "caio" version = "0.10.2" source = { editable = "../" } [package.metadata] requires-dist = [ { name = "aiomisc-pytest", marker = "extra == 'develop'" }, { name = "coveralls", marker = "extra == 'develop'" }, { name = "pytest", marker = "extra == 'develop'" }, { name = "pytest-cov", marker = "extra == 'develop'" }, { name = "pytest-rerunfailures", marker = "extra == 'develop'" }, { name = "setuptools", marker = "extra == 'develop'" }, ] provides-extras = ["develop"] [package.metadata.requires-dev] dev = [{ name = "rich" }] [[package]] name = "caio-benchmark" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "caio" }, { name = "matplotlib" }, { name = "rich" }, ] [package.metadata] requires-dist = [ { name = "caio", editable = "../" }, { name = "matplotlib" }, { name = "rich" }, ] [[package]] name = "contourpy" version = "1.3.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, ] [[package]] name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", ] dependencies = [ { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] name = "cycler" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] [[package]] name = "fonttools" version = "4.61.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, ] [[package]] name = "kiwisolver" version = "1.4.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] [[package]] name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] 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 = "matplotlib" version = "3.10.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "cycler" }, { name = "fonttools" }, { name = "kiwisolver" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "pillow" }, { name = "pyparsing" }, { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, ] [[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 = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, ] [[package]] name = "numpy" version = "2.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", ] sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, ] [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pillow" version = "12.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] [[package]] name = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyparsing" version = "3.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[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 = "rich" version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[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" }, ] caio-0.12.3/caio/000077500000000000000000000000001523515553700134365ustar00rootroot00000000000000caio-0.12.3/caio/__init__.py000066400000000000000000000057011523515553700155520ustar00rootroot00000000000000import os import warnings from importlib.metadata import Distribution from . import python_aio, python_aio_asyncio from .abstract import AbstractContext, AbstractOperation __version__ = Distribution.from_name("caio").version __author__ = "Dmitry Orlov " try: from . import linux_uring, linux_uring_asyncio except ImportError: linux_uring = None # type: ignore linux_uring_asyncio = None # type: ignore try: from . import linux_aio, linux_aio_asyncio except ImportError: linux_aio = None # type: ignore linux_aio_asyncio = None # type: ignore try: from . import thread_aio, thread_aio_asyncio except ImportError: thread_aio = None # type: ignore thread_aio_asyncio = None # type: ignore variants = tuple(filter(None, [linux_uring, linux_aio, thread_aio, python_aio])) variants_asyncio = tuple( filter( None, [ linux_uring_asyncio, linux_aio_asyncio, thread_aio_asyncio, python_aio_asyncio, ], ), ) preferred = variants[0] preferred_asyncio = variants_asyncio[0] def __select_implementation(): global preferred global preferred_asyncio implementations = { "uring": (linux_uring, linux_uring_asyncio), "linux": (linux_aio, linux_aio_asyncio), "thread": (thread_aio, thread_aio_asyncio), "python": (python_aio, python_aio_asyncio), } implementations = {k: v for k, v in implementations.items() if all(v)} default_implementation = os.path.join( os.path.dirname(os.path.abspath(__file__)), "default_implementation", ) requested = os.getenv("CAIO_IMPL") if not requested and os.path.isfile(default_implementation): with open(default_implementation, "r") as fp: for line in fp: line = line.strip() if line.startswith("#") or not line: continue if line in implementations: requested = line break elif requested and requested not in implementations: warnings.warn( f"CAIO_IMPL contains unsupported value {requested!r}. " f"Use one of {tuple(implementations)!r}", RuntimeWarning, ) return preferred, preferred_asyncio = implementations.get( requested, (preferred, preferred_asyncio), ) __select_implementation() Context = preferred.Context # type: ignore Operation = preferred.Operation # type: ignore AsyncioContext = preferred_asyncio.AsyncioContext # type: ignore __all__ = ( "AbstractContext", "AbstractOperation", "AsyncioContext", "Context", "Operation", "__author__", "__version__", "linux_aio", "linux_aio_asyncio", "linux_uring", "linux_uring_asyncio", "python_aio", "python_aio_asyncio", "thread_aio", "thread_aio_asyncio", "variants", "variants_asyncio", ) caio-0.12.3/caio/abstract.py000066400000000000000000000042141523515553700156140ustar00rootroot00000000000000import abc from collections.abc import Callable from typing import Any class AbstractContext(abc.ABC): @property def max_requests(self) -> int: raise NotImplementedError def submit(self, *aio_operations) -> int: raise NotImplementedError(aio_operations) def cancel(self, *aio_operations) -> int: raise NotImplementedError(aio_operations) class AbstractOperation(abc.ABC): @classmethod @abc.abstractmethod def read( cls, nbytes: int, fd: int, offset: int, priority=0, ) -> "AbstractOperation": """ Creates a new instance of AIOOperation on read mode. """ raise NotImplementedError @classmethod @abc.abstractmethod def write( cls, payload_bytes: bytes, fd: int, offset: int, priority=0, ) -> "AbstractOperation": """ Creates a new instance of AIOOperation on write mode. """ raise NotImplementedError @classmethod @abc.abstractmethod def fsync(cls, fd: int, priority=0) -> "AbstractOperation": """ Creates a new instance of AIOOperation on fsync mode. """ raise NotImplementedError @classmethod @abc.abstractmethod def fdsync(cls, fd: int, priority=0) -> "AbstractOperation": """ Creates a new instance of AIOOperation on fdsync mode. """ raise NotImplementedError @abc.abstractmethod def get_value(self) -> bytes | int | None: """ Method returns a bytes value of AIOOperation's result or None. """ raise NotImplementedError @property @abc.abstractmethod def fileno(self) -> int: raise NotImplementedError @property @abc.abstractmethod def offset(self) -> int: raise NotImplementedError @property @abc.abstractmethod def payload(self) -> bytes | memoryview | None: raise NotImplementedError @property @abc.abstractmethod def nbytes(self) -> int: raise NotImplementedError @abc.abstractmethod def set_callback(self, callback: Callable[[int], Any]) -> bool: raise NotImplementedError caio-0.12.3/caio/asyncio_base.py000066400000000000000000000071201523515553700164470ustar00rootroot00000000000000import abc import asyncio import typing from functools import partial from . import abstract ContextType = type[abstract.AbstractContext] OperationType = type[abstract.AbstractOperation] class AsyncioContextBase(abc.ABC): MAX_REQUESTS_DEFAULT = 512 CONTEXT_CLASS: ContextType OPERATION_CLASS: OperationType def __init__(self, max_requests=None, loop=None, deferred=False, **kwargs): max_requests = max_requests or self.MAX_REQUESTS_DEFAULT self.loop = loop or asyncio.get_event_loop() # Opt-in: batch multiple submissions into one syscall instead of # flushing/submitting eagerly after each one. Only linux_aio's and # linux_uring's adapters actually act on this (see their own # _submit_op/_on_submitted overrides) - it's a no-op elsewhere. self.deferred = deferred self.semaphore = asyncio.BoundedSemaphore(max_requests) self.context = self._create_context(max_requests, **kwargs) def _create_context(self, max_requests, **kwargs): return self.CONTEXT_CLASS(max_requests=max_requests, **kwargs) def _destroy_context(self): return async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): self.close() def close(self): self._destroy_context() async def submit(self, op): if not isinstance(op, self.OPERATION_CLASS): raise ValueError("Operation object expected") # noqa: TRY004 (pre-existing public exception type, not changing it here) future = self.loop.create_future() op.set_callback(partial(self._on_done, future)) async with self.semaphore: self._submit_op(op, future) self._on_submitted() try: await future except asyncio.CancelledError: try: self.context.cancel(op) except ValueError: pass raise return op.get_value() def _submit_op(self, op, future): """Default: submit immediately and raise on rejection right away. Subclasses may defer submission (batching) - in that case they must resolve `future` with an exception instead if it's later rejected.""" if self.context.submit(op) != 1: raise OSError("Operation was not submitted") def _on_submitted(self): """Hook called after each op is placed in the context's queue. Subclasses can override to implement batched submission (deferred flush). """ def _on_done(self, future, result): """ In general case impossible predict current thread and the thread of event loop. So have to use `call_soon_threadsave` the result setter. """ if future.done(): return self.loop.call_soon_threadsafe( lambda: future.done() or future.set_result(True), ) def read( self, nbytes: int, fd: int, offset: int, priority: int = 0, ) -> typing.Awaitable[bytes]: return self.submit( self.OPERATION_CLASS.read(nbytes, fd, offset, priority), ) def write( self, payload: bytes, fd: int, offset: int, priority: int = 0, ) -> typing.Awaitable[int]: return self.submit( self.OPERATION_CLASS.write(payload, fd, offset, priority), ) def fsync(self, fd: int) -> typing.Awaitable: return self.submit(self.OPERATION_CLASS.fsync(fd)) def fdsync(self, fd: int) -> typing.Awaitable: return self.submit(self.OPERATION_CLASS.fdsync(fd)) caio-0.12.3/caio/linux_aio.c000066400000000000000000001074731523515553700156050ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #define PY_SSIZE_T_CLEAN #include #include #if PY_VERSION_HEX >= 0x030D0000 && defined(Py_GIL_DISABLED) #define CAIO_BEGIN_CRITICAL_SECTION(object) \ PyCriticalSection caio_critical_section; \ PyCriticalSection_Begin( \ &caio_critical_section, (PyObject *)(object) \ ) #define CAIO_END_CRITICAL_SECTION() \ PyCriticalSection_End(&caio_critical_section) #else #define CAIO_BEGIN_CRITICAL_SECTION(object) #define CAIO_END_CRITICAL_SECTION() #endif #define CAIO_ATOMIC_LOAD(value) \ __atomic_load_n(&(value), __ATOMIC_ACQUIRE) #define CAIO_ATOMIC_STORE(value, new_value) \ __atomic_store_n(&(value), (new_value), __ATOMIC_RELEASE) #define CAIO_ATOMIC_LOAD_STORE(value, new_value) \ __atomic_exchange_n(&(value), (new_value), __ATOMIC_ACQ_REL) #if PY_VERSION_HEX >= 0x030D0000 && defined(Py_GIL_DISABLED) #define CAIO_DECLARE_FREE_THREADED(module) \ PyUnstable_Module_SetGIL((module), Py_MOD_GIL_NOT_USED) #else #define CAIO_DECLARE_FREE_THREADED(module) 0 #endif #include static const unsigned CTX_MAX_REQUESTS_DEFAULT = 32; static const unsigned EV_MAX_REQUESTS_DEFAULT = 512; static int kernel_support = -1; inline static int io_setup(unsigned nr, aio_context_t *ctxp) { return syscall(__NR_io_setup, nr, ctxp); } inline static int io_destroy(aio_context_t ctx) { return syscall(__NR_io_destroy, ctx); } inline static int io_getevents( aio_context_t ctx, long min_nr, long max_nr, struct io_event *events, struct timespec *timeout ) { return syscall(__NR_io_getevents, ctx, min_nr, max_nr, events, timeout); } inline static int io_submit(aio_context_t ctx, long nr, struct iocb **iocbpp) { return syscall(__NR_io_submit, ctx, nr, iocbpp); } inline static long io_cancel(aio_context_t ctx, struct iocb *aiocb, struct io_event *res) { return syscall(__NR_io_cancel, ctx, aiocb, res); } inline static int io_cancel_error(int result) { if (result == 0) return result; switch (errno) { case EAGAIN: PyErr_SetString( PyExc_SystemError, "Specified operation was not canceled [EAGAIN]" ); break; case EFAULT: PyErr_SetString( PyExc_RuntimeError, "One of the data structures points to invalid data [EFAULT]" ); break; case EINVAL: PyErr_SetString( PyExc_ValueError, "The AIO context specified by ctx_id is invalid [EINVAL]" ); break; case ENOSYS: PyErr_SetString( PyExc_NotImplementedError, "io_cancel() is not implemented on this architecture [ENOSYS]" ); break; default: PyErr_SetFromErrno(PyExc_SystemError); break; } return result; } inline static int io_submit_error(int result) { if (result >= 0) return result; switch (errno) { case EAGAIN: PyErr_SetString( PyExc_OverflowError, "Insufficient resources are available to queue any iocbs [EAGAIN]" ); break; case EBADF: PyErr_SetString( PyExc_ValueError, "The file descriptor specified in the first iocb is invalid [EBADF]" ); break; case EFAULT: PyErr_SetString( PyExc_ValueError, "One of the data structures points to invalid data [EFAULT]" ); break; case EINVAL: PyErr_SetString( PyExc_ValueError, "The AIO context specified by ctx_id is invalid. nr is less " "than 0. The iocb at *iocbpp[0] is not properly initialized, " "the operation specified is invalid for the file descriptor in " "the iocb, or the value in the aio_reqprio field is invalid. " "[EINVAL]" ); break; default: PyErr_SetFromErrno(PyExc_SystemError); break; } return result; } typedef struct { PyObject_HEAD aio_context_t ctx; int32_t fileno; uint32_t max_requests; PyObject* weakreflist; } AIOContext; typedef struct { PyObject_HEAD AIOContext* context; PyObject* py_buffer; PyObject* callback; char* buffer; int error; uint8_t in_progress; uint8_t done; /* genuine completion reached - unlike in_progress * (sticky forever, guards resubmission), this is what * payload/get_value() gate on */ struct iocb iocb; PyObject* weakreflist; } AIOOperation; static PyTypeObject* AIOOperationTypeP = NULL; static PyTypeObject* AIOContextTypeP = NULL; static PyObject *AIOOperation_callback_ref(AIOOperation *self) { PyObject *callback; CAIO_BEGIN_CRITICAL_SECTION(self); callback = Py_XNewRef(self->callback); CAIO_END_CRITICAL_SECTION(); return callback; } static void AIOContext_dealloc(AIOContext *self) { if (self->weakreflist != NULL) PyObject_ClearWeakRefs((PyObject *) self); if (self->ctx != 0) { aio_context_t ctx = self->ctx; self->ctx = 0; io_destroy(ctx); } if (self->fileno >= 0) { close(self->fileno); self->fileno = -1; } Py_TYPE(self)->tp_free((PyObject *) self); } /* AIOContext.__new__ classmethod definition */ static PyObject * AIOContext_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { AIOContext *self; self = (AIOContext *) type->tp_alloc(type, 0); return (PyObject *) self; } static int AIOContext_init(AIOContext *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = {"max_requests", NULL}; self->ctx = 0; self->max_requests = 0; /* EFD_NONBLOCK so poll()'s read() raises BlockingIOError when nothing * is pending yet, as documented, instead of blocking the caller. */ self->fileno = eventfd(0, EFD_NONBLOCK); if (self->fileno < 0) { PyErr_SetFromErrno(PyExc_SystemError); return -1; } if (!PyArg_ParseTupleAndKeywords(args, kwds, "|I", kwlist, &self->max_requests)) { return -1; } if (self->max_requests <= 0) { self->max_requests = CTX_MAX_REQUESTS_DEFAULT; } if (io_setup(self->max_requests, &self->ctx) < 0) { PyErr_SetFromErrno(PyExc_SystemError); return -1; } return 0; } static PyObject* AIOContext_repr(AIOContext *self) { if (self->ctx == 0) { PyErr_SetNone(PyExc_RuntimeError); return NULL; } return PyUnicode_FromFormat( "<%s as %p: max_requests=%i, ctx=%lli>", Py_TYPE(self)->tp_name, self, self->max_requests, self->ctx ); } PyDoc_STRVAR(AIOContext_submit_docstring, "Accepts multiple Operations. Returns \n\n" " Operation.submit(aio_op1, aio_op2, aio_opN, ...) -> int" ); static PyObject* AIOContext_submit(AIOContext *self, PyObject *args) { if (self == 0) { PyErr_SetString(PyExc_RuntimeError, "self is NULL"); return NULL; } if (self->ctx == 0) { PyErr_SetString(PyExc_RuntimeError, "self->ctx is NULL"); return NULL; } if (!PyTuple_Check(args)) { PyErr_SetNone(PyExc_ValueError); return NULL; } Py_ssize_t nr = PyTuple_GET_SIZE(args); if (nr == 0) { return PyLong_FromSsize_t(0); } /* Validate every argument's type up front, before claiming anything - * a later-in-the-batch type error must not leave earlier operations * half-claimed. */ for (Py_ssize_t i = 0; i < nr; i++) { PyObject *obj = PyTuple_GET_ITEM(args, i); if (PyObject_TypeCheck(obj, AIOOperationTypeP) == 0) { PyErr_Format( PyExc_TypeError, "Wrong type for argument %zd -> %R", i, obj ); return NULL; } } struct iocb **iocbpp = PyMem_New(struct iocb *, nr); AIOOperation **claimed = PyMem_New(AIOOperation *, nr); if (iocbpp == NULL || claimed == NULL) { PyMem_Free(iocbpp); PyMem_Free(claimed); PyErr_NoMemory(); return NULL; } /* Atomic exchange, not check-then-set: two Contexts racing on the same Operation * must not both hand its iocb to the kernel. Context reset via * Py_XDECREF, not overwrite - a previously-completed submission never * clears it. */ Py_ssize_t to_submit = 0; for (Py_ssize_t i = 0; i < nr; i++) { AIOOperation *op = (AIOOperation *) PyTuple_GET_ITEM(args, i); if (CAIO_ATOMIC_LOAD_STORE(op->in_progress, 1)) continue; Py_XDECREF(op->context); op->context = self; Py_INCREF(self); Py_INCREF(op); op->iocb.aio_flags |= IOCB_FLAG_RESFD; op->iocb.aio_resfd = self->fileno; claimed[to_submit] = op; iocbpp[to_submit] = &op->iocb; to_submit++; } if (to_submit == 0) { PyMem_Free(iocbpp); PyMem_Free(claimed); return PyLong_FromSsize_t(0); } int result = io_submit(self->ctx, to_submit, iocbpp); if (io_submit_error(result) < 0) { /* Nothing reached the kernel - roll back every claim above so * each op is exactly as retryable as it was before this call. */ for (Py_ssize_t i = 0; i < to_submit; i++) { AIOOperation *op = claimed[i]; CAIO_ATOMIC_STORE(op->in_progress, 0); Py_CLEAR(op->context); Py_DECREF(op); } PyMem_Free(iocbpp); PyMem_Free(claimed); return NULL; } /* io_submit() processes iocbs strictly in order and stops at the first * one it can't accept - `result` is exactly how many of `claimed` * actually reached the kernel. Anything beyond that never got queued * and will never produce a completion event, so it must be rolled * back the same way instead of leaking its claim forever. */ for (Py_ssize_t i = result; i < to_submit; i++) { AIOOperation *op = claimed[i]; CAIO_ATOMIC_STORE(op->in_progress, 0); Py_CLEAR(op->context); Py_DECREF(op); } PyMem_Free(iocbpp); PyMem_Free(claimed); return (PyObject*) PyLong_FromSsize_t(result); } PyDoc_STRVAR(AIOContext_cancel_docstring, "Cancels multiple Operations. Returns \n\n" " Operation.cancel(aio_op1, aio_op2, aio_opN, ...) -> int" ); static PyObject* AIOContext_cancel(AIOContext *self, PyObject *args, PyObject *kwds) { if (self == 0) { PyErr_SetString(PyExc_RuntimeError, "self is NULL"); return NULL; } if (self->ctx == 0) { PyErr_SetString(PyExc_RuntimeError, "self->ctx is NULL"); return NULL; } static char *kwlist[] = {"operation", NULL}; AIOOperation* op = NULL; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &op)) return NULL; if (PyObject_TypeCheck(op, AIOOperationTypeP) == 0) { PyErr_Format(PyExc_TypeError, "Operation required not %r", op); return NULL; } struct io_event ev; if (io_cancel_error(io_cancel(self->ctx, &op->iocb, &ev))) { return NULL; } if (ev.res >= 0) { op->iocb.aio_nbytes = ev.res; } else { op->error = -ev.res; } /* io_cancel() succeeding delivers this event synchronously, right * here - it will never also show up via process_events()'s own * io_getevents() loop, so this op's submit()-time context reference * must be released here, the only place that ever will for a * successfully cancelled operation. in_progress itself is * deliberately NOT reset - one-shot forever, matching thread_aio and * the (paused) Rust rewrite; a cancelled Operation is still terminal, * not retryable, only a fresh one constructed for a retry. */ Py_CLEAR(op->context); CAIO_ATOMIC_STORE(op->done, 1); PyObject *callback = AIOOperation_callback_ref(op); if (callback != NULL) { PyObject *rv = PyObject_CallFunction(callback, "K", ev.res); if (rv == NULL) { Py_DECREF(callback); Py_DECREF(op); return NULL; } Py_DECREF(rv); Py_DECREF(callback); } Py_DECREF(op); return (PyObject*) PyLong_FromSsize_t(ev.res); } PyDoc_STRVAR(AIOContext_process_events_docstring, "Gather events for Context.\n\n" " Context.process_events(max_requests=512, min_requests=0, timeout=0) -> int\n\n" " timeout=0 checks once and returns immediately. timeout>0 blocks up\n" " to that many seconds; timeout<0 blocks indefinitely. Both are for\n" " manual synchronous polling from a plain thread - do not also\n" " select()/poll() on `.fileno` from elsewhere while relying on this\n" " to wait, the two waiting mechanisms are alternatives, not meant to\n" " be combined on the same Context." ); static PyObject* AIOContext_process_events( AIOContext *self, PyObject *args, PyObject *kwds ) { if (self->ctx == 0) { PyErr_SetNone(PyExc_RuntimeError); return NULL; } uint32_t min_requests = 0; uint32_t max_requests = 0; int32_t tv_sec = 0; struct timespec timeout = {0, 0}; static char *kwlist[] = {"max_requests", "min_requests", "timeout", NULL}; if (!PyArg_ParseTupleAndKeywords( args, kwds, "|IIi", kwlist, &max_requests, &min_requests, &tv_sec )) { return NULL; } /* timeout<0 means "wait indefinitely" - io_getevents() itself takes a * NULL timeout to mean exactly that, so no emulation is needed here * (unlike linux_uring, whose io_uring_enter() has no native timeout * parameter at all). timeout=0 is a non-blocking check; timeout>0 * bounds the wait. */ struct timespec *timeout_arg = NULL; if (tv_sec >= 0) { timeout.tv_sec = tv_sec; timeout_arg = &timeout; } if (max_requests == 0) { max_requests = EV_MAX_REQUESTS_DEFAULT; } /* max_requests directly sizes the events buffer below - an unbounded * caller-controlled value (e.g. 2**32-1) must raise cleanly rather * than attempt a many-GiB allocation or, when it was still a stack * VLA, overflow the stack outright. */ static const uint32_t MAX_EVENTS_REQUEST = 1u << 20; if (max_requests > MAX_EVENTS_REQUEST) { PyErr_Format( PyExc_OverflowError, "max_requests (%u) exceeds the maximum allowed (%u)", max_requests, MAX_EVENTS_REQUEST ); return NULL; } if (min_requests > max_requests) { PyErr_Format( PyExc_ValueError, "min_requests \"%d\" must be lower then max_requests \"%d\"", min_requests, max_requests ); return NULL; } struct io_event *events = PyMem_New(struct io_event, max_requests); if (events == NULL) { PyErr_NoMemory(); return NULL; } /* io_getevents() can block for up to `timeout` - release the GIL so * this doesn't freeze every other thread in the interpreter for the * duration, defeating the entire point of using this asynchronously. */ int result; Py_BEGIN_ALLOW_THREADS result = io_getevents( self->ctx, min_requests, max_requests, events, timeout_arg ); Py_END_ALLOW_THREADS if (result < 0) { PyMem_Free(events); PyErr_SetFromErrno(PyExc_SystemError); return NULL; } AIOOperation* op; struct io_event* ev; /* Every completion here was already dequeued from the kernel by * io_getevents() above - it will never be seen again by a later call, * so each one's submit()-time claim (context, the op reference itself) * must be released exactly once right here, regardless of whether a * callback is set, and regardless of whether that callback raises. * The original version skipped the release entirely when callback was * NULL (a plain leak) and aborted the whole loop on the first raising * callback, silently dropping every completion after it (already * consumed from the kernel, but never delivered) - fixed by isolating * each callback's own exception instead of letting it escape. * in_progress itself is deliberately NOT reset here - one-shot * forever once genuinely submitted, matching thread_aio and the * (paused) Rust rewrite: a completed Operation must not be * resubmittable, only a fresh one constructed for a retry. */ int32_t i; for (i = 0; i < result; i++) { ev = &events[i]; op = (AIOOperation*)(uintptr_t) ev->data; if (ev->res >= 0) { op->iocb.aio_nbytes = ev->res; } else { op->error = -ev->res; } Py_CLEAR(op->context); CAIO_ATOMIC_STORE(op->done, 1); PyObject *callback = AIOOperation_callback_ref(op); if (callback != NULL) { PyObject *rv = PyObject_CallFunction(callback, "K", ev->res); if (rv == NULL) { PyErr_WriteUnraisable(callback); } else { Py_DECREF(rv); } Py_DECREF(callback); } Py_DECREF(op); } PyMem_Free(events); return (PyObject*) PyLong_FromSsize_t(i); } PyDoc_STRVAR(AIOContext_poll_docstring, "Read value from context file descriptor.\n\n" " Context().poll() -> int" ); static PyObject* AIOContext_poll( AIOContext *self, PyObject *args ) { if (self->ctx == 0) { PyErr_SetNone(PyExc_RuntimeError); return NULL; } if (self->fileno < 0) { PyErr_SetNone(PyExc_RuntimeError); return NULL; } uint64_t result = 0; int size = read(self->fileno, &result, sizeof(uint64_t)); if (size != sizeof(uint64_t)) { PyErr_SetNone(PyExc_BlockingIOError); return NULL; } return (PyObject*) PyLong_FromUnsignedLongLong(result); } /* AIOContext properties */ static PyMemberDef AIOContext_members[] = { { "fileno", T_INT, offsetof(AIOContext, fileno), READONLY, "fileno" }, { "max_requests", T_USHORT, offsetof(AIOContext, max_requests), READONLY, "max requests" }, {NULL} /* Sentinel */ }; static PyMethodDef AIOContext_methods[] = { { "submit", (PyCFunction) AIOContext_submit, METH_VARARGS, AIOContext_submit_docstring }, { "cancel", (PyCFunction) AIOContext_cancel, METH_VARARGS | METH_KEYWORDS, AIOContext_cancel_docstring }, { "process_events", (PyCFunction) AIOContext_process_events, METH_VARARGS | METH_KEYWORDS, AIOContext_process_events_docstring }, { "poll", (PyCFunction) AIOContext_poll, METH_NOARGS, AIOContext_poll_docstring }, {NULL} /* Sentinel */ }; static PyTypeObject AIOContextType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "Context", .tp_doc = "linux aio context representation", .tp_basicsize = sizeof(AIOContext), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_new = AIOContext_new, .tp_init = (initproc) AIOContext_init, .tp_dealloc = (destructor) AIOContext_dealloc, .tp_members = AIOContext_members, .tp_methods = AIOContext_methods, .tp_repr = (reprfunc) AIOContext_repr, .tp_weaklistoffset = offsetof(AIOContext, weakreflist) }; static int AIOOperation_traverse(AIOOperation *self, visitproc visit, void *arg) { Py_VISIT(self->context); Py_VISIT(self->callback); Py_VISIT(self->py_buffer); return 0; } static int AIOOperation_clear(AIOOperation *self) { Py_CLEAR(self->context); Py_CLEAR(self->callback); /* self->buffer is a separate PyMem_Calloc allocation py_buffer's own * memoryview only views, not owns - clearing py_buffer alone would * leak it (and, if this runs before this Operation's own eventual * dealloc, that dealloc's identical free-then-NULL below prevents a * double free only because this already set it to NULL). */ if (self->iocb.aio_lio_opcode == IOCB_CMD_PREAD && self->buffer != NULL) { PyMem_Free(self->buffer); self->buffer = NULL; } Py_CLEAR(self->py_buffer); return 0; } static void AIOOperation_dealloc(AIOOperation *self) { PyObject_GC_UnTrack(self); if (self->weakreflist != NULL) PyObject_ClearWeakRefs((PyObject *) self); AIOOperation_clear(self); Py_TYPE(self)->tp_free((PyObject *) self); } static PyObject* AIOOperation_repr(AIOOperation *self) { char* mode; switch (self->iocb.aio_lio_opcode) { case IOCB_CMD_PREAD: mode = "read"; break; case IOCB_CMD_PWRITE: mode = "write"; break; case IOCB_CMD_FSYNC: mode = "fsync"; break; case IOCB_CMD_FDSYNC: mode = "fdsync"; break; default: mode = "noop"; break; } return PyUnicode_FromFormat( "<%s at %p: mode=\"%s\", fd=%i, offset=%i, buffer=%p>", Py_TYPE(self)->tp_name, self, mode, self->iocb.aio_fildes, self->iocb.aio_offset, self->iocb.aio_buf ); } /* AIOOperation.read classmethod definition */ PyDoc_STRVAR(AIOOperation_read_docstring, "Creates a new instance of Operation on read mode.\n\n" " Operation.read(\n" " nbytes: int,\n" " aio_context: Context,\n" " fd: int, \n" " offset: int,\n" " priority=0\n" " )" ); static PyObject* AIOOperation_read( PyTypeObject *type, PyObject *args, PyObject *kwds ) { AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); static char *kwlist[] = {"nbytes", "fd", "offset", "priority", NULL}; if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "can not allocate memory"); return NULL; } memset(&self->iocb, 0, sizeof(struct iocb)); self->iocb.aio_data = (uint64_t)(uintptr_t) self; self->context = NULL; self->buffer = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->weakreflist = NULL; uint64_t nbytes = 0; int argIsOk = PyArg_ParseTupleAndKeywords( args, kwds, "KI|Lh", kwlist, &nbytes, &(self->iocb.aio_fildes), &(self->iocb.aio_offset), &(self->iocb.aio_reqprio) ); if (!argIsOk) { Py_DECREF(self); return NULL; } /* PyMem_Calloc can return NULL for a large enough (or just OOM-at-the- * time) nbytes - proceeding with a NULL buf would hand the kernel (via * aio_buf) and PyMemoryView_FromMemory a NULL pointer with a nonzero * declared size, corrupting memory instead of raising a catchable * error. */ self->buffer = PyMem_Calloc(nbytes, sizeof(char)); if (self->buffer == NULL && nbytes > 0) { Py_DECREF(self); PyErr_NoMemory(); return NULL; } self->iocb.aio_buf = (uint64_t)(uintptr_t) self->buffer; self->iocb.aio_nbytes = nbytes; self->py_buffer = PyMemoryView_FromMemory(self->buffer, nbytes, PyBUF_READ); if (self->py_buffer == NULL) { Py_DECREF(self); return NULL; } self->iocb.aio_lio_opcode = IOCB_CMD_PREAD; return (PyObject*) self; } /* AIOOperation.write classmethod definition */ PyDoc_STRVAR(AIOOperation_write_docstring, "Creates a new instance of Operation on write mode.\n\n" " Operation.write(\n" " payload_bytes: bytes,\n" " fd: int, \n" " offset: int,\n" " priority=0\n" " )" ); static PyObject* AIOOperation_write( PyTypeObject *type, PyObject *args, PyObject *kwds ) { AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); static char *kwlist[] = {"payload_bytes", "fd", "offset", "priority", NULL}; if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "can not allocate memory"); return NULL; } memset(&self->iocb, 0, sizeof(struct iocb)); self->iocb.aio_data = (uint64_t)(uintptr_t) self; self->context = NULL; self->buffer = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->weakreflist = NULL; Py_ssize_t nbytes = 0; /* Parsed into a plain local first, not directly into self->py_buffer: * "O" hands back a borrowed reference, and self->py_buffer must never * hold one - AIOOperation_dealloc unconditionally Py_CLEARs it. Only * assigned (and incref'd) below once confirmed to actually be the * bytes object this Operation is going to own. */ PyObject *payload_bytes = NULL; int argIsOk = PyArg_ParseTupleAndKeywords( args, kwds, "OI|Lh", kwlist, &payload_bytes, &(self->iocb.aio_fildes), &(self->iocb.aio_offset), &(self->iocb.aio_reqprio) ); if (!argIsOk) { Py_DECREF(self); return NULL; } if (!PyBytes_Check(payload_bytes)) { Py_DECREF(self); PyErr_SetString( PyExc_ValueError, "payload_bytes argument must be bytes" ); return NULL; } self->iocb.aio_lio_opcode = IOCB_CMD_PWRITE; if (PyBytes_AsStringAndSize( payload_bytes, &self->buffer, &nbytes )) { Py_DECREF(self); PyErr_SetString( PyExc_RuntimeError, "Can not convert bytes to c string" ); return NULL; } self->py_buffer = payload_bytes; Py_INCREF(self->py_buffer); self->iocb.aio_nbytes = nbytes; self->iocb.aio_buf = (uint64_t)(uintptr_t) self->buffer; return (PyObject*) self; } /* AIOOperation.fsync classmethod definition */ PyDoc_STRVAR(AIOOperation_fsync_docstring, "Creates a new instance of Operation on fsync mode.\n\n" " Operation.fsync(\n" " aio_context: AIOContext,\n" " fd: int, \n" " priority=0\n" " )" ); static PyObject* AIOOperation_fsync( PyTypeObject *type, PyObject *args, PyObject *kwds ) { AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); static char *kwlist[] = {"fd", "priority", NULL}; if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "can not allocate memory"); return NULL; } memset(&self->iocb, 0, sizeof(struct iocb)); self->iocb.aio_data = (uint64_t)(uintptr_t) self; self->context = NULL; self->buffer = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->weakreflist = NULL; int argIsOk = PyArg_ParseTupleAndKeywords( args, kwds, "I|h", kwlist, &(self->iocb.aio_fildes), &(self->iocb.aio_reqprio) ); if (!argIsOk) { Py_DECREF(self); return NULL; } self->iocb.aio_lio_opcode = IOCB_CMD_FSYNC; return (PyObject*) self; } /* AIOOperation.fdsync classmethod definition */ PyDoc_STRVAR(AIOOperation_fdsync_docstring, "Creates a new instance of Operation on fdsync mode.\n\n" " Operation.fdsync(\n" " aio_context: AIOContext,\n" " fd: int, \n" " priority=0\n" " )" ); static PyObject* AIOOperation_fdsync( PyTypeObject *type, PyObject *args, PyObject *kwds ) { AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); static char *kwlist[] = {"fd", "priority", NULL}; if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "can not allocate memory"); return NULL; } memset(&self->iocb, 0, sizeof(struct iocb)); self->iocb.aio_data = (uint64_t)(uintptr_t) self; self->buffer = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->weakreflist = NULL; int argIsOk = PyArg_ParseTupleAndKeywords( args, kwds, "I|h", kwlist, &(self->iocb.aio_fildes), &(self->iocb.aio_reqprio) ); if (!argIsOk) { Py_DECREF(self); return NULL; } self->iocb.aio_lio_opcode = IOCB_CMD_FDSYNC; return (PyObject*) self; } /* AIOOperation.get_value method definition */ PyDoc_STRVAR(AIOOperation_get_value_docstring, "Method returns a bytes value of Operation's result or None.\n\n" " Operation.get_value() -> Optional[bytes]" ); static PyObject* AIOOperation_get_value( AIOOperation *self, PyObject *args, PyObject *kwds ) { if ( CAIO_ATOMIC_LOAD(self->in_progress) && !CAIO_ATOMIC_LOAD(self->done) ) { PyErr_SetString( PyExc_RuntimeError, "get_value() is not available while the operation is in flight" ); return NULL; } if (self->error != 0) { PyErr_SetString( PyExc_SystemError, strerror(self->error) ); return NULL; } switch (self->iocb.aio_lio_opcode) { case IOCB_CMD_PREAD: /* self->buffer can only be NULL here if tp_clear() already * ran - only possible once this Operation is otherwise * unreachable (see AIOOperation_traverse/_clear), so nothing * could actually be waiting on this return value, but degrade * to None rather than hand out an uninitialized buffer either * way (PyBytes_FromStringAndSize(NULL, n>0) doesn't crash, * it silently returns garbage). */ if (self->buffer == NULL) Py_RETURN_NONE; return PyBytes_FromStringAndSize( self->buffer, self->iocb.aio_nbytes ); case IOCB_CMD_PWRITE: return PyLong_FromSsize_t(self->iocb.aio_nbytes); } Py_RETURN_NONE; } /* AIOOperation.set_callback method definition */ PyDoc_STRVAR(AIOOperation_set_callback_docstring, "Set callback which will be called after Operation will be finished.\n\n" " Operation.get_value() -> Optional[bytes]" ); static PyObject* AIOOperation_set_callback( AIOOperation *self, PyObject *args, PyObject *kwds ) { static char *kwlist[] = {"callback", NULL}; PyObject* callback; PyObject* old_callback; int argIsOk = PyArg_ParseTupleAndKeywords( args, kwds, "O", kwlist, &callback ); if (!argIsOk) return NULL; if (!PyCallable_Check(callback)) { PyErr_Format( PyExc_ValueError, "object %r is not callable", callback ); return NULL; } Py_INCREF(callback); CAIO_BEGIN_CRITICAL_SECTION(self); old_callback = self->callback; self->callback = callback; CAIO_END_CRITICAL_SECTION(); Py_XDECREF(old_callback); Py_RETURN_TRUE; } static PyObject *AIOOperation_payload_getter(AIOOperation *self, void *closure) { if ( CAIO_ATOMIC_LOAD(self->in_progress) && !CAIO_ATOMIC_LOAD(self->done) ) { PyErr_SetString( PyExc_RuntimeError, "payload is not available while the operation is in flight" ); return NULL; } /* fsync/fdsync Operations never allocate a buffer - matches T_OBJECT's * (as opposed to T_OBJECT_EX's) own NULL-to-None behavior, which this * getter replaces. */ if (self->py_buffer == NULL) Py_RETURN_NONE; Py_INCREF(self->py_buffer); return self->py_buffer; } static PyGetSetDef AIOOperation_getset[] = { { "payload", (getter) AIOOperation_payload_getter, NULL, "payload", NULL }, {NULL} }; /* AIOOperation properties */ static PyMemberDef AIOOperation_members[] = { { "context", T_OBJECT, offsetof(AIOOperation, context), READONLY, "context object" }, { "fileno", T_UINT, offsetof(AIOOperation, iocb.aio_fildes), READONLY, "file descriptor" }, { "priority", T_USHORT, offsetof(AIOOperation, iocb.aio_reqprio), READONLY, "request priority" }, { "offset", T_ULONGLONG, offsetof(AIOOperation, iocb.aio_offset), READONLY, "offset" }, { "nbytes", T_ULONGLONG, offsetof(AIOOperation, iocb.aio_nbytes), READONLY, "nbytes" }, {NULL} /* Sentinel */ }; /* AIOOperation methods */ static PyMethodDef AIOOperation_methods[] = { { "read", (PyCFunction) AIOOperation_read, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_read_docstring }, { "write", (PyCFunction) AIOOperation_write, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_write_docstring }, { "fsync", (PyCFunction) AIOOperation_fsync, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_fsync_docstring }, { "fdsync", (PyCFunction) AIOOperation_fdsync, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_fdsync_docstring }, { "get_value", (PyCFunction) AIOOperation_get_value, METH_NOARGS, AIOOperation_get_value_docstring }, { "set_callback", (PyCFunction) AIOOperation_set_callback, METH_VARARGS | METH_KEYWORDS, AIOOperation_set_callback_docstring }, {NULL} /* Sentinel */ }; /* AIOOperation class */ static PyTypeObject AIOOperationType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "aio.AIOOperation", .tp_doc = "linux aio operation representation", .tp_basicsize = sizeof(AIOOperation), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, .tp_dealloc = (destructor) AIOOperation_dealloc, .tp_traverse = (traverseproc) AIOOperation_traverse, .tp_clear = (inquiry) AIOOperation_clear, .tp_members = AIOOperation_members, .tp_getset = AIOOperation_getset, .tp_methods = AIOOperation_methods, .tp_repr = (reprfunc) AIOOperation_repr, .tp_weaklistoffset = offsetof(AIOOperation, weakreflist) }; static PyModuleDef linux_aio_module = { PyModuleDef_HEAD_INIT, .m_name = "linux_aio", .m_doc = "Linux AIO c API bindings.", .m_size = -1, }; PyMODINIT_FUNC PyInit_linux_aio(void) { Py_Initialize(); struct utsname uname_data; if (uname(&uname_data)) { PyErr_SetString(PyExc_ImportError, "Can not detect linux kernel version"); return NULL; } int release[2] = {0}; sscanf(uname_data.release, "%d.%d", &release[0], &release[1]); kernel_support = (release[0] > 4) || (release[0] == 4 && release[1] >= 18); if (!kernel_support) { PyErr_Format( PyExc_ImportError, "Linux kernel supported since 4.18 but current kernel is %s.", uname_data.release ); return NULL; } aio_context_t temp_ctx = 0; if (io_setup(1, &temp_ctx) < 0) { PyErr_Format( PyExc_ImportError, "Error on io_setup with code %d", errno ); return NULL; } if (io_destroy(temp_ctx)) { PyErr_Format( PyExc_ImportError, "Error on io_destroy with code %d", errno ); return NULL; } AIOContextTypeP = &AIOContextType; AIOOperationTypeP = &AIOOperationType; PyObject *m; m = PyModule_Create(&linux_aio_module); if (m == NULL) return NULL; if (CAIO_DECLARE_FREE_THREADED(m) < 0) { Py_DECREF(m); return NULL; } if (PyType_Ready(AIOContextTypeP) < 0) return NULL; Py_INCREF(AIOContextTypeP); if (PyModule_AddObject(m, "Context", (PyObject *) AIOContextTypeP) < 0) { Py_XDECREF(AIOContextTypeP); Py_XDECREF(m); return NULL; } if (PyType_Ready(AIOOperationTypeP) < 0) return NULL; Py_INCREF(AIOOperationTypeP); if (PyModule_AddObject(m, "Operation", (PyObject *) AIOOperationTypeP) < 0) { Py_XDECREF(AIOOperationTypeP); Py_XDECREF(m); return NULL; } return m; } caio-0.12.3/caio/linux_aio.pyi000066400000000000000000000026301523515553700161510ustar00rootroot00000000000000from collections.abc import Callable from typing import Any from .abstract import AbstractContext, AbstractOperation # noinspection PyPropertyDefinition class Context(AbstractContext): def __init__(self, max_requests: int = 32): ... @property def fileno(self) -> int: ... def poll(self) -> int: ... def process_events( self, max_requests: int = 512, min_requests: int = 0, timeout: int = 0, ) -> int: ... # noinspection PyPropertyDefinition class Operation(AbstractOperation): @classmethod def read( cls, nbytes: int, fd: int, offset: int, priority=0 ) -> AbstractOperation: ... @classmethod def write( cls, payload_bytes: bytes, fd: int, offset: int, priority=0, ) -> AbstractOperation: ... @classmethod def fsync(cls, fd: int, priority=0) -> AbstractOperation: ... @classmethod def fdsync(cls, fd: int, priority=0) -> AbstractOperation: ... def get_value(self) -> bytes | int | None: ... @property def fileno(self) -> int: ... @property def offset(self) -> int: ... @property def payload(self) -> bytes | memoryview | None: ... @property def nbytes(self) -> int: ... @property def context(self) -> AbstractContext | None: ... @property def priority(self) -> int: ... def set_callback(self, callback: Callable[[int], Any]) -> bool: ... caio-0.12.3/caio/linux_aio_asyncio.py000066400000000000000000000075271523515553700175370ustar00rootroot00000000000000from .asyncio_base import AsyncioContextBase from .linux_aio import Context, Operation class AsyncioContext(AsyncioContextBase): OPERATION_CLASS = Operation CONTEXT_CLASS = Context def _create_context(self, max_requests): context = super()._create_context(max_requests) self.loop.add_reader(context.fileno, self._on_read_event) self._pending = [] self._submit_scheduled = False return context def _submit_op(self, op, future): if not self.deferred: super()._submit_op(op, future) return # Context.submit() already accepts *ops and builds one iocbpp array # for the whole batch - call_soon() defers to the next _run_once() # pass, after every currently-ready submit() has queued its op, so # one submit() call handles all of them together. self._pending.append((op, future)) if not self._submit_scheduled: self._submit_scheduled = True self.loop.call_soon(self._deferred_submit) def _deferred_submit(self): self._submit_scheduled = False pending, self._pending = self._pending, [] # Already cancelled while waiting here - never reached the kernel, # nothing to submit or cancel for these. pending = [(op, fut) for op, fut in pending if not fut.done()] if not pending: return ops = [op for op, _ in pending] try: accepted = self.context.submit(*ops) except BaseException as exc: # noqa: BLE001 (must isolate any submit()-time exception, including SystemExit/KeyboardInterrupt, since this runs as a bare call_soon callback) # submit() itself can raise synchronously (e.g. a bad fd in the # batch) - this runs as a bare call_soon callback, not inside # any of the pending coroutines, so letting it escape here # would just get logged by asyncio's default handler and leave # every future in the batch unresolved forever. Every op in # this batch failed together (io_submit() rejects the whole # call on this kind of error, not just a prefix). for op, fut in pending: if not fut.done(): fut.set_exception(exc) return # io_submit() accepts a prefix; anything past `accepted` failed. for op, fut in pending[accepted:]: if not fut.done(): fut.set_exception(OSError("Operation was not submitted")) def _on_done(self, future, result): """ Allow to set result directly. Cause process_events running in the same thread """ if future.done(): return future.set_result(True) def _destroy_context(self): self.loop.remove_reader(self.context.fileno) def _on_read_event(self): # poll() raises BlockingIOError whenever the eventfd's counter # reads as zero - possible even right after epoll reported this fd # readable (e.g. the kernel coalesces multiple completions into one # counter increment, and the ones beyond the first can still be # landing at the moment poll() runs). Letting that exception escape # used to skip process_events() entirely for this wakeup - draining # doesn't depend on the eventfd count, so a stale/absent counter is # never a reason to skip it, only silently stranding whatever was # already completed with no other event left to wake this Context # up again. Confirmed to actually hang a real (non-tmpfs) benchmark # run this way for linux_uring's identical pattern - inline tmpfs # completions never exercise this path at all. try: self.context.poll() except BlockingIOError: pass while self.context.process_events(): pass caio-0.12.3/caio/linux_uring.c000066400000000000000000001453301523515553700161530ustar00rootroot00000000000000/* * io_uring AIO backend for caio * Requires Linux 5.6+ (IORING_OP_READ / IORING_OP_WRITE without iovec) */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define PY_SSIZE_T_CLEAN #include #include #if PY_VERSION_HEX >= 0x030D0000 && defined(Py_GIL_DISABLED) #define CAIO_BEGIN_CRITICAL_SECTION(object) \ PyCriticalSection caio_critical_section; \ PyCriticalSection_Begin( \ &caio_critical_section, (PyObject *)(object) \ ) #define CAIO_END_CRITICAL_SECTION() \ PyCriticalSection_End(&caio_critical_section) #else #define CAIO_BEGIN_CRITICAL_SECTION(object) #define CAIO_END_CRITICAL_SECTION() #endif #define CAIO_ATOMIC_LOAD(value) \ __atomic_load_n(&(value), __ATOMIC_ACQUIRE) #define CAIO_ATOMIC_STORE(value, new_value) \ __atomic_store_n(&(value), (new_value), __ATOMIC_RELEASE) #define CAIO_ATOMIC_LOAD_STORE(value, new_value) \ __atomic_exchange_n(&(value), (new_value), __ATOMIC_ACQ_REL) #if PY_VERSION_HEX >= 0x030D0000 && defined(Py_GIL_DISABLED) #define CAIO_DECLARE_FREE_THREADED(module) \ PyUnstable_Module_SetGIL((module), Py_MOD_GIL_NOT_USED) #else #define CAIO_DECLARE_FREE_THREADED(module) 0 #endif /* ---- syscall wrappers ---- */ static inline int io_uring_setup(uint32_t entries, struct io_uring_params *p) { return (int) syscall(__NR_io_uring_setup, entries, p); } static inline int io_uring_enter( int fd, uint32_t to_submit, uint32_t min_complete, uint32_t flags, sigset_t *sig ) { return (int) syscall( __NR_io_uring_enter, fd, to_submit, min_complete, flags, sig, _NSIG / 8 ); } static inline int io_uring_register( int fd, uint32_t opcode, void *arg, uint32_t nr_args ) { return (int) syscall(__NR_io_uring_register, fd, opcode, arg, nr_args); } /* ---- io_uring setup flags not always present in older kernel headers ---- */ #ifndef IORING_SETUP_SQPOLL #define IORING_SETUP_SQPOLL (1U << 1) /* Linux 5.1 */ #endif #ifndef IORING_SETUP_SINGLE_ISSUER #define IORING_SETUP_SINGLE_ISSUER (1U << 12) /* Linux 6.0 */ #endif #ifndef IORING_SETUP_NO_SQARRAY #define IORING_SETUP_NO_SQARRAY (1U << 16) /* Linux 6.1 */ #endif #ifndef IORING_SQ_NEED_WAKEUP #define IORING_SQ_NEED_WAKEUP (1U << 0) /* sq_flags: kernel thread asleep */ #endif #ifndef IORING_ENTER_SQ_WAKEUP #define IORING_ENTER_SQ_WAKEUP (1U << 1) /* io_uring_enter: wake SQPOLL thread */ #endif /* ---- Internal operation opcodes ---- */ enum URING_OP { URING_READ = 0, URING_WRITE = 1, URING_FSYNC = 2, URING_FDSYNC = 3, }; /* user_data sentinel for cancel/internal SQEs (not an AIOOperation pointer) */ #define CANCEL_USER_DATA UINT64_MAX static const uint32_t CTX_MAX_REQUESTS_DEFAULT = 32; static const uint32_t EV_MAX_REQUESTS_DEFAULT = 512; static PyTypeObject AIOOperationType; static PyTypeObject AIOContextType; /* ================================================================ AIOOperation ================================================================ */ typedef struct { PyObject_HEAD PyObject *py_buffer; /* memoryview (read) or bytes (write) */ PyObject *callback; PyObject *context; /* owning Context, held only while in flight */ PyObject *weakreflist; uint8_t opcode; /* URING_* enum */ uint32_t fileno; uint64_t offset; int32_t result; int32_t error; Py_ssize_t buf_size; char *buf; uint8_t in_progress; uint8_t done; /* genuine completion reached - unlike in_progress * (sticky forever, guards resubmission), this is * what payload/get_value() gate on */ } AIOOperation; static int AIOOperation_traverse(AIOOperation *self, visitproc visit, void *arg) { Py_VISIT(self->py_buffer); Py_VISIT(self->callback); Py_VISIT(self->context); return 0; } static int AIOOperation_clear(AIOOperation *self) { Py_CLEAR(self->callback); Py_CLEAR(self->context); /* buf points into py_buffer's internal storage for reads — do NOT free * it separately; Py_CLEAR(py_buffer) handles the memory. */ Py_CLEAR(self->py_buffer); return 0; } static PyObject *AIOOperation_callback_ref(AIOOperation *self) { PyObject *callback; CAIO_BEGIN_CRITICAL_SECTION(self); callback = Py_XNewRef(self->callback); CAIO_END_CRITICAL_SECTION(); return callback; } static void AIOOperation_dealloc(AIOOperation *self) { PyObject_GC_UnTrack(self); if (self->weakreflist != NULL) PyObject_ClearWeakRefs((PyObject *) self); AIOOperation_clear(self); Py_TYPE(self)->tp_free((PyObject *) self); } static PyObject *AIOOperation_repr(AIOOperation *self) { const char *mode; switch (self->opcode) { case URING_READ: mode = "read"; break; case URING_WRITE: mode = "write"; break; case URING_FSYNC: mode = "fsync"; break; case URING_FDSYNC: mode = "fdsync"; break; default: mode = "noop"; break; } return PyUnicode_FromFormat( "<%s at %p: mode=\"%s\", fd=%u, offset=%llu, result=%d, buffer=%p>", Py_TYPE(self)->tp_name, self, mode, self->fileno, (unsigned long long) self->offset, self->result, self->buf ); } PyDoc_STRVAR(AIOOperation_read_docstring, "Creates a new Operation for reading.\n\n" " Operation.read(nbytes, fd, offset, priority=0) -> Operation" ); static PyObject *AIOOperation_read( PyTypeObject *type, PyObject *args, PyObject *kwds ) { static char *kwlist[] = {"nbytes", "fd", "offset", "priority", NULL}; AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory"); return NULL; } self->buf = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->context = NULL; self->weakreflist = NULL; self->callback = NULL; self->error = 0; uint64_t nbytes = 0; uint16_t priority = 0; if (!PyArg_ParseTupleAndKeywords( args, kwds, "KI|KH", kwlist, &nbytes, &self->fileno, &self->offset, &priority)) { Py_DECREF(self); return NULL; } /* Allocate the result bytes object directly — the kernel writes into * its internal buffer, so get_value() can return it with no copy. * PyBytes_FromStringAndSize(NULL, n) leaves the memory uninitialized * (documented CPython behavior) - a short read's untouched tail must * read as zero, not whatever heap garbage happened to be there * (a real information-disclosure risk, not just cosmetic). */ self->py_buffer = PyBytes_FromStringAndSize(NULL, (Py_ssize_t) nbytes); if (self->py_buffer == NULL) { Py_DECREF(self); return NULL; } self->buf = PyBytes_AS_STRING(self->py_buffer); self->buf_size = (Py_ssize_t) nbytes; memset(self->buf, 0, (size_t) nbytes); self->opcode = URING_READ; return (PyObject *) self; } PyDoc_STRVAR(AIOOperation_write_docstring, "Creates a new Operation for writing.\n\n" " Operation.write(payload_bytes, fd, offset, priority=0) -> Operation" ); static PyObject *AIOOperation_write( PyTypeObject *type, PyObject *args, PyObject *kwds ) { static char *kwlist[] = {"payload_bytes", "fd", "offset", "priority", NULL}; AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory"); return NULL; } self->buf = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->context = NULL; self->weakreflist = NULL; self->callback = NULL; self->error = 0; uint16_t priority = 0; /* Parsed into a plain local first, not directly into self->py_buffer: * "O" hands back a borrowed reference, and self->py_buffer must never * hold one - AIOOperation_dealloc unconditionally Py_CLEARs it. Only * assigned (and incref'd) below once confirmed to actually be the * bytes object this Operation is going to own. */ PyObject *payload_bytes = NULL; if (!PyArg_ParseTupleAndKeywords( args, kwds, "OI|KH", kwlist, &payload_bytes, &self->fileno, &self->offset, &priority)) { Py_DECREF(self); return NULL; } if (!PyBytes_Check(payload_bytes)) { Py_DECREF(self); PyErr_SetString(PyExc_ValueError, "payload_bytes must be bytes"); return NULL; } if (PyBytes_AsStringAndSize(payload_bytes, &self->buf, &self->buf_size)) { Py_DECREF(self); return NULL; } self->py_buffer = payload_bytes; Py_INCREF(self->py_buffer); self->opcode = URING_WRITE; return (PyObject *) self; } PyDoc_STRVAR(AIOOperation_fsync_docstring, "Creates a new Operation for fsync.\n\n" " Operation.fsync(fd, priority=0) -> Operation" ); static PyObject *AIOOperation_fsync( PyTypeObject *type, PyObject *args, PyObject *kwds ) { static char *kwlist[] = {"fd", "priority", NULL}; AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory"); return NULL; } self->buf = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->context = NULL; self->weakreflist = NULL; self->callback = NULL; self->error = 0; uint16_t priority = 0; if (!PyArg_ParseTupleAndKeywords( args, kwds, "I|H", kwlist, &self->fileno, &priority)) { Py_DECREF(self); return NULL; } self->opcode = URING_FSYNC; return (PyObject *) self; } PyDoc_STRVAR(AIOOperation_fdsync_docstring, "Creates a new Operation for fdatasync.\n\n" " Operation.fdsync(fd, priority=0) -> Operation" ); static PyObject *AIOOperation_fdsync( PyTypeObject *type, PyObject *args, PyObject *kwds ) { static char *kwlist[] = {"fd", "priority", NULL}; AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory"); return NULL; } self->buf = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->context = NULL; self->weakreflist = NULL; self->callback = NULL; self->error = 0; uint16_t priority = 0; if (!PyArg_ParseTupleAndKeywords( args, kwds, "I|H", kwlist, &self->fileno, &priority)) { Py_DECREF(self); return NULL; } self->opcode = URING_FDSYNC; return (PyObject *) self; } PyDoc_STRVAR(AIOOperation_get_value_docstring, "Returns the result of the completed Operation.\n\n" " Operation.get_value() -> Optional[Union[bytes, int]]" ); static PyObject *AIOOperation_get_value( AIOOperation *self, PyObject *args, PyObject *kwds ) { if ( CAIO_ATOMIC_LOAD(self->in_progress) && !CAIO_ATOMIC_LOAD(self->done) ) { PyErr_SetString( PyExc_RuntimeError, "get_value() is not available while the operation is in flight" ); return NULL; } if (self->error != 0) { PyErr_SetString(PyExc_SystemError, strerror(self->error)); return NULL; } switch (self->opcode) { case URING_READ: /* py_buffer can only be NULL here if tp_clear() already ran - * only possible once this Operation is otherwise unreachable * (see AIOOperation_traverse/_clear), so nothing could actually * be waiting on this return value, but degrade to None rather * than crash on the now-dangling self->buf either way. */ if (self->py_buffer == NULL) Py_RETURN_NONE; /* Fast path: kernel filled the whole buffer — return py_buffer * directly with no copy. Partial reads (e.g. EOF) fall back to * a slice. */ if (self->buf_size == PyBytes_GET_SIZE(self->py_buffer)) { Py_INCREF(self->py_buffer); return self->py_buffer; } return PyBytes_FromStringAndSize(self->buf, self->buf_size); case URING_WRITE: return PyLong_FromSsize_t(self->result); } Py_RETURN_NONE; } PyDoc_STRVAR(AIOOperation_set_callback_docstring, "Sets a callback to invoke when the Operation completes.\n\n" " Operation.set_callback(callback) -> bool" ); static PyObject *AIOOperation_set_callback( AIOOperation *self, PyObject *callback ) { PyObject *old_callback; if (!PyCallable_Check(callback)) { PyErr_Format(PyExc_ValueError, "object %r is not callable", callback); return NULL; } Py_INCREF(callback); CAIO_BEGIN_CRITICAL_SECTION(self); old_callback = self->callback; self->callback = callback; CAIO_END_CRITICAL_SECTION(); Py_XDECREF(old_callback); Py_RETURN_TRUE; } static PyObject *AIOOperation_payload_getter(AIOOperation *self, void *closure) { if ( CAIO_ATOMIC_LOAD(self->in_progress) && !CAIO_ATOMIC_LOAD(self->done) ) { PyErr_SetString( PyExc_RuntimeError, "payload is not available while the operation is in flight" ); return NULL; } /* fsync/fdsync Operations never allocate a buffer - matches T_OBJECT's * (as opposed to T_OBJECT_EX's) own NULL-to-None behavior, which this * getter replaces. */ if (self->py_buffer == NULL) Py_RETURN_NONE; Py_INCREF(self->py_buffer); return self->py_buffer; } static PyGetSetDef AIOOperation_getset[] = { { "payload", (getter) AIOOperation_payload_getter, NULL, "payload", NULL }, {NULL} }; static PyMemberDef AIOOperation_members[] = { { "context", T_OBJECT, offsetof(AIOOperation, context), READONLY, "context object" }, { "fileno", T_UINT, offsetof(AIOOperation, fileno), READONLY, "file descriptor" }, { "offset", T_ULONGLONG, offsetof(AIOOperation, offset), READONLY, "offset" }, { "nbytes", T_PYSSIZET, offsetof(AIOOperation, buf_size), READONLY, "nbytes" }, { "result", T_INT, offsetof(AIOOperation, result), READONLY, "result" }, { "error", T_INT, offsetof(AIOOperation, error), READONLY, "error" }, {NULL} }; static PyMethodDef AIOOperation_methods[] = { { "read", (PyCFunction) AIOOperation_read, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_read_docstring }, { "write", (PyCFunction) AIOOperation_write, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_write_docstring }, { "fsync", (PyCFunction) AIOOperation_fsync, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_fsync_docstring }, { "fdsync", (PyCFunction) AIOOperation_fdsync, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_fdsync_docstring }, { "get_value", (PyCFunction) AIOOperation_get_value, METH_NOARGS, AIOOperation_get_value_docstring }, { "set_callback", (PyCFunction) AIOOperation_set_callback, METH_O, AIOOperation_set_callback_docstring }, {NULL} }; static PyTypeObject AIOOperationType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "linux_uring.Operation", .tp_doc = "io_uring AIO operation", .tp_basicsize = sizeof(AIOOperation), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, .tp_dealloc = (destructor) AIOOperation_dealloc, .tp_traverse = (traverseproc) AIOOperation_traverse, .tp_clear = (inquiry) AIOOperation_clear, .tp_repr = (reprfunc) AIOOperation_repr, .tp_members = AIOOperation_members, .tp_getset = AIOOperation_getset, .tp_methods = AIOOperation_methods, .tp_weaklistoffset = offsetof(AIOOperation, weakreflist), }; /* ================================================================ AIOContext ================================================================ */ typedef struct { PyObject_HEAD int uring_fd; int eventfd_fd; uint32_t max_requests; /* SQ ring mmap */ void *sq_ring_ptr; size_t sq_ring_size; uint32_t *sq_head; uint32_t *sq_tail; uint32_t *sq_ring_mask; uint32_t *sq_ring_entries; uint32_t *sq_flags; uint32_t *sq_array; /* SQE array mmap */ struct io_uring_sqe *sqes; size_t sqes_size; /* CQ ring mmap */ void *cq_ring_ptr; size_t cq_ring_size; uint32_t *cq_head; uint32_t *cq_tail; uint32_t *cq_ring_mask; uint32_t *cq_ring_entries; struct io_uring_cqe *cqes; uint8_t no_sqarray; /* IORING_SETUP_NO_SQARRAY was used */ uint8_t sqpoll; /* IORING_SETUP_SQPOLL was used */ PyObject *weakreflist; } AIOContext; static void AIOContext_dealloc(AIOContext *self) { if (self->weakreflist != NULL) PyObject_ClearWeakRefs((PyObject *) self); if (self->sq_ring_ptr != MAP_FAILED && self->sq_ring_ptr != NULL) munmap(self->sq_ring_ptr, self->sq_ring_size); if (self->sqes != MAP_FAILED && self->sqes != NULL) munmap(self->sqes, self->sqes_size); /* Skip CQ munmap when it shares the SQ ring mapping (IORING_FEAT_SINGLE_MMAP) */ if (self->cq_ring_ptr != MAP_FAILED && self->cq_ring_ptr != NULL && self->cq_ring_ptr != self->sq_ring_ptr) munmap(self->cq_ring_ptr, self->cq_ring_size); if (self->uring_fd >= 0) close(self->uring_fd); if (self->eventfd_fd >= 0) close(self->eventfd_fd); Py_TYPE(self)->tp_free((PyObject *) self); } static PyObject *AIOContext_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { AIOContext *self = (AIOContext *) type->tp_alloc(type, 0); if (self != NULL) { self->weakreflist = NULL; self->uring_fd = -1; self->eventfd_fd = -1; self->sq_ring_ptr = MAP_FAILED; self->cq_ring_ptr = MAP_FAILED; self->sqes = MAP_FAILED; self->no_sqarray = 0; self->sqpoll = 0; } return (PyObject *) self; } static int AIOContext_init(AIOContext *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = {"max_requests", "sqpoll", NULL}; self->max_requests = 0; int want_sqpoll = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Ip", kwlist, &self->max_requests, &want_sqpoll)) return -1; if (self->max_requests == 0) self->max_requests = CTX_MAX_REQUESTS_DEFAULT; /* eventfd for asyncio add_reader notification. EFD_NONBLOCK so poll()'s * read() raises BlockingIOError when nothing is pending yet, as * documented, instead of blocking the caller. */ self->eventfd_fd = eventfd(0, EFD_NONBLOCK); if (self->eventfd_fd < 0) { PyErr_SetFromErrno(PyExc_SystemError); return -1; } /* * Two probe-and-fallback flag tables. * * Default (sqpoll=False): conventional explicit submit via io_uring_enter. * flush() → io_uring_enter() → inline completions land immediately in CQ * → drain_cq fires futures before `await`, so page-cache ops skip * event-loop suspension entirely. Latency ~ linux_aio at low QD. * * Opt-in (sqpoll=True): SQPOLL kernel thread polls SQ ring; io_uring_enter * only needed to wake a sleeping thread. Eliminates per-op syscall * overhead at sustained high QD. EPERM on pre-5.11 kernels without * CAP_SYS_NICE is treated like EINVAL (try next entry). * * IORING_SETUP_SINGLE_ISSUER deliberately never tried: it pins the ring * to whichever thread's io_uring_setup()/io_uring_enter() call created * it, rejecting submit()/flush() from any other thread with -EEXIST - * this Context's own Python API makes no such thread-affinity promise. */ static const uint32_t flag_table_default[] = { IORING_SETUP_NO_SQARRAY, 0, }; static const uint32_t flag_table_sqpoll[] = { IORING_SETUP_SQPOLL | IORING_SETUP_NO_SQARRAY, IORING_SETUP_SQPOLL, 0, /* fallback: plain ring without SQPOLL */ }; const uint32_t *flag_table = want_sqpoll ? flag_table_sqpoll : flag_table_default; int table_len = want_sqpoll ? (int)(sizeof(flag_table_sqpoll) / sizeof(flag_table_sqpoll[0])) : (int)(sizeof(flag_table_default) / sizeof(flag_table_default[0])); struct io_uring_params params; uint32_t flags_used = 0; for (int i = 0; i < table_len; i++) { memset(¶ms, 0, sizeof(params)); params.flags = flag_table[i]; if (params.flags & IORING_SETUP_SQPOLL) params.sq_thread_idle = 100; /* ms; let kthread sleep when idle */ /* +1: SQPOLL dequeue lag headroom */ self->uring_fd = io_uring_setup(self->max_requests + 1, ¶ms); if (self->uring_fd >= 0) { flags_used = params.flags; break; } if (errno != EINVAL && errno != EPERM) { PyErr_SetFromErrno(PyExc_SystemError); return -1; } } if (self->uring_fd < 0) { PyErr_SetFromErrno(PyExc_SystemError); return -1; } self->no_sqarray = (flags_used & IORING_SETUP_NO_SQARRAY) ? 1 : 0; self->sqpoll = (flags_used & IORING_SETUP_SQPOLL) ? 1 : 0; /* * IORING_FEAT_SINGLE_MMAP: SQ ring and CQ ring share one mmap at * IORING_OFF_SQ_RING; use max(sq_size, cq_size) for the mapping. * Without it, use two separate mmaps (original behaviour). */ size_t sq_ring_size, cq_ring_size; if (self->no_sqarray) sq_ring_size = params.sq_off.flags + sizeof(uint32_t); else sq_ring_size = params.sq_off.array + params.sq_entries * sizeof(uint32_t); cq_ring_size = params.cq_off.cqes + params.cq_entries * sizeof(struct io_uring_cqe); if (params.features & IORING_FEAT_SINGLE_MMAP) { self->sq_ring_size = sq_ring_size > cq_ring_size ? sq_ring_size : cq_ring_size; self->sq_ring_ptr = mmap( NULL, self->sq_ring_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, self->uring_fd, IORING_OFF_SQ_RING ); if (self->sq_ring_ptr == MAP_FAILED) { PyErr_SetFromErrno(PyExc_SystemError); return -1; } self->cq_ring_ptr = self->sq_ring_ptr; /* shared mapping */ self->cq_ring_size = 0; /* don't munmap separately */ } else { self->sq_ring_size = sq_ring_size; self->sq_ring_ptr = mmap( NULL, self->sq_ring_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, self->uring_fd, IORING_OFF_SQ_RING ); if (self->sq_ring_ptr == MAP_FAILED) { PyErr_SetFromErrno(PyExc_SystemError); return -1; } self->cq_ring_size = cq_ring_size; self->cq_ring_ptr = mmap( NULL, self->cq_ring_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, self->uring_fd, IORING_OFF_CQ_RING ); if (self->cq_ring_ptr == MAP_FAILED) { PyErr_SetFromErrno(PyExc_SystemError); return -1; } } char *sq = (char *) self->sq_ring_ptr; self->sq_head = (uint32_t *)(sq + params.sq_off.head); self->sq_tail = (uint32_t *)(sq + params.sq_off.tail); self->sq_ring_mask = (uint32_t *)(sq + params.sq_off.ring_mask); self->sq_ring_entries = (uint32_t *)(sq + params.sq_off.ring_entries); self->sq_flags = (uint32_t *)(sq + params.sq_off.flags); self->sq_array = (uint32_t *)(sq + params.sq_off.array); /* unused when no_sqarray */ /* mmap SQE array */ self->sqes_size = params.sq_entries * sizeof(struct io_uring_sqe); self->sqes = mmap( NULL, self->sqes_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, self->uring_fd, IORING_OFF_SQES ); if (self->sqes == MAP_FAILED) { PyErr_SetFromErrno(PyExc_SystemError); return -1; } char *cq = (char *) self->cq_ring_ptr; self->cq_head = (uint32_t *)(cq + params.cq_off.head); self->cq_tail = (uint32_t *)(cq + params.cq_off.tail); self->cq_ring_mask = (uint32_t *)(cq + params.cq_off.ring_mask); self->cq_ring_entries = (uint32_t *)(cq + params.cq_off.ring_entries); self->cqes = (struct io_uring_cqe *)(cq + params.cq_off.cqes); /* * Register eventfd for completion notification, unconditionally with * plain IORING_REGISTER_EVENTFD (not the _ASYNC variant) in both * SQPOLL and non-SQPOLL mode. * * Non-SQPOLL mode used IORING_REGISTER_EVENTFD_ASYNC previously, to * avoid a redundant _on_read_event wakeup for ops that already * completed inline during io_uring_enter (caught by drain_cq() inside * flush() before the eventfd path would even run). That relies on the * kernel's own inline-vs-deferred classification for whether to signal * the fd - confirmed (via a genuine disk-backed repro, not tmpfs) to * fail to signal at all for at least one real deferred completion, an * unresolvable hang: the read reaches the CQ ring but nothing ever * wakes epoll_wait() to drain it. A spurious extra wakeup on an * already-fully-drained ring is harmless (process_events() just finds * nothing and returns immediately); a completion notification that * silently never arrives is not. Same reasoning applies to SQPOLL, * which already used the plain variant for a similar reason (the * SQPOLL thread's own synchronous completions must not be suppressed * either). */ if (io_uring_register(self->uring_fd, IORING_REGISTER_EVENTFD, &self->eventfd_fd, 1) < 0) { PyErr_SetFromErrno(PyExc_SystemError); return -1; } return 0; } static PyObject *AIOContext_repr(AIOContext *self) { return PyUnicode_FromFormat( "<%s as %p: max_requests=%u, uring_fd=%d, eventfd=%d, sqpoll=%d>", Py_TYPE(self)->tp_name, self, self->max_requests, self->uring_fd, self->eventfd_fd, (int) self->sqpoll ); } /* * Drain all currently-available CQEs from the completion ring. * No syscall — reads directly from the mmap'd ring. * Returns number of completions processed, or -1 on Python error. * * The whole batch (up to `max` real completions) is scanned and the ring's * own head is fully committed BEFORE any callback runs - a callback that * reentrantly calls process_events()/flush() must see a ring already * caught up through this entire batch, never a subset of it. Committing * head incrementally, one CQE at a time right before that CQE's own * callback, does not fix this: by the time callback #3 (say) runs, head * would already be past #3, but a reentrant call would then race the * outer loop to process #4..N itself, double-processing whichever ones * both reach first - the exact same class of bug, just shifted to a * different subset. Collecting the whole batch first and only invoking * callbacks afterward removes the race entirely. */ static int uring_drain_cq(AIOContext *self, uint32_t max) { CAIO_BEGIN_CRITICAL_SECTION(self); /* released before any callback runs */ uint32_t head = __atomic_load_n(self->cq_head, __ATOMIC_RELAXED); uint32_t tail = __atomic_load_n(self->cq_tail, __ATOMIC_ACQUIRE); uint32_t mask = *self->cq_ring_mask; uint32_t avail = tail - head; uint32_t cap = avail < max ? avail : max; AIOOperation **ops = NULL; int32_t *results = NULL; if (cap > 0) { ops = PyMem_New(AIOOperation *, cap); results = PyMem_New(int32_t, cap); if (ops == NULL || results == NULL) { PyMem_Free(ops); PyMem_Free(results); CAIO_END_CRITICAL_SECTION(); PyErr_NoMemory(); return -1; } } uint32_t count = 0; while (head != tail && count < max) { struct io_uring_cqe *cqe = &self->cqes[head & mask]; head++; if (cqe->user_data == CANCEL_USER_DATA) continue; /* in_progress deliberately NOT reset here - one-shot forever once * genuinely submitted, matching thread_aio/linux_aio and the * (paused) Rust rewrite: a completed Operation must not be * resubmittable, only a fresh one constructed for a retry. * context IS released here (unlike in_progress) - the kernel is * done touching anything for this op, so there's no more reason * to keep the Context alive on its behalf. */ AIOOperation *op = (AIOOperation *)(uintptr_t) cqe->user_data; op->result = cqe->res; if (cqe->res < 0) { op->error = -cqe->res; } else if (op->opcode == URING_READ) { op->buf_size = cqe->res; } Py_CLEAR(op->context); CAIO_ATOMIC_STORE(op->done, 1); ops[count] = op; results[count] = cqe->res; count++; } /* Ring state fully committed - reentrant callers now see this whole * batch as already consumed, before a single callback has run. */ __atomic_store_n(self->cq_head, head, __ATOMIC_RELEASE); CAIO_END_CRITICAL_SECTION(); for (uint32_t i = 0; i < count; i++) { AIOOperation *op = ops[i]; PyObject *callback = AIOOperation_callback_ref(op); if (callback != NULL) { PyObject *arg = PyLong_FromLong((long) results[i]); if (arg == NULL) { PyErr_WriteUnraisable(callback); } else { PyObject *rv = PyObject_CallOneArg(callback, arg); Py_DECREF(arg); if (rv == NULL) { PyErr_WriteUnraisable(callback); } else { Py_DECREF(rv); } } Py_DECREF(callback); } Py_DECREF(op); } PyMem_Free(ops); PyMem_Free(results); return (int) count; } PyDoc_STRVAR(AIOContext_submit_docstring, "Submits Operations to the io_uring SQ.\n\n" " Context.submit(op1, op2, ...) -> int" ); static PyObject *AIOContext_submit(AIOContext *self, PyObject *args) { if (self->uring_fd < 0) { PyErr_SetString(PyExc_RuntimeError, "context not initialized"); return NULL; } if (!PyTuple_Check(args)) { PyErr_SetNone(PyExc_ValueError); return NULL; } Py_ssize_t nr = PyTuple_GET_SIZE(args); if (nr == 0) return PyLong_FromLong(0); for (Py_ssize_t i = 0; i < nr; i++) { PyObject *obj = PyTuple_GET_ITEM(args, i); if (Py_TYPE(obj) != &AIOOperationType) { PyErr_Format(PyExc_TypeError, "argument %zd is not an Operation", i); return NULL; } } CAIO_BEGIN_CRITICAL_SECTION(self); /* serializes tail/SQE writes */ uint32_t tail = __atomic_load_n(self->sq_tail, __ATOMIC_RELAXED); uint32_t head = __atomic_load_n(self->sq_head, __ATOMIC_ACQUIRE); uint32_t mask = *self->sq_ring_mask; uint32_t capacity = *self->sq_ring_entries; uint32_t submitted = 0; for (Py_ssize_t i = 0; i < nr; i++) { AIOOperation *op = (AIOOperation *) PyTuple_GET_ITEM(args, i); /* Atomic exchange, not check-then-set: two Contexts racing on the same * Operation must not both stage an SQE for it. Claimed up front * so the check-and-set is one atomic step - the exit paths below * that can still skip a freshly-claimed op undo the claim. */ if (CAIO_ATOMIC_LOAD_STORE(op->in_progress, 1)) continue; if ((tail - head) >= capacity) { /* Not staged - give the claim back. Commit whatever WAS * staged earlier in this call first, or it'd be invisible to * the kernel forever (sq_tail never advanced past it). */ CAIO_ATOMIC_STORE(op->in_progress, 0); __atomic_store_n(self->sq_tail, tail, __ATOMIC_RELEASE); CAIO_END_CRITICAL_SECTION(); PyErr_SetString(PyExc_OverflowError, "io_uring SQ ring full"); return NULL; } uint32_t index = tail & mask; struct io_uring_sqe *sqe = &self->sqes[index]; memset(sqe, 0, sizeof(*sqe)); sqe->fd = (int32_t) op->fileno; sqe->off = op->offset; sqe->user_data = (uint64_t)(uintptr_t) op; switch (op->opcode) { case URING_READ: sqe->opcode = IORING_OP_READ; sqe->addr = (uint64_t)(uintptr_t) op->buf; sqe->len = (uint32_t) op->buf_size; break; case URING_WRITE: sqe->opcode = IORING_OP_WRITE; sqe->addr = (uint64_t)(uintptr_t) op->buf; sqe->len = (uint32_t) op->buf_size; break; case URING_FSYNC: sqe->opcode = IORING_OP_FSYNC; break; case URING_FDSYNC: sqe->opcode = IORING_OP_FSYNC; sqe->fsync_flags = IORING_FSYNC_DATASYNC; break; default: /* Unrecognized opcode: give the claim back, this op was * never staged. */ CAIO_ATOMIC_STORE(op->in_progress, 0); continue; } if (!self->no_sqarray) self->sq_array[index] = index; tail++; Py_INCREF(op); /* Held only while genuinely in flight (cleared on completion in * uring_drain_cq()) - without this, a Context the caller drops * while an operation is still outstanding could be garbage * collected mid-flight, munmapping the SQ/CQ rings and closing * uring_fd while the kernel may still be touching them. */ Py_XDECREF(op->context); op->context = (PyObject *) self; Py_INCREF(self); submitted++; } __atomic_store_n(self->sq_tail, tail, __ATOMIC_RELEASE); CAIO_END_CRITICAL_SECTION(); /* * Do NOT call io_uring_enter here. The Python asyncio layer batches * all SQEs written during the current event-loop tick and submits them * with a single io_uring_enter via Context.flush(). This trades * N per-op syscalls for one batched call, matching linux_aio's * io_submit(ctx, N, iocbpp) pattern. */ return PyLong_FromUnsignedLong(submitted); } PyDoc_STRVAR(AIOContext_flush_docstring, "Submit all pending SQEs to the kernel in one io_uring_enter call,\n" "then drain any completions that finished inline.\n\n" " Context.flush() -> int # number of completions processed" ); static PyObject *AIOContext_flush(AIOContext *self, PyObject *args) { if (self->uring_fd < 0) { PyErr_SetString(PyExc_RuntimeError, "context not initialized"); return NULL; } /* * Number of SQEs filled but not yet handed to the kernel: * sq_tail = next slot we will write (updated by submit()) * sq_head = next slot the kernel will consume (updated by kernel * after io_uring_enter returns) * Their difference is the number of pending SQEs. */ if (self->sqpoll) { /* * SQPOLL mode: the kernel thread picks up SQEs from the ring * automatically — no io_uring_enter needed for submission. * We only send a wakeup when the thread has gone to sleep * (IORING_SQ_NEED_WAKEUP set after sq_thread_idle ms of quiet). */ uint32_t sq_flags = __atomic_load_n(self->sq_flags, __ATOMIC_RELAXED); if (sq_flags & IORING_SQ_NEED_WAKEUP) { if (io_uring_enter(self->uring_fd, 0, 0, IORING_ENTER_SQ_WAKEUP, NULL) < 0) { PyErr_SetFromErrno(PyExc_SystemError); return NULL; } } int count = uring_drain_cq(self, UINT32_MAX); if (count < 0) return NULL; return PyLong_FromLong(count); } /* Non-SQPOLL: submit all pending SQEs with one io_uring_enter. */ uint32_t tail = __atomic_load_n(self->sq_tail, __ATOMIC_RELAXED); uint32_t head = __atomic_load_n(self->sq_head, __ATOMIC_ACQUIRE); uint32_t to_submit = tail - head; if (to_submit == 0) return PyLong_FromLong(0); uint32_t remaining = to_submit; while (remaining > 0) { int ret = io_uring_enter(self->uring_fd, remaining, 0, 0, NULL); if (ret < 0) { PyErr_SetFromErrno(PyExc_SystemError); return NULL; } remaining -= (uint32_t) ret; } /* * Drain any completions that finished inline within io_uring_enter. * For page-cache hits this fires futures immediately; truly async ops * will arrive later via the eventfd → _on_read_event path. */ int count = uring_drain_cq(self, UINT32_MAX); if (count < 0) return NULL; return PyLong_FromLong(count); } PyDoc_STRVAR(AIOContext_cancel_docstring, "Requests async cancellation of a submitted Operation.\n\n" " Context.cancel(operation) -> int" ); static PyObject *AIOContext_cancel( AIOContext *self, PyObject *args, PyObject *kwds ) { static char *kwlist[] = {"operation", NULL}; AIOOperation *op = NULL; if (!PyArg_ParseTupleAndKeywords( args, kwds, "O!", kwlist, &AIOOperationType, &op)) return NULL; CAIO_BEGIN_CRITICAL_SECTION(self); uint32_t tail = __atomic_load_n(self->sq_tail, __ATOMIC_RELAXED); uint32_t head = __atomic_load_n(self->sq_head, __ATOMIC_ACQUIRE); if ((tail - head) >= *self->sq_ring_entries) { CAIO_END_CRITICAL_SECTION(); PyErr_SetString(PyExc_OverflowError, "io_uring SQ ring full"); return NULL; } uint32_t index = tail & *self->sq_ring_mask; struct io_uring_sqe *sqe = &self->sqes[index]; memset(sqe, 0, sizeof(*sqe)); sqe->opcode = IORING_OP_ASYNC_CANCEL; sqe->addr = (uint64_t)(uintptr_t) op; /* target op's user_data */ sqe->user_data = CANCEL_USER_DATA; /* sentinel: skip in process_events */ if (!self->no_sqarray) self->sq_array[index] = index; __atomic_store_n(self->sq_tail, tail + 1, __ATOMIC_RELEASE); CAIO_END_CRITICAL_SECTION(); io_uring_enter(self->uring_fd, 1, 0, 0, NULL); return PyLong_FromLong(0); } PyDoc_STRVAR(AIOContext_process_events_docstring, "Collects completed operations and fires their callbacks.\n\n" " Context.process_events(max_requests=512, min_requests=0, timeout=0) -> int\n\n" " timeout=0 checks once and returns immediately (the usual asyncio-\n" " adapter path - real waiting happens via the eventfd/loop.add_reader()\n" " instead). timeout>0 blocks up to that many seconds; timeout<0 blocks\n" " indefinitely. Both are for manual synchronous polling from a plain\n" " thread - do not also select()/poll() on `.fileno` from elsewhere\n" " while relying on this to wait, the two waiting mechanisms are\n" " alternatives, not meant to be combined on the same Context." ); static PyObject *AIOContext_process_events( AIOContext *self, PyObject *args, PyObject *kwds ) { static char *kwlist[] = {"max_requests", "min_requests", "timeout", NULL}; uint32_t max_requests = 0; uint32_t min_requests = 0; int32_t tv_sec = 0; if (!PyArg_ParseTupleAndKeywords( args, kwds, "|IIi", kwlist, &max_requests, &min_requests, &tv_sec)) return NULL; if (max_requests == 0) max_requests = EV_MAX_REQUESTS_DEFAULT; if (min_requests > max_requests) { PyErr_Format( PyExc_ValueError, "min_requests (%u) must be <= max_requests (%u)", min_requests, max_requests ); return NULL; } /* Wait for at least min_requests completions: timeout=0 checks once, * timeout>0 bounds the wait, timeout<0 waits indefinitely. Plain * io_uring_enter(GETEVENTS) has no timeout parameter of its own (unlike * linux_aio's io_getevents, which takes a struct timespec directly) - * `tv_sec` used to be parsed and then silently discarded entirely, so * this used to always block indefinitely regardless of the argument. * GIL released throughout so this doesn't freeze every other thread in * the interpreter for the duration. */ if (min_requests > 0 && tv_sec == 0) { /* Fast path: timeout=0 means "check once, don't actually wait" - * matching linux_aio's own io_getevents(timeout={0,0}) semantics - * so skip the deadline/poll-loop machinery below entirely. */ int ret; Py_BEGIN_ALLOW_THREADS ret = io_uring_enter(self->uring_fd, 0, 0, IORING_ENTER_GETEVENTS, NULL); Py_END_ALLOW_THREADS if (ret < 0 && errno != EINTR) { PyErr_SetFromErrno(PyExc_SystemError); return NULL; } } else if (min_requests > 0) { int has_deadline = tv_sec > 0; struct timespec deadline; if (has_deadline) { clock_gettime(CLOCK_MONOTONIC, &deadline); deadline.tv_sec += tv_sec; } int saved_errno = 0; Py_BEGIN_ALLOW_THREADS for (;;) { uint32_t head = __atomic_load_n(self->cq_head, __ATOMIC_RELAXED); uint32_t tail = __atomic_load_n(self->cq_tail, __ATOMIC_ACQUIRE); if (tail - head >= min_requests) break; int ret = io_uring_enter( self->uring_fd, 0, 0, IORING_ENTER_GETEVENTS, NULL ); if (ret < 0 && errno != EINTR) { saved_errno = errno; break; } head = __atomic_load_n(self->cq_head, __ATOMIC_RELAXED); tail = __atomic_load_n(self->cq_tail, __ATOMIC_ACQUIRE); if (tail - head >= min_requests) break; int poll_timeout_ms = -1; /* poll()'s own "block indefinitely" */ if (has_deadline) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); int64_t remaining_ms = (int64_t) (deadline.tv_sec - now.tv_sec) * 1000 + (deadline.tv_nsec - now.tv_nsec) / 1000000; if (remaining_ms <= 0) break; poll_timeout_ms = remaining_ms > INT_MAX ? INT_MAX : (int) remaining_ms; } /* Block on the eventfd (the same fd loop.add_reader() watches * for the asyncio adapter) instead of busy-polling with a fixed * sleep interval - the kernel signals it the instant a * completion posts, so this wakes up immediately rather than up * to one poll interval late. A fixed-interval nanosleep() here * previously meant every wait effectively cost at least one * full interval, regardless of how quickly the completion * actually arrived - severe at low concurrency, where each * operation pays that cost on its own. * * This blocking path and the asyncio adapter's own eventfd * wakeup are alternative ways to wait, not meant to be combined * on the same Context (see the docstring) - so no separate * defense against racing some other waiter's read() of the * counter is needed here beyond what's already true of any * multi-waiter poll()/epoll on one fd (all current waiters see * readiness once the counter goes above zero; whoever's read() * clears it doesn't retroactively un-wake anyone who already * observed it). */ struct pollfd pfd = { .fd = self->eventfd_fd, .events = POLLIN }; int pret = poll(&pfd, 1, poll_timeout_ms); if (pret < 0 && errno != EINTR) { saved_errno = errno; break; } if (pret > 0 && (pfd.revents & POLLIN)) { uint64_t val; /* EFD_NONBLOCK: a short/failed read here just means * something else already drained the counter - harmless, * the loop re-checks the ring regardless. */ if (read(self->eventfd_fd, &val, sizeof(val)) < 0) { /* nothing to do - see comment above */ } } } Py_END_ALLOW_THREADS if (saved_errno != 0) { errno = saved_errno; PyErr_SetFromErrno(PyExc_SystemError); return NULL; } } int count = uring_drain_cq(self, max_requests); if (count < 0) return NULL; return PyLong_FromLong(count); } PyDoc_STRVAR(AIOContext_poll_docstring, "Drains the eventfd counter (call before process_events in add_reader handler).\n\n" " Context.poll() -> int" ); static PyObject *AIOContext_poll(AIOContext *self, PyObject *args) { uint64_t result = 0; ssize_t n = read(self->eventfd_fd, &result, sizeof(uint64_t)); if (n != (ssize_t) sizeof(uint64_t)) { PyErr_SetNone(PyExc_BlockingIOError); return NULL; } return PyLong_FromUnsignedLongLong(result); } static PyMemberDef AIOContext_members[] = { { "fileno", T_INT, offsetof(AIOContext, eventfd_fd), READONLY, "eventfd file descriptor" }, { "max_requests", T_UINT, offsetof(AIOContext, max_requests), READONLY, "max requests" }, { "sqpoll", T_UBYTE, offsetof(AIOContext, sqpoll), READONLY, "SQPOLL mode active" }, {NULL} }; static PyMethodDef AIOContext_methods[] = { { "submit", (PyCFunction) AIOContext_submit, METH_VARARGS, AIOContext_submit_docstring }, { "flush", (PyCFunction) AIOContext_flush, METH_NOARGS, AIOContext_flush_docstring }, { "cancel", (PyCFunction) AIOContext_cancel, METH_VARARGS | METH_KEYWORDS, AIOContext_cancel_docstring }, { "process_events", (PyCFunction) AIOContext_process_events, METH_VARARGS | METH_KEYWORDS, AIOContext_process_events_docstring }, { "poll", (PyCFunction) AIOContext_poll, METH_NOARGS, AIOContext_poll_docstring }, {NULL} }; static PyTypeObject AIOContextType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "Context", .tp_doc = "io_uring AIO context", .tp_basicsize = sizeof(AIOContext), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_new = AIOContext_new, .tp_init = (initproc) AIOContext_init, .tp_dealloc = (destructor) AIOContext_dealloc, .tp_repr = (reprfunc) AIOContext_repr, .tp_members = AIOContext_members, .tp_methods = AIOContext_methods, .tp_weaklistoffset = offsetof(AIOContext, weakreflist), }; /* ================================================================ Module ================================================================ */ static PyModuleDef linux_uring_module = { PyModuleDef_HEAD_INIT, .m_name = "linux_uring", .m_doc = "io_uring based AIO backend (Linux 5.6+).", .m_size = -1, }; PyMODINIT_FUNC PyInit_linux_uring(void) { /* Verify kernel >= 5.6 (IORING_OP_READ / IORING_OP_WRITE) */ struct utsname uts; if (uname(&uts) != 0) { PyErr_SetString(PyExc_ImportError, "uname() failed"); return NULL; } int major = 0, minor = 0; sscanf(uts.release, "%d.%d", &major, &minor); if (!(major > 5 || (major == 5 && minor >= 6))) { PyErr_Format( PyExc_ImportError, "linux_uring requires Linux 5.6+ (IORING_OP_READ/WRITE), " "current kernel is %s", uts.release ); return NULL; } /* Quick probe: ensure io_uring syscall is available */ struct io_uring_params probe; memset(&probe, 0, sizeof(probe)); int probe_fd = io_uring_setup(1, &probe); if (probe_fd < 0) { int saved_errno = errno; if (saved_errno == ENOSYS) { /* ENOSYS most commonly means a seccomp filter is blocking the * io_uring syscalls (the default Docker/Podman/OrbStack profile * blocks them). Less commonly it means an older kernel that * pre-dates io_uring, which the version check above should have * already caught. */ PyErr_Format( PyExc_ImportError, "io_uring_setup failed: syscall not available (ENOSYS). " "This usually means a seccomp filter is blocking io_uring. " "To fix:\n" " Docker/Podman : add --security-opt seccomp=unconfined\n" " Kubernetes : set securityContext.seccompProfile.type=Unconfined\n" " OrbStack : use a plain Linux VM instead of a container context\n" "Kernel: %s", uts.release ); } else { PyErr_Format( PyExc_ImportError, "io_uring_setup probe failed: %s", strerror(saved_errno) ); } return NULL; } close(probe_fd); /* Probe whether SQPOLL is actually usable (kernel + capabilities) - * exposed as SQPOLL_ALLOWED so callers can pick a default without * constructing a real Context just to find out. Context(sqpoll=True) * already falls back gracefully on its own either way (see * flag_table_sqpoll) - this is purely informational. */ struct io_uring_params sqpoll_probe; memset(&sqpoll_probe, 0, sizeof(sqpoll_probe)); sqpoll_probe.flags = IORING_SETUP_SQPOLL; sqpoll_probe.sq_thread_idle = 100; int sqpoll_probe_fd = io_uring_setup(1, &sqpoll_probe); int sqpoll_allowed = sqpoll_probe_fd >= 0; if (sqpoll_probe_fd >= 0) close(sqpoll_probe_fd); PyObject *m = PyModule_Create(&linux_uring_module); if (m == NULL) return NULL; if (CAIO_DECLARE_FREE_THREADED(m) < 0) { Py_DECREF(m); return NULL; } if (PyModule_AddObject(m, "SQPOLL_ALLOWED", PyBool_FromLong(sqpoll_allowed)) < 0) { Py_DECREF(m); return NULL; } if (PyType_Ready(&AIOContextType) < 0) return NULL; if (PyType_Ready(&AIOOperationType) < 0) return NULL; Py_INCREF(&AIOContextType); if (PyModule_AddObject(m, "Context", (PyObject *) &AIOContextType) < 0) { Py_DECREF(&AIOContextType); Py_DECREF(m); return NULL; } Py_INCREF(&AIOOperationType); if (PyModule_AddObject(m, "Operation", (PyObject *) &AIOOperationType) < 0) { Py_DECREF(&AIOOperationType); Py_DECREF(m); return NULL; } return m; } caio-0.12.3/caio/linux_uring.pyi000066400000000000000000000033111523515553700165220ustar00rootroot00000000000000from collections.abc import Callable from typing import Any from .abstract import AbstractContext, AbstractOperation # True if this kernel/process can actually negotiate IORING_SETUP_SQPOLL - # probed once at import time (see linux_uring.c's PyInit_linux_uring). SQPOLL_ALLOWED: bool # noinspection PyPropertyDefinition class Context(AbstractContext): def __init__(self, max_requests: int = 32, sqpoll: bool = False): ... @property def fileno(self) -> int: ... def poll(self) -> int: ... def process_events( self, max_requests: int = 512, min_requests: int = 0, timeout: int = 0, ) -> int: ... def flush(self) -> int: ... @property def sqpoll(self) -> bool: ... # noinspection PyPropertyDefinition class Operation(AbstractOperation): @classmethod def read( cls, nbytes: int, fd: int, offset: int, priority: int = 0, ) -> Operation: ... @classmethod def write( cls, payload_bytes: bytes, fd: int, offset: int, priority: int = 0, ) -> Operation: ... @classmethod def fsync(cls, fd: int, priority: int = 0) -> Operation: ... @classmethod def fdsync(cls, fd: int, priority: int = 0) -> Operation: ... def get_value(self) -> bytes | int | None: ... def set_callback(self, callback: Callable[[int], Any]) -> bool: ... @property def fileno(self) -> int: ... @property def offset(self) -> int: ... @property def payload(self) -> bytes | memoryview | None: ... @property def nbytes(self) -> int: ... @property def result(self) -> int: ... @property def error(self) -> int: ... @property def context(self) -> AbstractContext | None: ... caio-0.12.3/caio/linux_uring_asyncio.py000066400000000000000000000070611523515553700201040ustar00rootroot00000000000000from .asyncio_base import AsyncioContextBase from .linux_uring import SQPOLL_ALLOWED, Context, Operation class AsyncioContext(AsyncioContextBase): OPERATION_CLASS = Operation CONTEXT_CLASS = Context def _create_context(self, max_requests, **kwargs): # SQPOLL_ALLOWED reflects a real kernel/capability probe done once # at import time (see linux_uring.c) - default to it rather than # to sqpoll=False, but let an explicit caller kwarg win either way. kwargs.setdefault("sqpoll", SQPOLL_ALLOWED) context = super()._create_context(max_requests, **kwargs) self.loop.add_reader(context.fileno, self._on_read_event) self._flush_scheduled = False return context def _on_done(self, future, result): if future.done(): return future.set_result(True) def _destroy_context(self): self.loop.remove_reader(self.context.fileno) def _on_submitted(self): # Non-SQPOLL flush() completes page-cache ops inline (drain_cq fires # futures before `await future` even suspends); SQPOLL just wakes # the kernel thread if it's asleep - and only when it's actually # gone idle, so eager per-op flush() is already close to syscall- # free there. Batching doesn't reduce a cost that's mostly already # gone - measured no meaningful difference either way - so ignore # deferred whenever the kernel actually negotiated SQPOLL (not # just what was requested - EPERM/EINVAL can silently fall back to # a plain ring, see linux_uring.c's flag_table_sqpoll). if not self.deferred or self.context.sqpoll: self.context.flush() return # Nothing between submit()'s isinstance check and here ever awaits, # so N concurrently-scheduled submits used to each flush their own # single SQE back to back - no batching despite N being ready at # once. call_soon() defers to the next _run_once() pass, after all # of them have written their SQE, so one flush() covers the whole # batch - at the cost of one extra event-loop round-trip for a lone # unbatched op. if not self._flush_scheduled: self._flush_scheduled = True self.loop.call_soon(self._deferred_flush) def _deferred_flush(self): self._flush_scheduled = False self.context.flush() def _on_read_event(self): """Handle completions signalled via the eventfd.""" # poll() raises BlockingIOError whenever the eventfd's counter # reads as zero - possible even right after epoll reported this fd # readable (e.g. the kernel coalesces multiple completions into one # counter increment, and the ones beyond the first can still be # landing in the CQ ring at the moment poll() runs). Letting that # exception escape used to skip process_events() entirely for this # wakeup - draining is independent of the eventfd count (it reads # the CQ ring directly), so a stale/absent counter is never a # reason to skip it, only silently stranding whatever was already # completed with no other event left to wake this Context up # again. Confirmed to actually hang a real (non-tmpfs) benchmark # run this way - inline tmpfs completions never exercise this path # at all, since they never reach the eventfd in the first place. try: self.context.poll() except BlockingIOError: pass while self.context.process_events(): pass caio-0.12.3/caio/py.typed000066400000000000000000000000001523515553700151230ustar00rootroot00000000000000caio-0.12.3/caio/python_aio.py000066400000000000000000000335601523515553700161700ustar00rootroot00000000000000import operator import os import sys import threading from collections.abc import Callable from enum import IntEnum, unique from multiprocessing.pool import ThreadPool from threading import Lock, RLock from types import MappingProxyType from typing import Any from weakref import WeakValueDictionary from .abstract import AbstractContext, AbstractOperation fdsync = getattr(os, "fdatasync", os.fsync) NATIVE_PREAD_PWRITE = hasattr(os, "pread") and hasattr(os, "pwrite") @unique class OpCode(IntEnum): READ = 0 WRITE = 1 FSYNC = 2 FDSYNC = 3 NOOP = -1 @unique class ContextState(IntEnum): OPEN = 0 CLOSING = 1 CLOSED = 2 class Context(AbstractContext): """ python aio context implementation """ MAX_POOL_SIZE = 128 def __init__(self, max_requests: int = 32, pool_size: int = 8): # Set before any validation that can raise: __del__ runs even on a # partially-constructed object, and close() below relies on _state # existing and being non-OPEN to safely no-op before ever touching # _lock/pool, which aren't created until validation passes. self._state = ContextState.CLOSED if not (0 < pool_size < self.MAX_POOL_SIZE): raise ValueError( f"pool_size must be between 1 and {self.MAX_POOL_SIZE - 1}, " f"got {pool_size}", ) if max_requests <= 0: raise ValueError( f"max_requests must be a positive integer, got {max_requests}", ) self.__max_requests = max_requests self.pool = ThreadPool(pool_size) self._in_progress = 0 self._lock = Lock() self._state = ContextState.OPEN if not NATIVE_PREAD_PWRITE: self._locks_cleaner = Lock() self._locks: WeakValueDictionary = WeakValueDictionary() @property def max_requests(self) -> int: return self.__max_requests @staticmethod def _invoke_callback(operation: "Operation", value): """ Calls ``operation``'s callback with ``value``, if one was set via ``set_callback()``. A missing callback is a normal, expected case. A raising callback must not escape - this runs on ThreadPool's single internal result-handler thread, and an uncaught exception there kills that thread, silently stalling every future result/callback for the rest of this Context's lifetime. """ with operation._lock: callback = operation.callback if callback is None: return try: callback(value) except BaseException: # noqa: BLE001 (must isolate any callback exception, including SystemExit/KeyboardInterrupt) threading.excepthook( threading.ExceptHookArgs( (*sys.exc_info(), threading.current_thread()), ), ) def _release_slot(self): with self._lock: self._in_progress -= 1 def _rollback_claim(self, operation: "Operation"): """ Undoes a claim that was never actually scheduled (e.g. a concurrent close() tore down the pool between the capacity reservation and apply_async()) - unlike genuine completion, this must reset operation.in_progress, since the operation never actually ran and must stay retryable. """ with operation._lock, self._lock: operation.in_progress = False self._in_progress -= 1 def _execute(self, operation: "Operation") -> bool: """ Returns True if actually scheduled, False if skipped because ``operation`` was already in progress - matches the other three backends, which all silently skip (rather than raise on, or dispatch twice) a resubmit of an Operation still in flight. """ handler = self._OP_MAP[operation.opcode] # operation.in_progress is deliberately NOT reset on genuine # completion below - one-shot forever once actually scheduled, # matching all three native backends: a completed Operation must # not be resubmittable, only a fresh one constructed for a retry. def on_error(exc): self._release_slot() operation.exception = exc operation.written = 0 self._invoke_callback(operation, None) def on_success(result): self._release_slot() operation.written = result self._invoke_callback(operation, result) # operation.in_progress is the Operation's own lock, not this # Context's - two different Contexts submitting the same Operation # only ever share the Operation, never a Context, so a per-Context # lock alone can't stop them both from claiming it. with operation._lock, self._lock: if operation.in_progress: return False if self._state != ContextState.OPEN: raise RuntimeError("Context is closed") if self._in_progress >= self.__max_requests: raise RuntimeError( "Maximum simultaneous requests have been reached", ) operation.in_progress = True self._in_progress += 1 try: self.pool.apply_async( handler, args=(self, operation), callback=on_success, error_callback=on_error, ) except BaseException: # Scheduling itself failed (e.g. a concurrent close() already # tore down the pool) - the slot reserved above was never # actually claimed by a real job, so it must be given back # instead of permanently inflating _in_progress. self._rollback_claim(operation) raise return True if NATIVE_PREAD_PWRITE: def __pread(self, fd, size, offset): return os.pread(fd, size, offset) def __pwrite(self, fd, bytes, offset): return os.pwrite(fd, bytes, offset) else: def __fd_lock(self, fd): # Plain get-or-create under _locks_cleaner - two threads racing # on the same fd's first access must get back the same RLock, # or their lseek()+read()/write() pairs can interleave. The # WeakValueDictionary itself only keeps an fd's entry alive # while some __pread()/__pwrite() call still holds a strong ref # to it (the `with` block below) - otherwise this Context would # accumulate one RLock per fd it's ever touched, forever. with self._locks_cleaner: lock = self._locks.get(fd) if lock is None: lock = self._locks[fd] = RLock() return lock def __pread(self, fd, size, offset): with self.__fd_lock(fd): os.lseek(fd, 0, os.SEEK_SET) os.lseek(fd, offset, os.SEEK_SET) return os.read(fd, size) def __pwrite(self, fd, bytes, offset): with self.__fd_lock(fd): os.lseek(fd, 0, os.SEEK_SET) os.lseek(fd, offset, os.SEEK_SET) return os.write(fd, bytes) def _handle_read(self, operation: "Operation"): # Stored directly, not copied through a BytesIO - pread() already # returns exactly the bytes object get_value()/payload need to hand # back, and buffering it through BytesIO.write() just to unwrap it # again later cost a full extra copy per read for no benefit. data = self.__pread( operation.fileno, operation.nbytes, operation.offset, ) operation.buffer = data return len(data) def _handle_write(self, operation: "Operation"): # operation.buffer is the caller's own payload bytes, unwrapped - # no BytesIO round-trip needed to hand it to pwrite() either. return self.__pwrite( operation.fileno, operation.buffer, operation.offset, ) def _handle_fsync(self, operation: "Operation"): return os.fsync(operation.fileno) def _handle_fdsync(self, operation: "Operation"): return fdsync(operation.fileno) def _handle_noop(self, operation: "Operation"): return def submit(self, *aio_operations) -> int: for operation in aio_operations: if not isinstance(operation, Operation): raise ValueError(f"Invalid Operation {operation!r}") # noqa: TRY004 (pre-existing public exception type, not changing it here) count = 0 for operation in aio_operations: if self._execute(operation): count += 1 return count def cancel(self, *aio_operations) -> int: """ Cancels multiple Operations. Returns Operation.cancel(aio_op1, aio_op2, aio_opN, ...) -> int (Always returns zero, this method exists for compatibility reasons) """ return 0 def close(self): if self._state != ContextState.OPEN: return with self._lock: if self._state != ContextState.OPEN: return self._state = ContextState.CLOSING self.pool.close() self._state = ContextState.CLOSED def __del__(self): self.close() _OP_MAP = MappingProxyType({ OpCode.READ: _handle_read, OpCode.WRITE: _handle_write, OpCode.FSYNC: _handle_fsync, OpCode.FDSYNC: _handle_fdsync, OpCode.NOOP: _handle_noop, }) # noinspection PyPropertyDefinition class Operation(AbstractOperation): """ python aio operation implementation """ def __init__( self, fd: int, nbytes: int | None, offset: int | None, opcode: OpCode, payload: bytes | None = None, priority: int | None = None, ): # Validated eagerly, at construction time - matching the other 3 # backends, which reject a non-int-like fd/nbytes/offset/priority # (or a non-bytes write payload) synchronously via # PyArg_ParseTupleAndKeywords/PyBytes_Check, rather than storing it # and failing later inside a worker thread. operator.index() is the # same __index__-based coercion PyArg_ParseTupleAndKeywords' "I"/"K" # format codes use internally, so this accepts exactly what the C # constructors accept (plain ints, numpy-style int-likes, ...) and # rejects exactly what they reject (str, float, ...). fd = operator.index(fd) if nbytes is not None: nbytes = operator.index(nbytes) if offset is not None: offset = operator.index(offset) if priority is not None: priority = operator.index(priority) # Plain bytes, not a BytesIO wrapper - for a write this is the # caller's own payload, handed to pwrite() as-is; for a read it # starts empty and _handle_read() replaces it with pread()'s # result directly. Either way there's nothing to unwrap later, so # get_value()/payload return it with no extra copy - and it's # never None, so callers don't need to check. if opcode == OpCode.WRITE: if not isinstance(payload, bytes): raise ValueError(f"payload_bytes must be bytes, got {payload!r}") buffer = payload else: buffer = b"" self.callback: Callable[[int], Any] | None = None self.in_progress = False self._lock = Lock() self.buffer: bytes = buffer self.opcode = opcode self.__fileno = fd self.__offset = offset or 0 self.__opcode = opcode self.__nbytes = nbytes or 0 self.__priority = priority or 0 self.exception = None self.written = 0 @classmethod def read( cls, nbytes: int, fd: int, offset: int, priority=0, ) -> "Operation": """ Creates a new instance of Operation on read mode. """ return cls(fd, nbytes, offset, opcode=OpCode.READ, priority=priority) @classmethod def write( cls, payload_bytes: bytes, fd: int, offset: int, priority=0, ) -> "Operation": """ Creates a new instance of AIOOperation on write mode. """ return cls( fd, len(payload_bytes), offset, payload=payload_bytes, opcode=OpCode.WRITE, priority=priority, ) @classmethod def fsync(cls, fd: int, priority=0) -> "Operation": """ Creates a new instance of AIOOperation on fsync mode. """ return cls(fd, None, None, opcode=OpCode.FSYNC, priority=priority) @classmethod def fdsync(cls, fd: int, priority=0) -> "Operation": """ Creates a new instance of AIOOperation on fdsync mode. """ return cls(fd, None, None, opcode=OpCode.FDSYNC, priority=priority) def get_value(self) -> bytes | int | None: """ Method returns a bytes value of AIOOperation's result or None. """ if self.exception: raise self.exception if self.opcode == OpCode.WRITE: return self.written if self.opcode in (OpCode.FSYNC, OpCode.FDSYNC): return None return self.buffer @property def fileno(self) -> int: return self.__fileno @property def offset(self) -> int: return self.__offset @property def payload(self) -> memoryview | None: return memoryview(self.buffer) @property def nbytes(self) -> int: return self.__nbytes def set_callback(self, callback: Callable[[int], Any]) -> bool: if not callable(callback): raise ValueError(f"callback must be callable, got {callback!r}") # noqa: TRY004 (pre-existing public exception type, not changing it here) with self._lock: self.callback = callback return True caio-0.12.3/caio/python_aio_asyncio.py000066400000000000000000000003761523515553700177140ustar00rootroot00000000000000from .asyncio_base import AsyncioContextBase from .python_aio import Context, Operation class AsyncioContext(AsyncioContextBase): OPERATION_CLASS = Operation CONTEXT_CLASS = Context def _destroy_context(self): self.context.close() caio-0.12.3/caio/src/000077500000000000000000000000001523515553700142255ustar00rootroot00000000000000caio-0.12.3/caio/src/threadpool/000077500000000000000000000000001523515553700163665ustar00rootroot00000000000000caio-0.12.3/caio/src/threadpool/LICENSE000066400000000000000000000024361523515553700174000ustar00rootroot00000000000000Copyright (c) 2016, Mathias Brossard. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. caio-0.12.3/caio/src/threadpool/LINK000066400000000000000000000000501523515553700170410ustar00rootroot00000000000000https://github.com/mbrossard/threadpool caio-0.12.3/caio/src/threadpool/README.md000066400000000000000000000016001523515553700176420ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/mbrossard/threadpool.svg?branch=master)](https://travis-ci.org/mbrossard/threadpool) A simple C thread pool implementation ===================================== Currently, the implementation: * Works with pthreads only, but API is intentionally opaque to allow other implementations (Windows for instance). * Starts all threads on creation of the thread pool. * Reserves one task for signaling the queue is full. * Stops and joins all worker threads on destroy. Possible enhancements ===================== The API contains addtional unused 'flags' parameters that would allow some additional options: * Lazy creation of threads (easy) * Reduce number of threads automatically (hard) * Unlimited queue size (medium) * Kill worker threads on destroy (hard, dangerous) * Support Windows API (medium) * Reduce locking contention (medium/hard) caio-0.12.3/caio/src/threadpool/threadpool.c000066400000000000000000000204421523515553700206750ustar00rootroot00000000000000/* * Copyright (c) 2016, Mathias Brossard . * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @file threadpool.c * @brief Threadpool implementation file */ #include #include #include #include "threadpool.h" typedef enum { immediate_shutdown = 1, graceful_shutdown = 2 } threadpool_shutdown_t; /** * @struct threadpool_task * @brief the work struct * * @var function Pointer to the function that will perform the task. * @var argument Argument to be passed to the function. */ typedef struct { void (*function)(void *); void *argument; } threadpool_task_t; /** * @struct threadpool * @brief The threadpool struct * * @var notify Condition variable to notify worker threads. * @var threads Array containing worker threads ID. * @var thread_count Number of threads * @var queue Array containing the task queue. * @var queue_size Size of the task queue. * @var head Index of the first element. * @var tail Index of the next element. * @var count Number of pending tasks * @var shutdown Flag indicating if the pool is shutting down * @var started Number of started threads */ struct threadpool_t { pthread_mutex_t lock; pthread_cond_t notify; pthread_t *threads; threadpool_task_t *queue; int thread_count; int queue_size; int head; int tail; int count; int shutdown; int started; }; /** * @function void *threadpool_thread(void *threadpool) * @brief the worker thread * @param threadpool the pool which own the thread */ static void *threadpool_thread(void *threadpool); int threadpool_free(threadpool_t *pool); threadpool_t *threadpool_create(int thread_count, int queue_size, int flags) { threadpool_t *pool; int i; (void) flags; if(thread_count <= 0 || thread_count > MAX_THREADS || queue_size <= 0 || queue_size > MAX_QUEUE) { return NULL; } if((pool = (threadpool_t *)malloc(sizeof(threadpool_t))) == NULL) { goto err; } /* Initialize */ pool->thread_count = 0; pool->queue_size = queue_size; pool->head = pool->tail = pool->count = 0; pool->shutdown = pool->started = 0; /* Allocate thread and task queue */ pool->threads = (pthread_t *)malloc(sizeof(pthread_t) * thread_count); pool->queue = (threadpool_task_t *)malloc (sizeof(threadpool_task_t) * queue_size); /* Initialize mutex and conditional variable first */ if((pthread_mutex_init(&(pool->lock), NULL) != 0) || (pthread_cond_init(&(pool->notify), NULL) != 0) || (pool->threads == NULL) || (pool->queue == NULL)) { goto err; } /* Start worker threads */ for(i = 0; i < thread_count; i++) { if(pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void*)pool) != 0) { threadpool_destroy(pool, 0); return NULL; } pool->thread_count++; pool->started++; } return pool; err: if(pool) { threadpool_free(pool); } return NULL; } int threadpool_add(threadpool_t *pool, void (*function)(void *), void *argument, int flags) { int err = 0; int next; (void) flags; if(pool == NULL || function == NULL) { return threadpool_invalid; } if(pthread_mutex_lock(&(pool->lock)) != 0) { return threadpool_lock_failure; } next = (pool->tail + 1) % pool->queue_size; do { /* Are we full ? */ if(pool->count == pool->queue_size) { err = threadpool_queue_full; break; } /* Are we shutting down ? */ if(pool->shutdown) { err = threadpool_shutdown; break; } /* Add task to queue */ pool->queue[pool->tail].function = function; pool->queue[pool->tail].argument = argument; pool->tail = next; pool->count += 1; /* pthread_cond_broadcast */ if(pthread_cond_signal(&(pool->notify)) != 0) { err = threadpool_lock_failure; break; } } while(0); if(pthread_mutex_unlock(&pool->lock) != 0) { err = threadpool_lock_failure; } return err; } int threadpool_destroy(threadpool_t *pool, int flags) { int i, err = 0; if(pool == NULL) { return threadpool_invalid; } if(pthread_mutex_lock(&(pool->lock)) != 0) { return threadpool_lock_failure; } do { /* Already shutting down */ if(pool->shutdown) { err = threadpool_shutdown; break; } pool->shutdown = (flags & threadpool_graceful) ? graceful_shutdown : immediate_shutdown; /* Wake up all worker threads */ if((pthread_cond_broadcast(&(pool->notify)) != 0) || (pthread_mutex_unlock(&(pool->lock)) != 0)) { err = threadpool_lock_failure; break; } /* Join all worker thread */ for(i = 0; i < pool->thread_count; i++) { if(pthread_join(pool->threads[i], NULL) != 0) { err = threadpool_thread_failure; } } } while(0); /* Only if everything went well do we deallocate the pool */ if(!err) { threadpool_free(pool); } return err; } int threadpool_free(threadpool_t *pool) { if(pool == NULL || pool->started > 0) { return -1; } /* Did we manage to allocate ? */ if(pool->threads) { free(pool->threads); free(pool->queue); /* Because we allocate pool->threads after initializing the mutex and condition variable, we're sure they're initialized. Let's lock the mutex just in case. */ pthread_mutex_lock(&(pool->lock)); pthread_mutex_destroy(&(pool->lock)); pthread_cond_destroy(&(pool->notify)); } free(pool); return 0; } static void *threadpool_thread(void *threadpool) { threadpool_t *pool = (threadpool_t *)threadpool; threadpool_task_t task; for(;;) { /* Lock must be taken to wait on conditional variable */ pthread_mutex_lock(&(pool->lock)); /* Wait on condition variable, check for spurious wakeups. When returning from pthread_cond_wait(), we own the lock. */ while((pool->count == 0) && (!pool->shutdown)) { pthread_cond_wait(&(pool->notify), &(pool->lock)); } if((pool->shutdown == immediate_shutdown) || ((pool->shutdown == graceful_shutdown) && (pool->count == 0))) { break; } /* Grab our task */ task.function = pool->queue[pool->head].function; task.argument = pool->queue[pool->head].argument; pool->head = (pool->head + 1) % pool->queue_size; pool->count -= 1; /* Unlock */ pthread_mutex_unlock(&(pool->lock)); /* Get to work */ (*(task.function))(task.argument); } pool->started--; pthread_mutex_unlock(&(pool->lock)); pthread_exit(NULL); return(NULL); } caio-0.12.3/caio/src/threadpool/threadpool.h000066400000000000000000000064441523515553700207100ustar00rootroot00000000000000/* * Copyright (c) 2016, Mathias Brossard . * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef _THREADPOOL_H_ #define _THREADPOOL_H_ #ifdef __cplusplus extern "C" { #endif /** * @file threadpool.h * @brief Threadpool Header File */ /** * Increase this constants at your own risk * Large values might slow down your system */ #define MAX_THREADS 128 #define MAX_QUEUE 65536 typedef struct threadpool_t threadpool_t; typedef enum { threadpool_invalid = -1, threadpool_lock_failure = -2, threadpool_queue_full = -3, threadpool_shutdown = -4, threadpool_thread_failure = -5 } threadpool_error_t; typedef enum { threadpool_graceful = 1 } threadpool_destroy_flags_t; /** * @function threadpool_create * @brief Creates a threadpool_t object. * @param thread_count Number of worker threads. * @param queue_size Size of the queue. * @param flags Unused parameter. * @return a newly created thread pool or NULL */ threadpool_t *threadpool_create(int thread_count, int queue_size, int flags); /** * @function threadpool_add * @brief add a new task in the queue of a thread pool * @param pool Thread pool to which add the task. * @param function Pointer to the function that will perform the task. * @param argument Argument to be passed to the function. * @param flags Unused parameter. * @return 0 if all goes well, negative values in case of error (@see * threadpool_error_t for codes). */ int threadpool_add(threadpool_t *pool, void (*routine)(void *), void *arg, int flags); /** * @function threadpool_destroy * @brief Stops and destroys a thread pool. * @param pool Thread pool to destroy. * @param flags Flags for shutdown * * Known values for flags are 0 (default) and threadpool_graceful in * which case the thread pool doesn't accept any new tasks but * processes all pending tasks before shutdown. */ int threadpool_destroy(threadpool_t *pool, int flags); #ifdef __cplusplus } #endif #endif /* _THREADPOOL_H_ */ caio-0.12.3/caio/thread_aio.c000066400000000000000000000700201523515553700157000ustar00rootroot00000000000000#include #include #include #define PY_SSIZE_T_CLEAN #include #include #if PY_VERSION_HEX >= 0x030D0000 && defined(Py_GIL_DISABLED) #define CAIO_BEGIN_CRITICAL_SECTION(object) \ PyCriticalSection caio_critical_section; \ PyCriticalSection_Begin( \ &caio_critical_section, (PyObject *)(object) \ ) #define CAIO_END_CRITICAL_SECTION() \ PyCriticalSection_End(&caio_critical_section) #else #define CAIO_BEGIN_CRITICAL_SECTION(object) #define CAIO_END_CRITICAL_SECTION() #endif #define CAIO_ATOMIC_LOAD(value) \ __atomic_load_n(&(value), __ATOMIC_ACQUIRE) #define CAIO_ATOMIC_STORE(value, new_value) \ __atomic_store_n(&(value), (new_value), __ATOMIC_RELEASE) #define CAIO_ATOMIC_LOAD_STORE(value, new_value) \ __atomic_exchange_n(&(value), (new_value), __ATOMIC_ACQ_REL) #if PY_VERSION_HEX >= 0x030D0000 && defined(Py_GIL_DISABLED) #define CAIO_DECLARE_FREE_THREADED(module) \ PyUnstable_Module_SetGIL((module), Py_MOD_GIL_NOT_USED) #else #define CAIO_DECLARE_FREE_THREADED(module) 0 #endif #include "src/threadpool/threadpool.h" static const unsigned CTX_POOL_SIZE_DEFAULT = 8; static const unsigned CTX_MAX_REQUESTS_DEFAULT = 512; static PyTypeObject AIOOperationType; static PyTypeObject AIOContextType; typedef struct { PyObject_HEAD threadpool_t* pool; uint16_t max_requests; uint8_t pool_size; PyObject* weakreflist; } AIOContext; typedef struct { PyObject_HEAD PyObject* py_buffer; PyObject* callback; int opcode; unsigned int fileno; off_t offset; int result; uint8_t error; uint8_t in_progress; uint8_t done; /* genuine completion reached - set via a release * store by worker() before it ever touches the GIL, * and read via an acquire load from payload/ * get_value() (both always GIL-held); this ordering * is what makes result/buf_size/error - written by * worker() on its own thread, without the GIL - * safe to read once done is observed true. Distinct * from in_progress, which is sticky forever and * guards resubmission instead. */ Py_ssize_t buf_size; char* buf; PyObject* ctx; PyObject* weakreflist; } AIOOperation; enum THAIO_OP_CODE { THAIO_READ, THAIO_WRITE, THAIO_FSYNC, THAIO_FDSYNC, THAIO_NOOP, }; static PyObject *AIOOperation_callback_ref(AIOOperation *self) { PyObject *callback; CAIO_BEGIN_CRITICAL_SECTION(self); callback = Py_XNewRef(self->callback); CAIO_END_CRITICAL_SECTION(); return callback; } /* * Stops the thread pool - shared by close() and dealloc(). * * The pointer swap shares a critical section with submit()'s dispatch loop * (see there): a no-op under the GIL (already serialized), a real * per-object lock under free-threading. threadpool_destroy() runs outside * that section - self->pool is already NULL by then, so a concurrent * submit() bails immediately instead of waiting on the teardown below. */ static void AIOContext_close_pool(AIOContext *self) { threadpool_t* pool; CAIO_BEGIN_CRITICAL_SECTION(self); pool = self->pool; self->pool = NULL; CAIO_END_CRITICAL_SECTION(); if (pool == NULL) return; // Graceful: queued tasks still run instead of leaking their // Py_INCREF'd references. GIL released - workers need it back to run // their callbacks, so holding it here while joining would deadlock. Py_BEGIN_ALLOW_THREADS threadpool_destroy(pool, threadpool_graceful); Py_END_ALLOW_THREADS } static void AIOContext_dealloc(AIOContext *self) { if (self->weakreflist != NULL) PyObject_ClearWeakRefs((PyObject *) self); AIOContext_close_pool(self); Py_TYPE(self)->tp_free((PyObject *) self); } /* AIOContext.__new__ classmethod definition */ static PyObject * AIOContext_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { AIOContext *self; self = (AIOContext *) type->tp_alloc(type, 0); return (PyObject *) self; } static int AIOContext_init(AIOContext *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = {"max_requests", "pool_size", NULL}; self->pool = NULL; self->max_requests = 0; if (!PyArg_ParseTupleAndKeywords( args, kwds, "|HH", kwlist, &self->max_requests, &self->pool_size )) return -1; if (self->max_requests <= 0) { self->max_requests = CTX_MAX_REQUESTS_DEFAULT; } if (self->pool_size <= 0) { self->pool_size = CTX_POOL_SIZE_DEFAULT; } if (self->pool_size > MAX_THREADS) { PyErr_Format( PyExc_ValueError, "pool_size too large. Allowed lower then %d", MAX_THREADS ); return -1; } if (self->max_requests >= (MAX_QUEUE - 1)) { PyErr_Format( PyExc_ValueError, "max_requests too large. Allowed lower then %d", MAX_QUEUE - 1 ); return -1; } self->pool = threadpool_create(self->pool_size, self->max_requests, 0); if (self->pool == NULL) { PyErr_Format( PyExc_RuntimeError, "Pool initialization failed size=%d max_requests=%d", self->pool_size, self->max_requests ); return -1; } return 0; } static PyObject* AIOContext_repr(AIOContext *self) { if (self->pool == NULL) { PyErr_SetString(PyExc_RuntimeError, "Pool not initialized"); return NULL; } return PyUnicode_FromFormat( "<%s as %p: max_requests=%i, pool_size=%i, ctx=%lli>", Py_TYPE(self)->tp_name, self, self->max_requests, self->pool_size, self->pool ); } void worker(void *arg) { PyGILState_STATE state; AIOOperation* op = arg; PyObject* ctx = op->ctx; op->ctx = NULL; op->error = 0; if (op->opcode == THAIO_NOOP) { state = PyGILState_Ensure(); op->ctx = NULL; Py_DECREF(ctx); Py_DECREF(op); PyGILState_Release(state); return; } int fileno = op->fileno; off_t offset = op->offset; int buf_size = op->buf_size; char* buf = op->buf; int result; switch (op->opcode) { case THAIO_WRITE: result = pwrite(fileno, (const char*) buf, buf_size, offset); break; case THAIO_FSYNC: result = fsync(fileno); break; case THAIO_FDSYNC: #ifdef HAVE_FDATASYNC result = fdatasync(fileno); #else result = fsync(fileno); #endif break; case THAIO_READ: result = pread(fileno, buf, buf_size, offset); break; } op->ctx = NULL; op->result = result; if (result < 0) op->error = errno; if (op->opcode == THAIO_READ) { op->buf_size = result; } state = PyGILState_Ensure(); if (op->opcode == THAIO_WRITE) { Py_CLEAR(op->py_buffer); } /* Publish completion only after every result field and Python-owned * buffer transition is complete. payload/get_value() acquire-load done, * so a reader that observes true must also observe all writes above. */ CAIO_ATOMIC_STORE(op->done, 1); PyObject *callback = AIOOperation_callback_ref(op); if (callback != NULL) { PyObject *rv = PyObject_CallFunction(callback, "i", result); if (rv == NULL) { PyErr_WriteUnraisable(callback); } else { Py_DECREF(rv); } Py_DECREF(callback); } Py_DECREF(ctx); Py_DECREF(op); PyGILState_Release(state); } inline int process_pool_error(int code) { switch (code) { case threadpool_invalid: PyErr_SetString( PyExc_RuntimeError, "Thread pool pointer is invalid" ); return code; case threadpool_lock_failure: PyErr_SetString( PyExc_RuntimeError, "Failed to lock thread pool" ); return code; case threadpool_queue_full: PyErr_Format( PyExc_RuntimeError, "Thread pool queue full" ); return code; case threadpool_shutdown: PyErr_SetString( PyExc_RuntimeError, "Thread pool is shutdown" ); return code; case threadpool_thread_failure: PyErr_SetString( PyExc_RuntimeError, "Thread failure" ); return code; } if (code < 0) PyErr_SetString(PyExc_RuntimeError, "Unknown error"); return code; } PyDoc_STRVAR(AIOContext_submit_docstring, "Accepts multiple Operations. Returns \n\n" " Operation.submit(aio_op1, aio_op2, aio_opN, ...) -> int" ); static PyObject* AIOContext_submit( AIOContext *self, PyObject *args ) { if (self == NULL) { PyErr_SetString(PyExc_RuntimeError, "self is NULL"); return NULL; } if (!PyTuple_Check(args)) { PyErr_SetNone(PyExc_ValueError); return NULL; } Py_ssize_t nr = PyTuple_Size(args); Py_ssize_t i; PyObject* obj; // Heap-allocated rather than a stack VLA sized by the caller-controlled // tuple length - *ops(*a_huge_tuple) must not be able to overflow the // stack. AIOOperation** ops = NULL; if (nr > 0) { ops = PyMem_New(AIOOperation*, nr); if (ops == NULL) { PyErr_NoMemory(); return NULL; } } for (i=0; i < nr; i++) { obj = PyTuple_GetItem(args, i); if (PyObject_TypeCheck(obj, &AIOOperationType) == 0) { PyErr_Format( PyExc_TypeError, "Wrong type for argument %zd", i ); PyMem_Free(ops); return NULL; } ops[i] = (AIOOperation*) obj; } Py_ssize_t j=0; int result = 0; int failed = 0; // Shares AIOContext_close_pool()'s critical section (see there) - must // cover the whole loop, not just the read, or a concurrent close() // could destroy `pool` between the check and threadpool_add(). CAIO_BEGIN_CRITICAL_SECTION(self); threadpool_t* pool = self->pool; if (pool == NULL) { PyErr_SetString(PyExc_RuntimeError, "self->pool is NULL"); failed = 1; } else { for (i=0; i < nr; i++) { // Atomic exchange, not check-then-set: two Contexts racing on // the same Operation must not both dispatch it to a worker. if (CAIO_ATOMIC_LOAD_STORE(ops[i]->in_progress, 1)) continue; ops[i]->ctx = (void*) self; Py_INCREF(ops[i]); Py_INCREF(self); result = threadpool_add(pool, worker, (void*) ops[i], 0); if (process_pool_error(result) < 0) { CAIO_ATOMIC_STORE(ops[i]->in_progress, 0); ops[i]->ctx = NULL; Py_DECREF(ops[i]); Py_DECREF(self); failed = 1; break; } j++; } } CAIO_END_CRITICAL_SECTION(); PyMem_Free(ops); if (failed) return NULL; return (PyObject*) PyLong_FromSsize_t(j); } PyDoc_STRVAR(AIOContext_cancel_docstring, "Cancels multiple Operations. Returns \n\n" " Operation.cancel(aio_op1, aio_op2, aio_opN, ...) -> int\n\n" "(Always returns zero, this method exists for compatibility reasons)" ); static PyObject* AIOContext_cancel( AIOContext *self, PyObject *args ) { return (PyObject*) PyLong_FromSsize_t(0); } PyDoc_STRVAR(AIOContext_close_docstring, "Stops the native thread pool. Idempotent - a second call is a no-op.\n\n" "Graceful: any Operation already running or still queued gets to run " "to completion first. submit() after close() raises RuntimeError." ); static PyObject* AIOContext_close( AIOContext *self, PyObject *Py_UNUSED(ignored) ) { AIOContext_close_pool(self); Py_RETURN_NONE; } /* AIOContext properties */ static PyMemberDef AIOContext_members[] = { { "pool_size", T_INT, offsetof(AIOContext, pool_size), READONLY, "pool_size" }, { "max_requests", T_USHORT, offsetof(AIOContext, max_requests), READONLY, "max requests" }, {NULL} /* Sentinel */ }; static PyMethodDef AIOContext_methods[] = { { "submit", (PyCFunction) AIOContext_submit, METH_VARARGS, AIOContext_submit_docstring }, { "cancel", (PyCFunction) AIOContext_cancel, METH_VARARGS, AIOContext_cancel_docstring }, { "close", (PyCFunction) AIOContext_close, METH_NOARGS, AIOContext_close_docstring }, {NULL} /* Sentinel */ }; static PyTypeObject AIOContextType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "Context", .tp_doc = "thread aio context", .tp_basicsize = sizeof(AIOContext), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_new = AIOContext_new, .tp_init = (initproc) AIOContext_init, .tp_dealloc = (destructor) AIOContext_dealloc, .tp_members = AIOContext_members, .tp_methods = AIOContext_methods, .tp_repr = (reprfunc) AIOContext_repr, .tp_weaklistoffset = offsetof(AIOContext, weakreflist) }; static int AIOOperation_traverse(AIOOperation *self, visitproc visit, void *arg) { Py_VISIT(self->callback); Py_VISIT(self->py_buffer); Py_VISIT(self->ctx); return 0; } static int AIOOperation_clear(AIOOperation *self) { Py_CLEAR(self->callback); /* self->buf is a separate PyMem_Calloc allocation py_buffer's own * memoryview only views, not owns - clearing py_buffer alone would * leak it (and, if this runs before this Operation's own eventual * dealloc, that dealloc's identical free-then-NULL below prevents a * double free only because this already set it to NULL). */ if ((self->opcode == THAIO_READ) && self->buf != NULL) { PyMem_Free(self->buf); self->buf = NULL; } Py_CLEAR(self->py_buffer); /* Normally already NULL by any point tp_clear/dealloc could run - * worker() clears it before ever touching the GIL again - but that's * only true once genuinely dispatched; cleared here too in case a * cyclic collection runs during the narrow submit()-to-dispatch * window where it's still set. */ Py_CLEAR(self->ctx); return 0; } static void AIOOperation_dealloc(AIOOperation *self) { PyObject_GC_UnTrack(self); if (self->weakreflist != NULL) PyObject_ClearWeakRefs((PyObject *) self); AIOOperation_clear(self); Py_TYPE(self)->tp_free((PyObject *) self); } static PyObject* AIOOperation_repr(AIOOperation *self) { char* mode; switch (self->opcode) { case THAIO_READ: mode = "read"; break; case THAIO_WRITE: mode = "write"; break; case THAIO_FSYNC: mode = "fsync"; break; case THAIO_FDSYNC: mode = "fdsync"; break; default: mode = "noop"; break; } return PyUnicode_FromFormat( "<%s at %p: mode=\"%s\", fd=%i, offset=%i, result=%i, buffer=%p>", Py_TYPE(self)->tp_name, self, mode, self->fileno, self->offset, self->result, self->buf ); } /* AIOOperation.read classmethod definition */ PyDoc_STRVAR(AIOOperation_read_docstring, "Creates a new instance of Operation on read mode.\n\n" " Operation.read(\n" " nbytes: int,\n" " aio_context: Context,\n" " fd: int, \n" " offset: int,\n" " priority=0\n" " )" ); static PyObject* AIOOperation_read( PyTypeObject *type, PyObject *args, PyObject *kwds ) { AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); static char *kwlist[] = {"nbytes", "fd", "offset", "priority", NULL}; if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "can not allocate memory"); return NULL; } self->buf = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->weakreflist = NULL; uint64_t nbytes = 0; uint16_t priority; int argIsOk = PyArg_ParseTupleAndKeywords( args, kwds, "KI|LH", kwlist, &nbytes, &(self->fileno), &(self->offset), &priority ); if (!argIsOk) { Py_DECREF(self); return NULL; } // PyMem_Calloc can return NULL for a large enough (or just // OOM-at-the-time) nbytes - proceeding with a NULL buf would hand the // kernel (via pread() in worker()) and PyMemoryView_FromMemory a NULL // pointer with a nonzero declared size, corrupting memory instead of // raising a catchable error. self->buf = PyMem_Calloc(nbytes, sizeof(char)); if (self->buf == NULL && nbytes > 0) { Py_DECREF(self); PyErr_NoMemory(); return NULL; } self->buf_size = nbytes; self->py_buffer = PyMemoryView_FromMemory( self->buf, self->buf_size, PyBUF_READ ); if (self->py_buffer == NULL) { Py_DECREF(self); return NULL; } self->opcode = THAIO_READ; return (PyObject*) self; } /* AIOOperation.write classmethod definition */ PyDoc_STRVAR(AIOOperation_write_docstring, "Creates a new instance of Operation on write mode.\n\n" " Operation.write(\n" " payload_bytes: bytes,\n" " fd: int, \n" " offset: int,\n" " priority=0\n" " )" ); static PyObject* AIOOperation_write( PyTypeObject *type, PyObject *args, PyObject *kwds ) { AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); static char *kwlist[] = {"payload_bytes", "fd", "offset", "priority", NULL}; if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "can not allocate memory"); return NULL; } // unused uint16_t priority; self->buf = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->weakreflist = NULL; // Parsed into a plain local first, not directly into self->py_buffer: // "O" hands back a borrowed reference, and self->py_buffer must never // hold one - AIOOperation_dealloc unconditionally Py_CLEARs it. Only // assigned (and incref'd) below once it's confirmed to actually be the // bytes object this Operation is going to own. PyObject* payload_bytes = NULL; int argIsOk = PyArg_ParseTupleAndKeywords( args, kwds, "OI|LH", kwlist, &payload_bytes, &(self->fileno), &(self->offset), &priority ); if (!argIsOk) { Py_DECREF(self); return NULL; } if (!PyBytes_Check(payload_bytes)) { Py_DECREF(self); PyErr_SetString( PyExc_ValueError, "payload_bytes argument must be bytes" ); return NULL; } self->opcode = THAIO_WRITE; if (PyBytes_AsStringAndSize( payload_bytes, &self->buf, &self->buf_size )) { Py_DECREF(self); PyErr_SetString( PyExc_RuntimeError, "Can not convert bytes to c string" ); return NULL; } self->py_buffer = payload_bytes; Py_INCREF(self->py_buffer); return (PyObject*) self; } /* AIOOperation.fsync classmethod definition */ PyDoc_STRVAR(AIOOperation_fsync_docstring, "Creates a new instance of Operation on fsync mode.\n\n" " Operation.fsync(\n" " aio_context: AIOContext,\n" " fd: int, \n" " priority=0\n" " )" ); static PyObject* AIOOperation_fsync( PyTypeObject *type, PyObject *args, PyObject *kwds ) { AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); static char *kwlist[] = {"fd", "priority", NULL}; if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "can not allocate memory"); return NULL; } uint16_t priority; self->buf = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->weakreflist = NULL; int argIsOk = PyArg_ParseTupleAndKeywords( args, kwds, "I|H", kwlist, &(self->fileno), &priority ); if (!argIsOk) { Py_DECREF(self); return NULL; } self->opcode = THAIO_FSYNC; return (PyObject*) self; } /* AIOOperation.fdsync classmethod definition */ PyDoc_STRVAR(AIOOperation_fdsync_docstring, "Creates a new instance of Operation on fdsync mode.\n\n" " Operation.fdsync(\n" " aio_context: AIOContext,\n" " fd: int, \n" " priority=0\n" " )" ); static PyObject* AIOOperation_fdsync( PyTypeObject *type, PyObject *args, PyObject *kwds ) { AIOOperation *self = (AIOOperation *) type->tp_alloc(type, 0); static char *kwlist[] = {"fd", "priority", NULL}; if (self == NULL) { PyErr_SetString(PyExc_MemoryError, "can not allocate memory"); return NULL; } self->buf = NULL; self->py_buffer = NULL; self->in_progress = 0; self->done = 0; self->weakreflist = NULL; uint16_t priority; int argIsOk = PyArg_ParseTupleAndKeywords( args, kwds, "I|H", kwlist, &(self->fileno), &priority ); if (!argIsOk) { Py_DECREF(self); return NULL; } self->opcode = THAIO_FDSYNC; return (PyObject*) self; } /* AIOOperation.get_value method definition */ PyDoc_STRVAR(AIOOperation_get_value_docstring, "Method returns a bytes value of Operation's result or None.\n\n" " Operation.get_value() -> Optional[bytes]" ); static PyObject* AIOOperation_get_value( AIOOperation *self, PyObject *args, PyObject *kwds ) { if ( CAIO_ATOMIC_LOAD(self->in_progress) && !CAIO_ATOMIC_LOAD(self->done) ) { PyErr_SetString( PyExc_RuntimeError, "get_value() is not available while the operation is in flight" ); return NULL; } if (self->error != 0) { PyErr_SetString( PyExc_SystemError, strerror(self->error) ); return NULL; } switch (self->opcode) { case THAIO_READ: /* self->buf can only be NULL here if tp_clear() already ran - * only possible once this Operation is otherwise unreachable * (see AIOOperation_traverse/_clear), so nothing could * actually be waiting on this return value, but degrade to * None rather than hand out an uninitialized buffer either * way (PyBytes_FromStringAndSize(NULL, n>0) doesn't crash, * it silently returns garbage). */ if (self->buf == NULL) Py_RETURN_NONE; return PyBytes_FromStringAndSize( self->buf, self->buf_size ); case THAIO_WRITE: return PyLong_FromSsize_t(self->result); } Py_RETURN_NONE; } /* AIOOperation.get_value method definition */ PyDoc_STRVAR(AIOOperation_set_callback_docstring, "Set callback which will be called after Operation will be finished.\n\n" " Operation.get_value() -> Optional[bytes]" ); static PyObject* AIOOperation_set_callback( AIOOperation *self, PyObject *args, PyObject *kwds ) { static char *kwlist[] = {"callback", NULL}; PyObject* callback; PyObject* old_callback; int argIsOk = PyArg_ParseTupleAndKeywords( args, kwds, "O", kwlist, &callback ); if (!argIsOk) return NULL; if (!PyCallable_Check(callback)) { PyErr_Format( PyExc_ValueError, "object %r is not callable", callback ); return NULL; } Py_INCREF(callback); CAIO_BEGIN_CRITICAL_SECTION(self); old_callback = self->callback; self->callback = callback; CAIO_END_CRITICAL_SECTION(); Py_XDECREF(old_callback); Py_RETURN_TRUE; } static PyObject *AIOOperation_payload_getter(AIOOperation *self, void *closure) { if ( CAIO_ATOMIC_LOAD(self->in_progress) && !CAIO_ATOMIC_LOAD(self->done) ) { PyErr_SetString( PyExc_RuntimeError, "payload is not available while the operation is in flight" ); return NULL; } /* fsync/fdsync Operations never allocate a buffer, and a completed * write's is freed in worker() before done is published (i.e. before * this getter could ever observe it) - matches T_OBJECT's (as opposed * to T_OBJECT_EX's) own NULL-to-None behavior, which this getter * replaces. */ if (self->py_buffer == NULL) Py_RETURN_NONE; Py_INCREF(self->py_buffer); return self->py_buffer; } static PyGetSetDef AIOOperation_getset[] = { { "payload", (getter) AIOOperation_payload_getter, NULL, "payload", NULL }, {NULL} }; /* AIOOperation properties */ static PyMemberDef AIOOperation_members[] = { { "fileno", T_UINT, offsetof(AIOOperation, fileno), READONLY, "file descriptor" }, { "offset", T_ULONGLONG, offsetof(AIOOperation, offset), READONLY, "offset" }, { "nbytes", T_ULONGLONG, offsetof(AIOOperation, buf_size), READONLY, "nbytes" }, { "result", T_INT, offsetof(AIOOperation, result), READONLY, "result" }, { "error", T_INT, offsetof(AIOOperation, error), READONLY, "error" }, {NULL} /* Sentinel */ }; /* AIOOperation methods */ static PyMethodDef AIOOperation_methods[] = { { "read", (PyCFunction) AIOOperation_read, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_read_docstring }, { "write", (PyCFunction) AIOOperation_write, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_write_docstring }, { "fsync", (PyCFunction) AIOOperation_fsync, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_fsync_docstring }, { "fdsync", (PyCFunction) AIOOperation_fdsync, METH_CLASS | METH_VARARGS | METH_KEYWORDS, AIOOperation_fdsync_docstring }, { "get_value", (PyCFunction) AIOOperation_get_value, METH_NOARGS, AIOOperation_get_value_docstring }, { "set_callback", (PyCFunction) AIOOperation_set_callback, METH_VARARGS | METH_KEYWORDS, AIOOperation_set_callback_docstring }, {NULL} /* Sentinel */ }; /* AIOOperation class */ static PyTypeObject AIOOperationType = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "aio.AIOOperation", .tp_doc = "thread aio operation representation", .tp_basicsize = sizeof(AIOOperation), .tp_itemsize = 0, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, .tp_dealloc = (destructor) AIOOperation_dealloc, .tp_traverse = (traverseproc) AIOOperation_traverse, .tp_clear = (inquiry) AIOOperation_clear, .tp_members = AIOOperation_members, .tp_getset = AIOOperation_getset, .tp_methods = AIOOperation_methods, .tp_repr = (reprfunc) AIOOperation_repr, .tp_weaklistoffset = offsetof(AIOOperation, weakreflist) }; static PyModuleDef thread_aio_module = { PyModuleDef_HEAD_INIT, .m_name = "thread_aio", .m_doc = "Thread based AIO.", .m_size = -1, }; PyMODINIT_FUNC PyInit_thread_aio(void) { Py_Initialize(); PyObject *m; m = PyModule_Create(&thread_aio_module); if (m == NULL) return NULL; if (CAIO_DECLARE_FREE_THREADED(m) < 0) { Py_DECREF(m); return NULL; } if (PyType_Ready(&AIOContextType) < 0) return NULL; Py_INCREF(&AIOContextType); if (PyModule_AddObject(m, "Context", (PyObject *) &AIOContextType) < 0) { Py_XDECREF(&AIOContextType); Py_XDECREF(m); return NULL; } if (PyType_Ready(&AIOOperationType) < 0) return NULL; Py_INCREF(&AIOOperationType); if (PyModule_AddObject(m, "Operation", (PyObject *) &AIOOperationType) < 0) { Py_XDECREF(&AIOOperationType); Py_XDECREF(m); return NULL; } return m; } caio-0.12.3/caio/thread_aio.pyi000066400000000000000000000024001523515553700162540ustar00rootroot00000000000000from collections.abc import Callable from typing import Any from .abstract import AbstractContext, AbstractOperation # noinspection PyPropertyDefinition class Context(AbstractContext): def __init__(self, max_requests: int = 512, pool_size=8): ... @property def pool_size(self) -> int: ... def close(self) -> None: ... # noinspection PyPropertyDefinition class Operation(AbstractOperation): @classmethod def read( cls, nbytes: int, fd: int, offset: int, priority=0 ) -> AbstractOperation: ... @classmethod def write( cls, payload_bytes: bytes, fd: int, offset: int, priority=0, ) -> AbstractOperation: ... @classmethod def fsync(cls, fd: int, priority=0) -> AbstractOperation: ... @classmethod def fdsync(cls, fd: int, priority=0) -> AbstractOperation: ... def get_value(self) -> bytes | int | None: ... @property def fileno(self) -> int: ... @property def offset(self) -> int: ... @property def payload(self) -> bytes | memoryview | None: ... @property def nbytes(self) -> int: ... @property def result(self) -> int: ... @property def error(self) -> int: ... def set_callback(self, callback: Callable[[int], Any]) -> bool: ... caio-0.12.3/caio/thread_aio_asyncio.py000066400000000000000000000004351523515553700176360ustar00rootroot00000000000000from .asyncio_base import AsyncioContextBase from .thread_aio import Context, Operation class AsyncioContext(AsyncioContextBase): MAX_REQUESTS_DEFAULT = 512 OPERATION_CLASS = Operation CONTEXT_CLASS = Context def _destroy_context(self): self.context.close() caio-0.12.3/example.py000066400000000000000000000013671523515553700145370ustar00rootroot00000000000000import asyncio from caio import AsyncioContext loop = asyncio.get_event_loop() async def main(): # max_requests=128 by default ctx = AsyncioContext(max_requests=128) with open("test.file", "wb+") as fp: fd = fp.fileno() # Execute one write operation await ctx.write(b"Hello world", fd, offset=0) # Execute one read operation print(await ctx.read(32, fd, offset=0)) # Execute one fdsync operation await ctx.fdsync(fd) op1 = ctx.write(b"Hello from ", fd, offset=0) op2 = ctx.write(b"async world", fd, offset=11) await asyncio.gather(op1, op2) print(await ctx.read(32, fd, offset=0)) # Hello from async world loop.run_until_complete(main()) caio-0.12.3/pyproject.toml000066400000000000000000000027551523515553700154500ustar00rootroot00000000000000[build-system] requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] name = "caio" version = "0.10.2" license = "Apache-2.0" license-files = ["COPYING"] description = "Asynchronous file IO for Linux MacOS or Windows." readme = "README.md" authors = [{ name = "Dmitry Orlov", email = "me@mosquito.su"}] requires-python = ">=3.10" classifiers = [ "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Microsoft", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Free Threading :: 2 - Beta", ] [project.urls] "Source Code" = "https://github.com/mosquito/caio/" [project.optional-dependencies] develop = [ "aiomisc-pytest", "coveralls", "pytest", "pytest-cov", "pytest-rerunfailures", "setuptools", ] [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" [tool.setuptools.packages.find] include = ["caio*"] [tool.ruff] line-length = 80 [tool.ruff.lint] ignore = ["C901", "E252"] [dependency-groups] dev = [ "rich", ] caio-0.12.3/scripts/000077500000000000000000000000001523515553700142125ustar00rootroot00000000000000caio-0.12.3/scripts/make-wheels.sh000066400000000000000000000007461523515553700167570ustar00rootroot00000000000000set -ex mkdir -p dist MACHINE=$(/opt/python/cp311-cp311/bin/python3 -c 'import platform; print(platform.machine())') function build_wheel() { /opt/python/$1/bin/pip install build /opt/python/$1/bin/python -m build --wheel } build_wheel cp310-cp310 build_wheel cp311-cp311 build_wheel cp312-cp312 build_wheel cp313-cp313 build_wheel cp314-cp314 build_wheel cp314-cp314t cd dist for f in ./*-linux*_${MACHINE}*; do if [ -f $f ]; then auditwheel repair $f -w . ; rm $f; fi; done caio-0.12.3/setup.py000066400000000000000000000023651523515553700142430ustar00rootroot00000000000000import platform from setuptools import Extension, setup module_name = "caio" OS_NAME = platform.system().lower() extensions = [] if "linux" in OS_NAME: extensions.append( Extension( "{}.thread_aio".format(module_name), [ "{}/thread_aio.c".format(module_name), "{}/src/threadpool/threadpool.c".format(module_name), ], extra_compile_args=["-g", "-DHAVE_FDATASYNC"], ), ) elif "darwin" in OS_NAME: extensions.append( Extension( "{}.thread_aio".format(module_name), [ "{}/thread_aio.c".format(module_name), "{}/src/threadpool/threadpool.c".format(module_name), ], extra_compile_args=["-g"], ), ) if "linux" in OS_NAME: extensions.append( Extension( "{}.linux_aio".format(module_name), ["{}/linux_aio.c".format(module_name)], extra_compile_args=["-g"], ), ) extensions.append( Extension( "{}.linux_uring".format(module_name), ["{}/linux_uring.c".format(module_name)], extra_compile_args=["-g"], ), ) setup( ext_modules=extensions, ) caio-0.12.3/tests/000077500000000000000000000000001523515553700136655ustar00rootroot00000000000000caio-0.12.3/tests/conftest.py000066400000000000000000000172631523515553700160750ustar00rootroot00000000000000import functools import threading import time import types import pytest from caio import ( linux_aio, linux_aio_asyncio, linux_uring, linux_uring_asyncio, python_aio, thread_aio, variants, variants_asyncio, ) class ConcurrentThreads: """Run test workers and surface their failures in the main thread.""" def __init__(self, timeout=30): self.timeout = timeout self._barriers = [] self._threads = [] self._errors = [] self._errors_lock = threading.Lock() def barrier(self, parties): barrier = threading.Barrier(parties, timeout=self.timeout) self._barriers.append(barrier) return barrier def start(self, target, *args): thread = threading.Thread( target=self._run, args=(target, args), name=f"{target.__name__}-{len(self._threads)}", ) self._threads.append(thread) thread.start() return thread def start_many(self, target, arguments): for args in arguments: self.start(target, *args) def join(self, timeout=None): deadline = time.monotonic() + ( self.timeout if timeout is None else timeout ) for thread in self._threads: thread.join(max(0, deadline - time.monotonic())) alive = [thread.name for thread in self._threads if thread.is_alive()] assert not alive, f"worker threads did not stop: {', '.join(alive)}" if self._errors: raise self._errors[0] def close(self): for barrier in self._barriers: barrier.abort() for thread in self._threads: thread.join(timeout=5) def _run(self, target, args): try: target(*args) except BaseException as exc: # noqa: BLE001 (surface child-thread failures) with self._errors_lock: self._errors.append(exc) for barrier in self._barriers: barrier.abort() @pytest.fixture def workers(): threads = ConcurrentThreads() yield threads threads.close() @pytest.fixture def submit_and_wait(): def submit(context, operations, result_for=None, timeout=10): operations = tuple(operations) count = len(operations) results = [None] * count remaining = [count] errors = [] lock = threading.Lock() done = threading.Event() if result_for is None: result_for = lambda _operation, result: result def make_callback(index, operation): def callback(result): with lock: try: results[index] = result_for(operation, result) except BaseException as exc: # noqa: BLE001 (surface callback failures) errors.append(exc) finally: remaining[0] -= 1 if remaining[0] == 0: done.set() return callback for index, operation in enumerate(operations): operation.set_callback(make_callback(index, operation)) assert context.submit(operation) == 1 assert done.wait(timeout), ( f"only {count - remaining[0]}/{count} operations completed" ) if errors: raise errors[0] return results return submit def named_variant(name, **attrs): ns = types.SimpleNamespace(__name__=name, **attrs) return ns # Test-only sqpoll/deferred variants - not part of caio's public API. extra_context_variants = [] extra_asyncio_variants = [] if linux_uring is not None: extra_context_variants.append(named_variant( "linux_uring[sqpoll=True]", Context=functools.partial(linux_uring.Context, sqpoll=True), Operation=linux_uring.Operation, )) extra_asyncio_variants.append(named_variant( "linux_uring[sqpoll=True]", AsyncioContext=functools.partial(linux_uring_asyncio.AsyncioContext, sqpoll=True), )) # sqpoll=False forced: SQPOLL_ALLOWED's default would otherwise mask # the deferred-batching path this variant exists to cover. extra_asyncio_variants.append(named_variant( "linux_uring[deferred=True]", AsyncioContext=functools.partial( linux_uring_asyncio.AsyncioContext, sqpoll=False, deferred=True, ), )) if linux_aio is not None: extra_asyncio_variants.append(named_variant( "linux_aio[deferred=True]", AsyncioContext=functools.partial(linux_aio_asyncio.AsyncioContext, deferred=True), )) all_variants = variants + tuple(extra_context_variants) all_variants_asyncio = variants_asyncio + tuple(extra_asyncio_variants) # extra_context_variants are always linux_uring, always polling-capable - # added directly rather than via hasattr() (doesn't see through partial). polling_variants = [ v for v in variants if hasattr(v.Context, "process_events") ] + extra_context_variants # Only thread_aio/python_aio have a worker-pool queue distinct from # "accepted" - max_requests/pool_size are independent there. pooled_variants = tuple(v for v in variants if v in (thread_aio, python_aio)) @pytest.fixture(params=pooled_variants) def pooled_backend(request): return request.param def drain(ctx, want, timeout=5.0): """Waits for `want` completions via whatever polling API the backend exposes, without busy-spinning (a tight CPU-bound retry loop can starve a backend's own background thread/kernel thread of scheduling time) and without silently giving up (raises on timeout instead).""" total = 0 if hasattr(ctx, "flush"): total += ctx.flush() deadline = time.monotonic() + timeout while total < want: if time.monotonic() > deadline: raise TimeoutError(f"drain: only {total}/{want} completions after {timeout}s") time.sleep(0.001) total += ctx.process_events() return total def wait_until(ctx, predicate, timeout=5.0): """Like drain(), but works across every backend uniformly, including the two purely callback-driven ones (thread_aio/python_aio, whose Context has no flush()/process_events() at all - results just arrive on a background worker thread on their own). Polls an arbitrary predicate (e.g. `lambda: len(results) >= n`) instead of a completion count, pumping the context each iteration only for backends that actually need pumping to make progress.""" deadline = time.monotonic() + timeout while not predicate(): if time.monotonic() > deadline: raise TimeoutError(f"condition not met within {timeout}s") if hasattr(ctx, "flush"): ctx.flush() if hasattr(ctx, "process_events"): ctx.process_events() time.sleep(0.001) return True @pytest.fixture(params=all_variants) def context_maker(request): return request.param.Context @pytest.fixture(params=all_variants) def operation_maker(request): return request.param.Operation @pytest.fixture(params=all_variants) def backend(request): # Keeps Context/Operation paired from the same backend, unlike # context_maker+operation_maker's independent cross product. return request.param @pytest.fixture(params=all_variants_asyncio) def async_context_maker(request): return request.param.AsyncioContext @pytest.fixture(params=all_variants_asyncio) async def async_context(request): """Ready, already-entered AsyncioContext, default args. Use async_context_maker instead for a non-default constructor kwarg.""" async with request.param.AsyncioContext() as context: yield context @pytest.fixture(params=polling_variants) def polling_backend(request): return request.param caio-0.12.3/tests/test_aio_context.py000066400000000000000000000046151523515553700176200ustar00rootroot00000000000000import tempfile import pytest def test_aio_context(context_maker): ctx = context_maker() assert ctx is not None ctx = context_maker(1) assert ctx is not None ctx = context_maker(32218) assert ctx is not None def test_context_repr_does_not_crash(context_maker): ctx = context_maker() assert isinstance(repr(ctx), str) def test_operation_repr_does_not_crash(backend): with tempfile.NamedTemporaryFile() as f: fd = f.fileno() ops = [ backend.Operation.read(16, fd, 0), backend.Operation.write(b"hello", fd, 0), backend.Operation.fsync(fd), backend.Operation.fdsync(fd), ] for op in ops: assert isinstance(repr(op), str) def test_write_operation_properties_before_submission(backend): """.fileno/.offset/.nbytes/.payload for a freshly constructed write Operation must reflect the constructor arguments identically across every backend, even before it's ever submitted - unlike read (see below), nothing here depends on backend-specific buffer allocation strategy.""" with tempfile.NamedTemporaryFile() as f: fd = f.fileno() payload = b"the quick brown fox" op = backend.Operation.write(payload, fd, 7) assert op.fileno == fd assert op.offset == 7 assert op.nbytes == len(payload) assert bytes(op.payload) == payload def test_read_operation_nbytes_before_submission(backend): """Only .nbytes is guaranteed consistent for an unsubmitted read across backends - .payload's pre-submission content is an implementation detail (some backends pre-allocate a same-size buffer, python_aio starts with an empty BytesIO instead), so it's deliberately not asserted here.""" with tempfile.NamedTemporaryFile() as f: fd = f.fileno() op = backend.Operation.read(16, fd, 3) assert op.fileno == fd assert op.offset == 3 assert op.nbytes == 16 @pytest.mark.parametrize("ctor_name", ["fsync", "fdsync"]) def test_sync_operation_properties(backend, ctor_name): with tempfile.NamedTemporaryFile() as f: fd = f.fileno() op = getattr(backend.Operation, ctor_name)(fd) assert op.fileno == fd assert op.offset == 0 assert op.nbytes == 0 # Some backends return None, others an empty buffer - both falsy. assert not op.payload caio-0.12.3/tests/test_asyncio_adapter.py000066400000000000000000000240261523515553700204470ustar00rootroot00000000000000import asyncio import hashlib import os from unittest.mock import Mock import aiomisc import pytest @aiomisc.timeout(5) async def test_linux_uring_asyncio_forwards_context_kwargs(): uring_asyncio = pytest.importorskip("caio.linux_uring_asyncio") async with uring_asyncio.AsyncioContext( max_requests=8, sqpoll=True, deferred=True, ) as context: # Unsupported kernels/permissions may make the C context fall back # to a regular ring. Reaching this point proves that the asyncio # adapter forwarded the backend-specific option instead of rejecting # it in _create_context(). assert context.context.sqpoll in (0, 1) assert context.deferred is True @aiomisc.timeout(5) async def test_adapter(tmp_path, async_context): context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() assert await context.read(32, fd, 0) == b"" s = b"Hello world" assert await context.write(s, fd, 0) == len(s) assert await context.read(32, fd, 0) == s s = b"Hello real world" assert await context.write(s, fd, 0) == len(s) assert await context.read(32, fd, 0) == s part = b"\x00\x01\x02\x03" limit = 32 expected_hash = hashlib.md5(part * limit).hexdigest() await asyncio.gather( *[context.write(part, fd, len(part) * i) for i in range(limit)] ) await context.fdsync(fd) data = await context.read(limit * len(part), fd, 0) assert data == part * limit assert hashlib.md5(bytes(data)).hexdigest() == expected_hash @aiomisc.timeout(3) async def test_bad_file_descritor(tmp_path, async_context): context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() with pytest.raises((SystemError, OSError, AssertionError, ValueError)): assert await context.read(1, fd, 0) == b"" with pytest.raises((SystemError, OSError, AssertionError, ValueError)): assert await context.write(b"hello", fd, 0) @pytest.fixture async def asyncio_exception_handler(): handler = Mock( side_effect=lambda _loop, ctx: _loop.default_exception_handler(ctx) ) event_loop = asyncio.get_running_loop() current_handler = event_loop.get_exception_handler() event_loop.set_exception_handler(handler=handler) yield handler event_loop.set_exception_handler(current_handler) @aiomisc.timeout(3) async def test_operations_cancel_cleanly( tmp_path, async_context, asyncio_exception_handler ): context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() await context.write(b"\x00", fd, 1024**2 - 1) assert os.stat(fd).st_size == 1024**2 for _ in range(50): reads = [ asyncio.create_task(context.read(2**16, fd, 2**16 * i)) for i in range(16) ] _, pending = await asyncio.wait( reads, return_when=asyncio.FIRST_COMPLETED ) for read in pending: read.cancel() if pending: await asyncio.wait(pending) asyncio_exception_handler.assert_not_called() @aiomisc.timeout(3) async def test_write_operations_cancel_cleanly( tmp_path, async_context, asyncio_exception_handler ): """Mirrors test_operations_cancel_cleanly but for writes - cancellation handling shouldn't be read-specific.""" context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() for _ in range(50): writes = [ asyncio.create_task( context.write(b"\x01" * 2**16, fd, 2**16 * i), ) for i in range(16) ] _, pending = await asyncio.wait( writes, return_when=asyncio.FIRST_COMPLETED ) for write in pending: write.cancel() if pending: await asyncio.wait(pending) asyncio_exception_handler.assert_not_called() @aiomisc.timeout(3) async def test_cancel_before_first_step_runs(tmp_path, async_context, asyncio_exception_handler): """Cancelling right after the op's own first step (submit queued, still suspended at `await future`) - covers context.cancel() raising ValueError for an op the backend never actually got to submit to the kernel yet.""" context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 fd = fp.fileno() task = asyncio.ensure_future(context.write(b"x", fd, 0)) await asyncio.sleep(0) task.cancel() with pytest.raises(asyncio.CancelledError): await task asyncio_exception_handler.assert_not_called() @aiomisc.timeout(5) async def test_zero_byte_read_and_write(tmp_path, async_context): context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() assert await context.write(b"", fd, 0) == 0 assert await context.read(0, fd, 0) == b"" @aiomisc.timeout(5) async def test_partial_read_at_eof(tmp_path, async_context): """Requesting more bytes than the file actually has must return exactly what's there, not garbage/padding out to the requested size - this exercises the "kernel filled less than the whole buffer" slow path that the fast, no-copy path (used when the buffer is filled completely) doesn't.""" context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() payload = b"hello" assert await context.write(payload, fd, 0) == len(payload) data = await context.read(len(payload) * 20, fd, 0) assert data == payload assert len(data) == len(payload) @aiomisc.timeout(5) async def test_fsync_and_fdsync(tmp_path, async_context): context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() await context.write(b"data", fd, 0) # Return value not asserted - None on some backends, b"" on others # (python_aio), both meaningless here. await context.fsync(fd) await context.fdsync(fd) @aiomisc.timeout(15) async def test_large_transfer(tmp_path, async_context): context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() payload = os.urandom(4 * 1024 * 1024) expected_hash = hashlib.sha256(payload).hexdigest() written = await context.write(payload, fd, 0) assert written == len(payload) data = await context.read(len(payload), fd, 0) assert len(data) == len(payload) assert hashlib.sha256(bytes(data)).hexdigest() == expected_hash @aiomisc.timeout(5) async def test_write_extends_file_sparsely(tmp_path, async_context): context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() hole_size = 4 * 1024 * 1024 await context.write(b"\x01", fd, hole_size) assert os.stat(fd).st_size == hole_size + 1 hole = await context.read(hole_size, fd, 0) assert hole == b"\x00" * hole_size @aiomisc.timeout(10) async def test_max_requests_backpressure(tmp_path, async_context_maker): """A tiny max_requests must still let far more concurrent operations complete correctly - the asyncio-level semaphore is responsible for never handing the backend more in-flight operations than it was configured for, so this must not surface a "queue full"/"ring full" error from the backend itself.""" async with async_context_maker(max_requests=2) as context: with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() chunk = 4096 count = 40 expected = [bytes([i % 256]) * chunk for i in range(count)] await asyncio.gather( *[ context.write(expected[i], fd, i * chunk) for i in range(count) ] ) results = await asyncio.gather( *[context.read(chunk, fd, i * chunk) for i in range(count)] ) assert results == expected @aiomisc.timeout(10) async def test_concurrent_non_overlapping_chunks(tmp_path, async_context): """Writes distinct, non-overlapping regions concurrently, then reads them back concurrently - if any backend's buffer handling ever aliased two in-flight operations' memory (e.g. a zero-copy fast path reused somewhere it shouldn't), this would show up as data from the wrong chunk appearing in the wrong place.""" context = async_context with open(str(tmp_path / "temp.bin"), "wb+") as fp: # noqa: ASYNC230 (brief sync setup, not the operation under test) fd = fp.fileno() chunk = 8192 count = 32 expected = [ bytes([(i * 7 + 3) % 256]) * chunk for i in range(count) ] await asyncio.gather( *[ context.write(expected[i], fd, i * chunk) for i in range(count) ] ) results = await asyncio.gather( *[context.read(chunk, fd, i * chunk) for i in range(count)] ) for i, (got, want) in enumerate(zip(results, expected)): assert got == want, f"chunk {i} mismatch" caio-0.12.3/tests/test_free_threading.py000066400000000000000000000575271523515553700202640ustar00rootroot00000000000000import gc import os import select import sys import sysconfig import threading import time import weakref import pytest import caio from caio import python_aio # os.open() defaults to text mode on Windows without this - \n <-> \r\n # translation would corrupt any payload containing a raw \n byte. _O_BINARY = getattr(os, "O_BINARY", 0) def test_importing_caio_does_not_enable_gil(): """Every importable C backend must declare and support free threading.""" if not sysconfig.get_config_var("Py_GIL_DISABLED"): pytest.skip("not a free-threaded build") assert sys._is_gil_enabled() is False, ( "importing caio enabled the GIL; at least one C extension has not " "declared free-threading support" ) def test_high_concurrency_stress_no_data_races(tmp_path, submit_and_wait): """Hammers python_aio's own bookkeeping (the _lock-protected _in_progress counter and per-operation in_progress flag) with many concurrent writes then reads dispatched across a real multi-worker ThreadPool. Under a free-threaded interpreter these workers can run truly in parallel instead of being serialized by the GIL, so a race in that bookkeeping - two workers claiming the same slot, a result crossing over to the wrong Operation - would show up here as a lost write or a chunk read back with the wrong content, not just a thread-timing fluke.""" count = 200 chunk = 4096 path = tmp_path / "stress.bin" path.write_bytes(b"\x00" * (count * chunk)) fd = os.open(str(path), os.O_RDWR | _O_BINARY) ctx = None try: ctx = python_aio.Context(max_requests=count, pool_size=32) expected = [bytes([i % 256]) * chunk for i in range(count)] writes = ( python_aio.Operation.write(payload, fd, index * chunk) for index, payload in enumerate(expected) ) written = submit_and_wait(ctx, writes) assert written == [chunk] * count reads = ( python_aio.Operation.read(chunk, fd, index * chunk) for index in range(count) ) read_back = submit_and_wait( ctx, reads, result_for=lambda operation, _result: operation.get_value(), ) assert read_back == expected finally: os.close(fd) if ctx is not None: ctx.close() def test_same_operation_is_claimed_once_across_contexts(workers): """An Operation's one-shot claim must be synchronized by the Operation. Context._execute() currently protects ``operation.in_progress`` with the Context's lock. That only serializes submissions through the *same* Context: two Contexts use two unrelated locks and can both observe False before either stores True when the GIL is disabled. Synchronizing each pair of submissions makes the race frequent enough to be a useful regression test without depending on filesystem timing. """ if getattr(sys, "_is_gil_enabled", lambda: True)(): pytest.skip("requires a free-threaded interpreter with the GIL disabled") iterations = 20_000 contexts = ( python_aio.Context(max_requests=iterations * 2), python_aio.Context(max_requests=iterations * 2), ) start = workers.barrier(3) finished = workers.barrier(3) current = [None] submitted = [0, 0] def submitter(index, context): for _ in range(iterations): start.wait() submitted[index] += context.submit(current[0]) finished.wait() try: workers.start_many(submitter, enumerate(contexts)) for _ in range(iterations): current[0] = python_aio.Operation( 0, None, None, python_aio.OpCode.NOOP, ) start.wait() finished.wait() workers.join() assert sum(submitted) == iterations, ( f"{sum(submitted) - iterations} Operations were submitted twice" ) finally: workers.close() for context in contexts: context.close() context.pool.join() def _require_gil_disabled(): if getattr(sys, "_is_gil_enabled", lambda: True)(): pytest.skip("requires a free-threaded interpreter with the GIL disabled") def _pump_contexts(contexts, predicate, timeout=10): deadline = time.monotonic() + timeout while not predicate(): if time.monotonic() >= deadline: raise TimeoutError("contexts did not complete in time") for context in contexts: if hasattr(context, "flush"): context.flush() for context in contexts: if hasattr(context, "process_events"): context.process_events() time.sleep(0.001) def _close_contexts(contexts): for context in contexts: close = getattr(context, "close", None) if close is not None: close() for context in contexts: pool = getattr(context, "pool", None) if pool is not None: pool.join() @pytest.fixture( params=[2, 4, 8, 16, 32], ids=lambda value: f"submitters={value}", ) def ft_submitter_count(request): return request.param @pytest.fixture(params=[1, 3, 17, 64], ids=lambda value: f"batch={value}") def ft_operations_per_submit(request): return request.param @pytest.fixture(params=[1, 8], ids=lambda value: f"submit-rounds={value}") def ft_submit_rounds(request): return request.param @pytest.fixture(params=[2, 4, 8], ids=lambda value: f"claimants={value}") def ft_claimant_count(request): return request.param @pytest.fixture( params=[257, 4_096, 20_000], ids=lambda value: f"claims={value}", ) def ft_claim_operation_count(request): return request.param @pytest.fixture( params=[63, 256, 4_096], ids=lambda value: f"operations={value}", ) def ft_operation_count(request): return request.param @pytest.fixture( params=[2, 4, 8, 16], ids=lambda value: f"drainers={value}", ) def ft_drainer_count(request): return request.param @pytest.fixture(params=[1, 3, 8, 32], ids=lambda value: f"rounds={value}") def ft_drain_rounds(request): return request.param @pytest.fixture(params=[2, 8, 16], ids=lambda value: f"readers={value}") def ft_reader_count(request): return request.param @pytest.fixture(params=[1, 257, 2_000], ids=lambda value: f"reads={value}") def ft_reads_per_thread(request): return request.param @pytest.fixture(params=[2, 8, 16], ids=lambda value: f"observers={value}") def ft_observer_count(request): return request.param @pytest.fixture( params=[257, 2_000], ids=lambda value: f"completions={value}", ) def ft_completion_count(request): return request.param @pytest.fixture(params=[2, 8, 32], ids=lambda value: f"setters={value}") def ft_callback_setter_count(request): return request.param @pytest.fixture(params=[1, 257, 2_000], ids=lambda value: f"sets={value}") def ft_callback_sets_per_thread(request): return request.param @pytest.fixture(params=[2, 8, 32], ids=lambda value: f"cancellers={value}") def ft_canceller_count(request): return request.param @pytest.fixture(params=[1, 8, 64], ids=lambda value: f"cancel-rounds={value}") def ft_cancel_rounds(request): return request.param def test_same_operation_is_claimed_once_across_contexts_all_backends( tmp_path, backend, ft_claimant_count, ft_claim_operation_count, workers, ): """Every backend must synchronize a one-shot claim on the Operation. This is deliberately separate from the more aggressive python_aio test above: it only uses the shared public API, so future free-threading C backends run exactly the same cross-Context race. """ _require_gil_disabled() iterations = ft_claim_operation_count claim_window = min(iterations, 512) path = tmp_path / "cross-context-claim.bin" fd = os.open(path, os.O_RDWR | os.O_CREAT | _O_BINARY, 0o600) contexts = tuple( backend.Context(max_requests=claim_window) for _ in range(ft_claimant_count) ) start = workers.barrier(ft_claimant_count + 1) finished = workers.barrier(ft_claimant_count + 1) current = [None] submitted = [0] * ft_claimant_count callback_count = [0] callback_lock = threading.Lock() def on_done(_result): with callback_lock: callback_count[0] += 1 def submitter(index, context): for _ in range(iterations): start.wait() submitted[index] += context.submit(current[0]) finished.wait() try: workers.start_many(submitter, enumerate(contexts)) for iteration in range(iterations): operation = backend.Operation.write(b"x", fd, 0) operation.set_callback(on_done) current[0] = operation start.wait() finished.wait() if (iteration + 1) % claim_window == 0: expected_callbacks = sum(submitted) def window_finished(expected=expected_callbacks): with callback_lock: return callback_count[0] >= expected _pump_contexts( contexts, window_finished, timeout=20, ) workers.join() accepted = sum(submitted) def all_callbacks_finished(): with callback_lock: return callback_count[0] >= accepted _pump_contexts( contexts, all_callbacks_finished, timeout=20, ) assert accepted == iterations, ( f"{accepted - iterations} Operations were submitted twice" ) assert callback_count[0] == iterations finally: workers.close() _close_contexts(contexts) os.close(fd) def test_concurrent_submits_to_one_context_all_backends( tmp_path, backend, ft_submitter_count, ft_operations_per_submit, ft_submit_rounds, workers, ): """Distinct Operations submitted concurrently must not corrupt Context.""" _require_gil_disabled() worker_count = ft_submitter_count operations_per_worker = ft_operations_per_submit submit_rounds = ft_submit_rounds operations_per_worker_total = operations_per_worker * submit_rounds total = worker_count * operations_per_worker_total path = tmp_path / "one-context-submit.bin" fd = os.open(path, os.O_RDWR | os.O_CREAT | _O_BINARY, 0o600) context = backend.Context(max_requests=total) submit_start = workers.barrier(worker_count) operations = [] callback_results = [None] * total callback_lock = threading.Lock() submitted = [0] * worker_count def make_callback(index, operation): def callback(result): with callback_lock: callback_results[index] = (result, operation.get_value()) return callback for index in range(total): payload = index.to_bytes(4, "little") operation = backend.Operation.write(payload, fd, index * 4) operation.set_callback(make_callback(index, operation)) operations.append(operation) def submitter(worker_index): first = worker_index * operations_per_worker_total accepted = 0 for round_index in range(submit_rounds): batch_first = first + round_index * operations_per_worker batch_last = batch_first + operations_per_worker # Every round stages a batch after the same barrier. Multiple # rounds vary scheduling and repeatedly collide inside the # Context's SQ-tail read/write window. submit_start.wait() accepted += context.submit( *operations[batch_first:batch_last], ) submitted[worker_index] = accepted try: workers.start_many( submitter, ((index,) for index in range(worker_count)), ) workers.join() assert sum(submitted) == total def all_callbacks_finished(): with callback_lock: return all( result is not None for result in callback_results ) _pump_contexts( (context,), all_callbacks_finished, timeout=20, ) assert callback_results == [(4, 4)] * total os.lseek(fd, 0, os.SEEK_SET) expected = b"".join(index.to_bytes(4, "little") for index in range(total)) assert os.read(fd, len(expected)) == expected finally: workers.close() _close_contexts((context,)) os.close(fd) def test_concurrent_process_events_delivers_each_completion_once( tmp_path, polling_backend, ft_operation_count, ft_drainer_count, ft_drain_rounds, workers, ): """Concurrent drainers must never consume or callback one event twice.""" _require_gil_disabled() operation_count = ft_operation_count drainer_count = ft_drainer_count drain_rounds = ft_drain_rounds path = tmp_path / "concurrent-process-events.bin" fd = os.open(path, os.O_RDWR | os.O_CREAT | _O_BINARY, 0o600) context = polling_backend.Context(max_requests=operation_count) callback_counts = [0] * operation_count callback_lock = threading.Lock() drain_round = workers.barrier(drainer_count) def make_callback(index): def callback(_result): with callback_lock: callback_counts[index] += 1 return callback operations = [] for index in range(operation_count): operation = polling_backend.Operation.write(b"x", fd, index) operation.set_callback(make_callback(index)) operations.append(operation) assert context.submit(*operations) == operation_count is_sqpoll = bool(getattr(context, "sqpoll", False)) if hasattr(context, "flush"): context.flush() if is_sqpoll: with callback_lock: completed_inline = sum(callback_counts) if completed_inline < operation_count: # flush() above wakes a sleeping SQPOLL thread and may drain # already-ready CQEs. If work remains, wait until eventfd says # the kernel has populated more of the CQ, but leave that CQ # untouched for the synchronized drainers below. readable, _, _ = select.select([context.fileno], [], [], 10) assert readable, "SQPOLL produced no completion notification" time.sleep(0.01) def drain(): for _ in range(drain_rounds): # Force all drainers to enter every round together. Without # this barrier a fast first thread can consume the whole CQ # before the others even start, accidentally serializing the # test and hiding a duplicate cq_head read/commit. drain_round.wait() context.process_events( max_requests=operation_count, min_requests=0, timeout=0, ) try: workers.start_many(drain, (() for _ in range(drainer_count))) workers.join() def all_callbacks_finished(): with callback_lock: return sum(callback_counts) >= operation_count # Fixed synchronized rounds should normally drain everything. Finish # any genuinely late kernel completions serially so the assertion # below diagnoses duplicate delivery, not storage latency. _pump_contexts((context,), all_callbacks_finished, timeout=20) assert callback_counts == [1] * operation_count finally: workers.close() os.close(fd) def test_completed_operation_supports_concurrent_readers( tmp_path, backend, ft_reader_count, ft_reads_per_thread, workers, ): """C backends must safely return owned result references to all threads.""" _require_gil_disabled() payload = bytes(range(256)) * 16 path = tmp_path / "concurrent-result-readers.bin" path.write_bytes(payload) fd = os.open(path, os.O_RDONLY | _O_BINARY) context = backend.Context(max_requests=8) operation = backend.Operation.read(len(payload), fd, 0) completed = threading.Event() operation.set_callback(lambda _result: completed.set()) assert context.submit(operation) == 1 _pump_contexts((context,), completed.is_set) assert operation.get_value() == payload reader_count = ft_reader_count reads_per_thread = ft_reads_per_thread start = workers.barrier(reader_count + 1) def read_result(): start.wait() for _ in range(reads_per_thread): if operation.get_value() != payload: raise AssertionError("concurrent get_value() returned wrong data") try: workers.start_many(read_result, (() for _ in range(reader_count))) start.wait() workers.join() finally: workers.close() _close_contexts((context,)) os.close(fd) def test_payload_access_is_safe_while_worker_publishes_completion( tmp_path, pooled_backend, ft_observer_count, ft_completion_count, workers, ): """Publishing ``done`` must not expose a buffer being cleared concurrently. In thread_aio the worker used to release-store ``done = 1`` before acquiring a Python thread state and clearing a completed write's ``py_buffer``. A free-running reader could therefore pass the in-flight check and Py_INCREF the same pointer while the worker Py_DECREFed it. """ _require_gil_disabled() operation_count = ft_completion_count observer_count = ft_observer_count path = tmp_path / "payload-completion-race.bin" fd = os.open(path, os.O_RDWR | os.O_CREAT | _O_BINARY, 0o600) context = pooled_backend.Context( max_requests=operation_count + 1, pool_size=1, ) blocker_started = threading.Event() blocker_release = threading.Event() blocker = pooled_backend.Operation.fsync(fd) blocker.set_callback( lambda _result: ( blocker_started.set(), blocker_release.wait(30), ), ) assert context.submit(blocker) == 1 assert blocker_started.wait(10), "worker did not enter blocker callback" completed_count = [0] completed_lock = threading.Lock() all_completed = threading.Event() operations = [] def on_done(_result): # thread_aio publishes done before invoking this callback and clears # the write buffer immediately after it returns. Yielding here makes # observers repeatedly enter payload/get_value in that exact state # and then overlap the buffer's ownership transition. time.sleep(0.001) with completed_lock: completed_count[0] += 1 if completed_count[0] == operation_count: all_completed.set() for index in range(operation_count): operation = pooled_backend.Operation.write(b"x", fd, index) operation.set_callback(on_done) operations.append(operation) assert context.submit(*operations) == operation_count observer_start = workers.barrier(observer_count + 1) def observe(): observer_start.wait() while not all_completed.is_set(): for operation in operations: try: payload = operation.payload if payload is not None: bytes(payload) except RuntimeError: pass try: operation.get_value() except RuntimeError: pass try: workers.start_many(observe, (() for _ in range(observer_count))) observer_start.wait() blocker_release.set() assert all_completed.wait(30), "worker did not finish queued operations" workers.join(timeout=10) assert completed_count[0] == operation_count finally: blocker_release.set() workers.close() _close_contexts((context,)) os.close(fd) def test_concurrent_callback_replacement_releases_every_old_callback( backend, ft_callback_setter_count, ft_callback_sets_per_thread, workers, ): """set_callback must atomically replace and release its owned reference.""" _require_gil_disabled() setter_count = ft_callback_setter_count sets_per_thread = ft_callback_sets_per_thread operation = backend.Operation.fsync(0) start = workers.barrier(setter_count + 1) class Callback: def __call__(self, _result): return None callbacks = [ Callback() for _ in range(setter_count * sets_per_thread) ] callback_refs = [weakref.ref(callback) for callback in callbacks] def replace_callbacks(thread_index): start.wait() first = thread_index * sets_per_thread for index in range(first, first + sets_per_thread): assert operation.set_callback(callbacks[index]) is True try: workers.start_many( replace_callbacks, ((index,) for index in range(setter_count)), ) start.wait() workers.join() # Move the Operation off every callback in the contested set. Each # set_callback owns exactly one reference and must release the old # one, even when several threads replace the same slot concurrently. operation.set_callback(Callback()) callbacks.clear() gc.collect() leaked = sum(ref() is not None for ref in callback_refs) assert leaked == 0, f"{leaked} replaced callbacks are still referenced" finally: workers.close() def test_concurrent_uring_cancel_requests_do_not_corrupt_submission_ring( ft_canceller_count, ft_cancel_rounds, workers, ): """cancel and submit share io_uring's SQ tail and must share its lock.""" _require_gil_disabled() if caio.linux_uring is None: pytest.skip("linux_uring backend is unavailable") canceller_count = ft_canceller_count cancel_rounds = ft_cancel_rounds operation_count = canceller_count * cancel_rounds context = caio.linux_uring.Context( max_requests=operation_count * 2, sqpoll=False, ) read_fd, write_fd = os.pipe() callback_counts = [0] * operation_count callback_lock = threading.Lock() def make_callback(index): def callback(_result): with callback_lock: callback_counts[index] += 1 return callback targets = [ caio.linux_uring.Operation.read( 1, read_fd, (1 << 64) - 1, ) for _ in range(operation_count) ] for index, operation in enumerate(targets): operation.set_callback(make_callback(index)) assert context.submit(*targets) == operation_count context.flush() start_round = workers.barrier(canceller_count) cancelled = [0] * canceller_count def cancel(thread_index): for round_index in range(cancel_rounds): start_round.wait() cancelled[thread_index] += context.cancel( targets[round_index * canceller_count + thread_index], ) try: workers.start_many( cancel, ((index,) for index in range(canceller_count)), ) workers.join() assert cancelled == [0] * canceller_count def all_targets_completed(): with callback_lock: return sum(callback_counts) >= operation_count _pump_contexts((context,), all_targets_completed, timeout=10) assert callback_counts == [1] * operation_count finally: workers.close() # Release any read whose cancel SQE was lost so Context teardown # cannot leave the kernel holding pointers to live Operations. try: os.write(write_fd, b"x" * operation_count) _pump_contexts( (context,), lambda: sum(callback_counts) >= operation_count, timeout=5, ) except (BrokenPipeError, TimeoutError): pass os.close(write_fd) os.close(read_fd) caio-0.12.3/tests/test_impl_selector.py000066400000000000000000000030361523515553700201410ustar00rootroot00000000000000import os import platform import sys from subprocess import check_output import pytest import caio @pytest.fixture(params=caio.variants) def implementation(request): if request.param is caio.linux_uring: return "uring" if request.param is caio.linux_aio: return "linux" if request.param is caio.thread_aio: return "thread" if request.param is caio.python_aio: return "python" raise RuntimeError(f"Unknown variant {request.param!r}") @pytest.mark.skipif(platform.system() == 'Windows', reason="Windows skip") def test_env_selector(implementation): output = check_output( [ sys.executable, "-c", "import caio, inspect; print(caio.Context.__doc__)" ], env={"CAIO_IMPL": implementation} ).decode() assert implementation in output, output @pytest.fixture() def implementation_file(implementation): path = os.path.dirname(caio.__file__) fname = os.path.join(path, "default_implementation") try: with open(fname, "w") as fp: fp.write("# NEWER COMMIT THIS FILE") fp.write("\nwrong string\n") fp.write(implementation) fp.write("\n\n") yield implementation finally: os.remove(fname) def test_file_selector(implementation_file): output = check_output( [ sys.executable, "-c", "import caio, inspect; print(caio.Context.__doc__)" ], ).decode() assert implementation_file in output, output caio-0.12.3/tests/test_operation_refcounting.py000066400000000000000000000163431523515553700217100ustar00rootroot00000000000000import gc import sys import tempfile import time import pytest from conftest import wait_until def test_get_value_on_fsync_does_not_corrupt_none_refcount(backend): """get_value() on fsync/fdsync must return None on every backend. The 3 C backends used to `return Py_None` instead of Py_RETURN_NONE there - a borrowed reference handed out as if it were new; run it enough times that an under-refcounted None would show up as a wrong count (or, in the worst case, a crash from None being freed). max_requests=256 is just generous headroom for 200 in-flight ops submitted without pacing - not testing capacity limits here.""" with tempfile.NamedTemporaryFile() as f: ctx = backend.Context(max_requests=256) results = [] for _ in range(200): op = backend.Operation.fsync(f.fileno()) op.set_callback(results.append) ctx.submit(op) assert wait_until(ctx, lambda: len(results) >= 200) op = backend.Operation.fsync(f.fileno()) done = [] op.set_callback(done.append) ctx.submit(op) assert wait_until(ctx, lambda: bool(done)) assert op.get_value() is None def test_resubmitting_an_in_flight_operation_does_not_corrupt_context(backend): """Resubmitting an Operation that's already in_progress must be a silent no-op (not counted, not re-queued) rather than corrupting anything about which Context it's tied to - previously (thread_aio.c) the C struct's ctx pointer was overwritten unconditionally for every argument before the in_progress check, with no matching incref, on whichever Context received the resubmission attempt. python_aio's plain-Python in-flight guard has nothing analogous to corrupt, but the observable contract (only ctx_a's submit counts, only its callback fires) must still hold identically.""" with tempfile.NamedTemporaryFile() as f: ctx_a = backend.Context(max_requests=32) ctx_b = backend.Context(max_requests=32) results = [] op = backend.Operation.write(b"x", f.fileno(), 0) op.set_callback(results.append) submitted_a = ctx_a.submit(op) submitted_b = ctx_b.submit(op) # op is in_progress - must be a no-op assert submitted_a == 1 assert submitted_b == 0 assert wait_until(ctx_a, lambda: bool(results)) assert results == [1] del ctx_a, ctx_b gc.collect() def test_large_batch_submit_does_not_crash(backend): """A big enough batch used to overflow a caller-controlled-size stack VLA in thread_aio.c/linux_aio.c/linux_uring.c. Not a C-specific concern by nature (large batches should just work everywhere), so this runs against all backends, including python_aio.""" with tempfile.NamedTemporaryFile() as f: ctx = backend.Context(max_requests=8192) results = [] ops = [] for _ in range(4000): op = backend.Operation.fsync(f.fileno()) op.set_callback(results.append) ops.append(op) submitted = ctx.submit(*ops) assert submitted == 4000 assert wait_until(ctx, lambda: len(results) >= 4000, timeout=15.0) def test_context_teardown_drains_queued_operations(pooled_backend): """Dropping a Context with operations still queued must not abandon them (silently leaking their references) - it should drain the queue (graceful threadpool shutdown) so every already in-flight callback still fires. Scoped to pooled_backend (thread_aio/python_aio): only a real worker-thread pool has a "queued but not yet dispatched" state distinct from "accepted" - linux_aio/linux_uring have no such queue.""" with tempfile.NamedTemporaryFile() as f: ctx = pooled_backend.Context(max_requests=256, pool_size=2) results = [] ops = [] for _ in range(64): op = pooled_backend.Operation.fsync(f.fileno()) op.set_callback(results.append) ops.append(op) ctx.submit(*ops) del ctx gc.collect() deadline = time.monotonic() + 10.0 while len(results) < 64 and time.monotonic() < deadline: time.sleep(0.01) assert len(results) >= 64, ( f"only {len(results)}/64 callbacks fired after teardown" ) def test_bad_args_do_not_accumulate_leaked_operations(backend): """Operation.read/write/fsync/fdsync used to `return NULL` on a bad argument without Py_DECREF-ing the object tp_alloc had already handed them - leaking it forever (unreachable from Python, but never freed). There's no live reference to take a weakref of in this scenario (the object never successfully escapes the failing constructor), so the only way to actually observe a per-call leak is total live-object count not growing with iteration count. Covers all 4 constructors, each run enough times that a real per-call leak would show up as unmistakable linear growth. Runs on every backend, including python_aio: its Operation.__init__ validates fd/nbytes/offset/priority the same way (operator.index(), matching PyArg_ParseTupleAndKeywords' own __index__-based coercion), raising the same TypeError synchronously rather than storing a bad value and failing later.""" with tempfile.NamedTemporaryFile() as f: for ctor, args in [ (backend.Operation.read, ("not-an-int", f.fileno(), 0)), (backend.Operation.write, (b"x", "not-an-int", 0)), (backend.Operation.fsync, ("not-an-int",)), (backend.Operation.fdsync, ("not-an-int",)), ]: gc.collect() before = len(gc.get_objects()) for _ in range(500): try: ctor(*args) except TypeError: pass gc.collect() after = len(gc.get_objects()) # Generous slack for unrelated interpreter/test-harness churn - # a real per-call leak of 500 Operation objects would blow far # past this. assert after - before < 100, ( f"{ctor.__qualname__}: object count grew by {after - before} " "over 500 failed constructions - looks like a per-call leak" ) def test_write_with_non_bytes_payload_does_not_corrupt_its_refcount(backend): """A non-bytes write payload must be rejected with ValueError, without corrupting the caller's own reference to it, on every backend. The 3 C backends used to store the "O"-parsed payload_bytes argument (a borrowed reference) directly into the C struct's own buffer field before validating it was actually bytes - on the ensuing type-check failure, cleaning up the partially-constructed Operation decremented a reference it never owned, over-decrementing the caller's own object. python_aio can't have that exact bug (no manual refcounting to get wrong), but validates the same way and the observable contract - raise before construction completes, no lingering reference - must hold identically.""" payload = ["not", "bytes"] before = sys.getrefcount(payload) with pytest.raises(ValueError): backend.Operation.write(payload, 0, 0) gc.collect() assert sys.getrefcount(payload) == before caio-0.12.3/tests/test_python_aio_callback.py000066400000000000000000000075011523515553700212660ustar00rootroot00000000000000import tempfile import threading import time import pytest from caio import python_aio def test_submit_without_callback_completes_normally(): """set_callback() is optional - a missing callback must be a normal case, not something that trips up the result-handler thread.""" with tempfile.NamedTemporaryFile() as f: ctx = python_aio.Context() op = python_aio.Operation.write(b"hello", f.fileno(), 0) ctx.submit(op) deadline = time.monotonic() + 5.0 while ctx._in_progress and time.monotonic() < deadline: time.sleep(0.001) assert op.get_value() == 5 def test_raising_callback_does_not_kill_the_result_handler_thread(): """A callback that raises must not take down ThreadPool's single result-handler thread - if it did, every operation submitted after the raising one would never have its result/callback delivered.""" with tempfile.NamedTemporaryFile() as f: ctx = python_aio.Context() unraisable = [] original_hook = threading.excepthook threading.excepthook = unraisable.append try: failing_op = python_aio.Operation.write(b"first", f.fileno(), 0) failing_op.set_callback(lambda _: 1 / 0) ctx.submit(failing_op) deadline = time.monotonic() + 5.0 while failing_op.written == 0 and time.monotonic() < deadline: time.sleep(0.001) assert len(unraisable) == 1 assert unraisable[0].exc_type is ZeroDivisionError results = [] following_op = python_aio.Operation.write(b"second", f.fileno(), 5) following_op.set_callback(results.append) ctx.submit(following_op) deadline = time.monotonic() + 5.0 while not results and time.monotonic() < deadline: time.sleep(0.001) assert results == [6] finally: threading.excepthook = original_hook def test_max_requests_one_never_admits_a_second_concurrent_submit(): """The capacity check used to compare with `>` instead of `>=`, so max_requests=1 silently admitted a second in-flight operation - simulate "one already in flight" directly rather than racing a real background job, since a real read/write completes far too fast to reliably catch the window.""" ctx = python_aio.Context(max_requests=1, pool_size=1) ctx._in_progress = 1 with pytest.raises(RuntimeError): ctx.submit(python_aio.Operation.fsync(0)) def test_bounds_are_validated_at_construction(): with pytest.raises(ValueError): python_aio.Context(pool_size=0) with pytest.raises(ValueError): python_aio.Context(max_requests=0) with pytest.raises(ValueError): python_aio.Context(pool_size=python_aio.Context.MAX_POOL_SIZE) def test_set_callback_rejects_non_callable(): op = python_aio.Operation.fsync(0) with pytest.raises(ValueError): op.set_callback("not callable") def test_close_is_idempotent(): ctx = python_aio.Context() ctx.close() ctx.close() with pytest.raises(RuntimeError): ctx.submit(python_aio.Operation.fsync(0)) def test_scheduling_failure_rolls_back_the_reserved_slot(): """If apply_async() itself raises after the capacity slot was already reserved (e.g. a concurrent close() tore down the underlying pool between the capacity check and the actual scheduling call), the slot must be given back - otherwise _in_progress stays permanently inflated even though no job was ever actually running.""" ctx = python_aio.Context(max_requests=1) ctx.pool.close() # underlying pool gone, but ctx._state is still OPEN with pytest.raises(Exception): # noqa: B017 (whatever ThreadPool itself raises) ctx.submit(python_aio.Operation.fsync(0)) assert ctx._in_progress == 0 caio-0.12.3/tests/test_raw_low_level.py000066400000000000000000001614071523515553700201500ustar00rootroot00000000000000""" Tests against the raw Context/Operation API directly, bypassing the asyncio adapter - this is a real, documented part of the public API (see e.g. caio/linux_uring.pyi), and it's also where backend-specific low-level behavior (polling, queue capacity, cancellation) actually lives. Not every backend exposes the same surface here (only linux_aio and linux_uring have process_events/poll; only linux_uring also has flush), so most tests below are conditional on hasattr() rather than assuming a uniform API. """ import gc import os import re import subprocess import sys import threading import time import weakref import pytest from conftest import drain ABSURD_NBYTES = 2**62 def test_raw_submit_and_drain(tmp_path, polling_backend): with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = polling_backend.Context(max_requests=8) ops = [ polling_backend.Operation.write(bytes([i]) * 4, fd, i * 4) for i in range(4) ] submitted = ctx.submit(*ops) assert submitted == 4 processed = drain(ctx, 4) assert processed == 4 for op in ops: assert op.get_value() == 4 read_ops = [ polling_backend.Operation.read(4, fd, i * 4) for i in range(4) ] ctx.submit(*read_ops) drain(ctx, 4) for i, op in enumerate(read_ops): assert op.get_value() == bytes([i]) * 4 def test_raw_process_events_respects_min_max(tmp_path, polling_backend): with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = polling_backend.Context(max_requests=8) with pytest.raises(ValueError): ctx.process_events(max_requests=1, min_requests=5) op = polling_backend.Operation.write(b"x", fd, 0) ctx.submit(op) drain(ctx, 1) assert op.get_value() == 1 def test_process_events_respects_timeout_and_releases_gil(polling_backend): """process_events(min_requests=N, timeout=T) must give up after roughly `timeout` seconds if fewer than N completions ever show up, rather than blocking indefinitely - and must release the GIL while waiting, not freeze the whole interpreter for the duration. Runs in a subprocess with its own `timeout=`, so if this regresses to an unbounded, GIL-held block, the subprocess gets killed at the OS level instead of hanging the whole test session. Submits nothing at all, so min_requests=1 can never be satisfied, and checks two things at once inside the subprocess: wall time is bounded by roughly `timeout` (whole seconds), and a second thread *inside that same subprocess* keeps making progress while the call is blocked - proving the GIL was actually released, not just that the call happened to return quickly for some other reason. """ if not polling_backend.__name__.startswith("caio."): pytest.skip("synthetic test-only variant, not importable by name in a subprocess") code = ( "import threading, time\n" f"import {polling_backend.__name__} as m\n" "ctx = m.Context(max_requests=8)\n" "progress = []\n" "stop = threading.Event()\n" "def other_thread():\n" " while not stop.is_set():\n" " progress.append(time.monotonic())\n" " time.sleep(0.01)\n" "t = threading.Thread(target=other_thread, daemon=True)\n" "t.start()\n" "start = time.monotonic()\n" "n = ctx.process_events(max_requests=8, min_requests=1, timeout=1)\n" "elapsed = time.monotonic() - start\n" "stop.set()\n" "t.join(timeout=2.0)\n" "print(f'RESULT: n={n} elapsed={elapsed} progress={len(progress)}')\n" ) try: proc = subprocess.run( [sys.executable, "-c", code], capture_output=True, text=True, timeout=15, check=False, ) except subprocess.TimeoutExpired: pytest.fail("process_events(timeout=1) blocked far longer than its own timeout (subprocess timed out)") match = re.search(r"RESULT: n=(\d+) elapsed=([\d.]+) progress=(\d+)", proc.stdout) assert match, f"unexpected output; stdout={proc.stdout!r} stderr={proc.stderr!r}" n, elapsed, progress = int(match[1]), float(match[2]), int(match[3]) assert n == 0 assert elapsed < 5.0, f"process_events(timeout=1) took {elapsed:.1f}s - timeout was ignored" assert progress >= 3, ( "a second thread made too little progress while process_events() was " "blocked - the GIL was probably held throughout instead of released" ) def test_raw_poll_reflects_completions(tmp_path, polling_backend): with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = polling_backend.Context(max_requests=8) op = polling_backend.Operation.write(b"hi", fd, 0) ctx.submit(op) # poll() drains the eventfd counter, so it must not be called # speculatively before we know it's signaled (it's a blocking fd) - # only call it once flush()/process_events() has confirmed there's # something to report. drain(ctx, 1) assert op.get_value() == 2 def test_poll_does_not_block_when_nothing_pending(polling_backend): """poll() with nothing actually pending must raise BlockingIOError, as documented, rather than block the calling thread. Runs in a subprocess, killed via subprocess.run's own `timeout` (real OS-level termination) if it hangs - a background thread can't safely stand in for this check here, since poll() doesn't release the GIL, so a thread stuck inside it would hold the GIL hostage forever regardless of what the test's own main thread does. """ if not polling_backend.__name__.startswith("caio."): pytest.skip("synthetic test-only variant, not importable by name in a subprocess") code = ( f"import {polling_backend.__name__} as m\n" f"ctx = m.Context(max_requests=8)\n" f"try:\n" f" ctx.poll()\n" f" print('RESULT: unexpectedly-returned')\n" f"except BlockingIOError:\n" f" print('RESULT: raised BlockingIOError')\n" ) try: proc = subprocess.run( [sys.executable, "-c", code], capture_output=True, text=True, timeout=10, check=False, ) except subprocess.TimeoutExpired: pytest.fail("poll() blocked instead of raising BlockingIOError (subprocess timed out)") assert "RESULT: raised BlockingIOError" in proc.stdout, ( f"stdout={proc.stdout!r} stderr={proc.stderr!r}" ) def test_payload_and_get_value_blocked_while_in_flight(tmp_path, polling_backend): """payload/get_value() must refuse to return an operation's buffer while the kernel may still be actively writing into it - for linux_uring in particular, that buffer is the *same* Python bytes object the kernel writes into directly (zero-copy), so handing it out mid-flight would mean observing (or even, via `hash()`, corrupting the cached hash of) a nominally-immutable object while it's still being mutated. For linux_aio the read buffer is wrapped in a Mutex, but the kernel doesn't know about that lock, so reading through it mid-flight is still a data race, just a narrower one (torn reads instead of a shared identity issue). No timing race needed: `submit()` sets its own in-flight bookkeeping synchronously (for linux_uring, even before the SQE reaches the kernel via flush()), and it's only cleared once *this process* handles the completion via process_events()/flush() - regardless of how fast the kernel itself actually finishes the write. """ with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = polling_backend.Context(max_requests=8) op = polling_backend.Operation.write(b"hello", fd, 0) ctx.submit(op) with pytest.raises(RuntimeError): _ = op.payload with pytest.raises(RuntimeError): _ = op.get_value() drain(ctx, 1) # Once actually completed, both must work normally again. assert op.get_value() == 5 assert bytes(op.payload) == b"hello" def test_thread_aio_payload_and_get_value_blocked_while_in_flight(tmp_path): """thread_aio only: same contract as test_payload_and_get_value_blocked_while_in_flight above, but this backend has no process_events()/poll() split to hook a synchronous in-flight check onto - completion is purely callback-driven, and its worker() thread runs the actual pread()/pwrite() syscall without holding the GIL, so payload/get_value() reading result/buf_size/error concurrently from the submitting thread is a genuine, unsynchronized data race, not just a narrow window. No timing race needed here either: pool_size=1 plus a first operation whose own callback blocks (on blocker_release) makes the single worker thread provably still busy with it - so a second operation submitted right after is provably still queued, never touched by the worker, when the assertions below run. """ thread_aio = pytest.importorskip("caio.thread_aio") with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = thread_aio.Context(max_requests=8, pool_size=1) blocker_started = threading.Event() blocker_release = threading.Event() blocker_op = thread_aio.Operation.fsync(fd) blocker_op.set_callback( lambda r: (blocker_started.set(), blocker_release.wait(5.0)), ) ctx.submit(blocker_op) assert blocker_started.wait(5.0), "blocker operation's callback never ran" op = thread_aio.Operation.write(b"hello", fd, 0) op_done = threading.Event() op.set_callback(lambda r: op_done.set()) ctx.submit(op) with pytest.raises(RuntimeError): _ = op.payload with pytest.raises(RuntimeError): _ = op.get_value() blocker_release.set() assert op_done.wait(5.0), "operation never completed after unblocking the pool" # Once actually completed, both must work normally again. assert op.get_value() == 5 assert op.payload is None # thread_aio frees a write's payload on completion def test_read_with_absurd_nbytes_raises_cleanly(backend): """Operation.read() with an unreasonably large nbytes must raise a catchable exception, never crash the process. A naive allocation strategy for a size this large doesn't just panic (which PyO3 would convert to a catchable PanicException) - it can *abort the whole process* (SIGABRT) via Rust's global alloc-error handler, which is not recoverable from Python at all. Runs in a subprocess since a real abort would otherwise take the whole test session down with it. python_aio doesn't preallocate a buffer at construction time (nbytes is just stored, the real buffer is sized lazily from whatever the actual read returns), so it was never at risk here and is skipped. """ if backend.__name__ == "caio.python_aio": pytest.skip("python_aio doesn't preallocate at construction time") if not backend.__name__.startswith("caio."): pytest.skip("synthetic test-only variant, not importable by name in a subprocess") code = ( f"import {backend.__name__} as m\n" f"try:\n" f" m.Operation.read({ABSURD_NBYTES}, 0, 0)\n" f" print('RESULT: unexpectedly-succeeded')\n" f"except (OverflowError, MemoryError, ValueError) as e:\n" f" print(f'RESULT: raised {{type(e).__name__}}')\n" ) proc = subprocess.run( [sys.executable, "-c", code], capture_output=True, text=True, timeout=30, check=False, ) assert proc.returncode == 0, ( f"process aborted/crashed (returncode={proc.returncode}); " f"stdout={proc.stdout!r} stderr={proc.stderr!r}" ) assert "RESULT: raised" in proc.stdout, ( f"expected a catchable exception; stdout={proc.stdout!r} stderr={proc.stderr!r}" ) @pytest.mark.parametrize("op_count", [1, 4]) def test_queue_full_does_not_crash(tmp_path, backend, op_count): """Submitting far beyond a tiny max_requests must never hang or corrupt state, but backends genuinely disagree on *how* they signal it: - thread_aio, linux_uring, python_aio each track capacity themselves (a queue/ring/counter checked before any work starts) and raise immediately once it's exceeded. - linux_aio's capacity is the kernel's own io_context depth. Since writes to a regular buffered file usually complete synchronously within io_submit() itself (Linux AIO doesn't provide real asynchrony for buffered I/O on most filesystems), the "backlog" rarely builds up enough to hit that limit - and even when it does, io_submit()'s documented behavior is to return fewer than requested rather than raise (matching the original C implementation, which just returns that count as-is). So for linux_aio this only asserts the submitted-count invariant, not an exception. This test exists to make that difference explicit, not to force one "correct" answer.""" with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = backend.Context(max_requests=op_count) ops = [ backend.Operation.write(b"x", fd, i) for i in range(op_count * 64) ] if backend.__name__ == "caio.linux_aio": submitted = ctx.submit(*ops) assert 0 <= submitted <= len(ops) else: with pytest.raises((OverflowError, RuntimeError, ValueError)): ctx.submit(*ops) # The context must remain safely destructible either way. del ctx, ops def test_cancel_on_real_operation_does_not_crash(tmp_path, backend): with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = backend.Context(max_requests=4) op = backend.Operation.read(4096, fd, 0) ctx.submit(op) try: result = ctx.cancel(op) except (SystemError, RuntimeError, ValueError, NotImplementedError) as e: # linux_aio/linux_uring may legitimately report "too late to # cancel" or similar as an exception; that's fine, it just # must not crash or hang. result = e assert result is not None # Drain whatever ends up happening to the op (cancelled or raced # to completion) so the Context can tear down cleanly. if hasattr(ctx, "process_events"): try: drain(ctx, 1, timeout=2.0) except TimeoutError: pass def test_linux_aio_operation_context_back_reference_clears_on_completion(tmp_path): """linux_aio only: submit() makes each accepted Operation hold a strong reference to its own Context (via the `.context` property) while genuinely in flight - so it stays usable (e.g. to call `op.context.cancel(op)`) even if the caller drops their own Context reference early - but this back-reference is cleared once the Operation reaches a terminal state (drained via process_events()/ cancel()), not held forever. Holding it past completion (the original behavior) created an uncollectable reference cycle: registry -> Operation -> context -> Context -> registry. PyO3 classes don't participate in Python's cyclic GC, so once accepted, plain refcounting could never reclaim either object even after the operation finished, leaking both Python objects and - until something else eventually dropped the Context - the underlying kernel AIO context/eventfd too. """ linux_aio = pytest.importorskip("caio.linux_aio") with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = linux_aio.Context(max_requests=8) op = linux_aio.Operation.write(b"x" * 4, fd, 0) assert ctx.submit(op) == 1 assert op.context is ctx del ctx # Still alive and fully usable through the Operation's own # back-reference while genuinely in flight - dropping the # caller's own reference alone must not have torn it down. still_alive = op.context assert still_alive.max_requests == 8 drain(still_alive, 1) assert op.get_value() == 4 # Once terminal, nothing needs the back-reference anymore - it's # cleared, breaking the cycle. assert op.context is None def test_uring_operation_context_back_reference_clears_on_completion(tmp_path): """linux_uring only: same contract as linux_aio's `.context` property (see test_linux_aio_operation_context_back_reference_clears_on_completion above) - submit() makes each accepted Operation hold a strong reference to its own Context while genuinely in flight, cleared once the operation reaches a terminal state (drained via process_events()/flush()). Without this, a Context the caller drops while an operation is still outstanding could be garbage collected mid-flight, munmapping the SQ/CQ rings and closing uring_fd while the kernel might still be touching them. """ linux_uring = pytest.importorskip("caio.linux_uring") with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = linux_uring.Context(max_requests=8) op = linux_uring.Operation.write(b"x" * 4, fd, 0) assert ctx.submit(op) == 1 assert op.context is ctx del ctx still_alive = op.context assert still_alive.max_requests == 8 drain(still_alive, 1) assert op.get_value() == 4 assert op.context is None def test_uring_context_stays_alive_via_operation_while_genuinely_in_flight(tmp_path): """linux_uring only: a Context the caller drops while a submitted Operation is genuinely still in flight must not be collected out from under it - op.context (see the test above) keeps it alive via plain refcounting. Proven here with weakref, independent of the object's own repr/attributes: the outstanding write is blocked on a full pipe with no reader, so it provably cannot have completed yet when `del ctx` runs. Once the pipe is drained and the write actually completes, op.context clears and nothing else references the Context, so the weakref must clear too. """ linux_uring = pytest.importorskip("caio.linux_uring") import fcntl # Unix-only; this whole test is uring-only (Linux-only) F_SETPIPE_SZ = 1031 r_fd, w_fd = os.pipe() try: fcntl.fcntl(w_fd, F_SETPIPE_SZ, 4096) os.write(w_fd, b"f" * 4096) # fill the pipe so the next write blocks until drained ctx = linux_uring.Context(max_requests=8) ctx_ref = weakref.ref(ctx) # Offset must be -1 (io_uring's convention for non-seekable files, # same as plain write(2)) - a pipe rejects an explicit offset # outright, which would fail the write instantly instead of # leaving it pending. op = linux_uring.Operation.write(b"pending", w_fd, 0xFFFFFFFFFFFFFFFF) assert ctx.submit(op) == 1 del ctx gc.collect() assert ctx_ref() is not None, ( "Context was collected while its Operation was still genuinely " "in flight - op.context should have kept it alive" ) os.read(r_fd, 8192) # unblocks the pending write drain(ctx_ref(), 1, timeout=5.0) assert op.get_value() == len(b"pending") gc.collect() assert ctx_ref() is None, ( "Context outlived its Operation's completion - op.context " "should have been cleared, leaving nothing to keep it alive" ) finally: os.close(r_fd) os.close(w_fd) def test_linux_aio_context_becomes_collectible_after_operation_completes(tmp_path): """linux_aio only: keeping a *completed* Operation alive - a common pattern, e.g. holding onto it to read `.get_value()` again later - must not also keep its Context alive, once the caller has otherwise dropped their own Context reference. Disables the cyclic garbage collector for the duration of the check so only plain refcounting can possibly free the Context: before this fix, `op.context` was never cleared on completion, so a completed Operation the caller happened to keep around would keep its Context (and the underlying kernel AIO context/eventfd) alive indefinitely too, regardless of whether the caller still wanted it. Note this is deliberately not the same scenario as an Operation abandoned *before* it ever completes (both external references dropped with nothing left to ever drain it) - see test_linux_aio_abandoned_operation_is_collectible_by_gc below for that one, which plain refcounting alone (as exercised here, with the cyclic GC disabled) genuinely cannot free - it needs `gc.collect()`. """ linux_aio = pytest.importorskip("caio.linux_aio") gc.disable() try: with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = linux_aio.Context(max_requests=8) op = linux_aio.Operation.write(b"x" * 4, fd, 0) ctx_ref = weakref.ref(ctx) assert ctx.submit(op) == 1 drain(ctx, 1) assert op.get_value() == 4 del ctx # the caller drops Context... assert ctx_ref() is None, "Context leaked via a completed Operation's stale back-reference" # ...but the completed Operation itself is still perfectly # usable, unaffected by its Context having been freed. assert op.get_value() == 4 finally: gc.enable() def test_operation_self_referential_callback_cycle_is_collectible_by_gc(backend): """A callback closure that captures its own Operation (a natural pattern - e.g. `op.set_callback(lambda res: op.get_value())` to stash the result somewhere) forms a genuine reference cycle: op -> callback -> cell -> op. Plain refcounting can never free this on its own - cyclic GC must find and break it, via each C backend's Operation now supporting the GC protocol (Py_TPFLAGS_HAVE_GC / tp_traverse / tp_clear), same as python_aio's plain Python class already did without any extra work. Unlike test_linux_aio_abandoned_operation_is_collectible_by_gc above, this doesn't need a Context at all - the cycle is entirely within the Operation object itself, so it's a scenario all four backends can hit, not just the two native ones with a `.context` back-reference. """ def make_cycle(): op = backend.Operation.fsync(1) op.set_callback(lambda res: op) return weakref.ref(op) gc.disable() try: op_ref = make_cycle() assert op_ref() is not None, ( "expected plain refcounting alone to NOT free this cycle - " "if it did, this test isn't exercising the scenario it claims to" ) finally: gc.enable() gc.collect() assert op_ref() is None, "gc.collect() must break the self-referential callback cycle" def test_resubmission_behavior_is_backend_specific(tmp_path, backend): """All four backends now agree that submit()-ing the same Operation object twice back to back, with no drain in between, must silently skip the second attempt - none of them will ever hand out two concurrent dispatches of the same one-shot Operation: - thread_aio marks "in_progress" sticky forever (never reset), so a second submit() always silently skips it, regardless of timing. - linux_uring/linux_aio reset "in_progress" only once the operation actually completes; since this test never calls flush() (the only thing that hands submitted SQEs to the kernel for linux_uring's default, non-SQPOLL mode), the operation cannot possibly have completed yet, so the second submit() is deterministically skipped here too - otherwise the kernel would end up with two concurrent iocbs/SQEs pointing at the same buffer, a real data race for reads. - python_aio now has the same in-flight guard as the other three (Operation.in_progress, checked/set under Context's own lock). This test exists to catch any regression in this shared guarantee.""" with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = backend.Context(max_requests=4) op = backend.Operation.write(b"x", fd, 0) first = ctx.submit(op) assert first == 1 second = ctx.submit(op) assert second == 0, "expected sticky/not-yet-completed in-flight state to skip resubmission" if hasattr(ctx, "process_events"): try: drain(ctx, first + second, timeout=2.0) except TimeoutError: pass def test_linux_aio_rejects_concurrent_resubmit(tmp_path): """linux_aio only, complementing test_resubmission_behavior_is_backend_specific above: submit() must atomically claim each op, so the kernel never ends up with two concurrent iocbs pointing at the very same buffer. """ linux_aio = pytest.importorskip("caio.linux_aio") with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = linux_aio.Context(max_requests=4) op = linux_aio.Operation.write(b"x", fd, 0) first = ctx.submit(op) assert first == 1 second = ctx.submit(op) assert second == 0, "must not hand the kernel two concurrent iocbs for the same buffer" drain(ctx, first, timeout=2.0) def test_submit_same_operation_object_twice_in_one_batch_is_accepted_once(tmp_path, backend): """All four backends: submitting the same in-flight Operation object twice within a single submit() call must only ever be accepted once - not twice with two different RequestIds/dispatches silently overwriting each other. A prior bug (thread_aio/linux_aio/linux_uring) accepted both occurrences (each one's own already-in-progress check ran before either was actually marked), causing the underlying I/O to happen twice with only one completion ever delivered. """ path = tmp_path / "data.bin" fd = os.open(str(path), os.O_WRONLY | os.O_CREAT, 0o600) try: ctx = backend.Context(max_requests=8) op = backend.Operation.write(b"x", fd, 0) done = threading.Event() op.set_callback(lambda _res: done.set()) accepted = ctx.submit(op, op) assert accepted == 1, "the same Operation object must only be accepted once per submit() call" if hasattr(ctx, "process_events"): drain(ctx, 1) else: assert done.wait(timeout=5.0), "operation never completed" assert op.get_value() == 1 finally: os.close(fd) def test_resubmit_after_short_transfer_uses_original_size(tmp_path, polling_backend): """linux_uring, linux_aio: an operation that once got a short read must not be resubmittable at all (both are one-shot on the shared caio-core engine now - design/generalized-safe-design.md decision #4), and a fresh `Operation.read(nbytes, ...)` retrying the same read must still ask for the ORIGINAL requested size, not the smaller amount the short read actually transferred - otherwise a retried read would be silently capped forever, even once more data became available and a full-size transfer would succeed. No error involved - just a short read (fewer bytes available than requested) followed by more data actually showing up before the retry, so the only way the second read comes back short is if the request size itself got silently clamped. """ path = tmp_path / "data.bin" path.write_bytes(b"abcd") fd = os.open(str(path), os.O_RDONLY) try: ctx = polling_backend.Context(max_requests=8) op = polling_backend.Operation.read(8, fd, 0) assert ctx.submit(op) == 1 drain(ctx, 1) assert bytes(op.get_value()) == b"abcd", "expected a short read of the 4 available bytes" with open(str(path), "r+b") as f: f.seek(4) f.write(b"efgh") # One-shot: the same, already-completed Operation must not be # accepted again - silently skipped (submitted count 0). assert ctx.submit(op) == 0, "a completed one-shot Operation must not be resubmittable" retry = polling_backend.Operation.read(8, fd, 0) assert ctx.submit(retry) == 1 drain(ctx, 1) assert bytes(retry.get_value()) == b"abcdefgh", ( "a fresh Operation must request the full original size, never a shrunk one" ) finally: os.close(fd) def test_uring_failed_op_not_resubmittable_and_leaves_no_stale_error(tmp_path): """linux_uring only: an operation that failed at completion time (a real CQE with res=-EBADF, not an eager io_submit()-time raise - see below) must not be resubmittable afterward (one-shot, design decision #4), and a *fresh* Operation retried against the same, now-valid fd number must succeed with no trace of the previous attempt's error. This used to be tested by resubmitting the very same Operation object after fixing up the fd via dup2() (fd is fixed for an Operation's whole lifetime - see AbstractOperation's docstring) - that specific resubmit-after-failure path is now structurally impossible under the one-shot contract, so this instead checks the two properties that replace it: rejection of the stale object, and a clean fresh attempt. linux_uring only: a write against a closed fd fails at *completion* time here. linux_aio's io_submit() validates the fd eagerly and raises a Python exception immediately instead (confirmed by hand) rather than ever reaching its own, symmetric completion-time error handling - so the same scenario can't be driven through linux_aio's public API. """ linux_uring = pytest.importorskip("caio.linux_uring") bad_path = tmp_path / "closed.bin" good_path = tmp_path / "good.bin" ctx = linux_uring.Context(max_requests=8) # create ctx first: its own fds must not collide below closed_fd = os.open(str(bad_path), os.O_WRONLY | os.O_CREAT, 0o600) os.close(closed_fd) op = linux_uring.Operation.write(b"hello", closed_fd, 0) assert ctx.submit(op) == 1 drain(ctx, 1) assert op.error != 0, "expected the write against a closed fd to fail" with pytest.raises(SystemError): op.get_value() good_fd = os.open(str(good_path), os.O_WRONLY | os.O_CREAT, 0o600) try: # good_fd often already *is* closed_fd's number here (the OS tends # to hand out the lowest just-freed fd), making this a documented # POSIX no-op - but dup2() is what makes the aliasing deliberate # and correct regardless of which number the allocator happens to # pick. os.dup2(good_fd, closed_fd) assert ctx.submit(op) == 0, "a completed one-shot Operation must not be resubmittable, even after fd repair" retry = linux_uring.Operation.write(b"hello", closed_fd, 0) assert ctx.submit(retry) == 1 drain(ctx, 1) assert retry.error == 0, "a fresh Operation must carry no stale error from a prior, unrelated attempt" assert retry.get_value() == 5 finally: os.close(closed_fd) if good_fd != closed_fd: os.close(good_fd) def test_uring_short_read_tail_is_zeroed_not_heap_garbage(tmp_path): """linux_uring only: bytes past a short read's actual transfer count must be deterministic zero, not whatever was previously sitting in that heap memory. `PyBytes_FromStringAndSize(NULL, n)` leaves its buffer uninitialized, so this must be zeroed explicitly. Dirties the heap with distinguishable non-zero bytes first (allocating and freeing several same-sized buffers) to raise the odds an uninitialized allocation would visibly reuse that memory and fail this check - not a fully deterministic reproduction of the bug (heap reuse isn't guaranteed), but this assertion is unconditionally the correct behavior regardless. """ linux_uring = pytest.importorskip("caio.linux_uring") nbytes = 256 for _ in range(64): bytearray(b"\xff" * nbytes) path = tmp_path / "data.bin" path.write_bytes(b"AB") # far shorter than nbytes fd = os.open(str(path), os.O_RDONLY) try: ctx = linux_uring.Context(max_requests=8) op = linux_uring.Operation.read(nbytes, fd, 0) ctx.submit(op) drain(ctx, 1) assert bytes(op.get_value()) == b"AB" assert bytes(op.payload) == b"AB" + b"\x00" * (nbytes - 2), ( "untouched tail past a short read must be zero, not leftover heap contents" ) finally: os.close(fd) def test_uring_read_resubmit_rejected_and_previous_result_unmutated(tmp_path): """linux_uring only: a completed read Operation must not be resubmittable (one-shot, design decision #4) - so the bytes object a previous completion already handed out via get_value()/.payload can never be mutated by a later kernel write into the same buffer, since there is no later kernel write into it. A fresh Operation started against the same fd afterward must get its own, independent buffer. """ linux_uring = pytest.importorskip("caio.linux_uring") path = tmp_path / "data.bin" path.write_bytes(b"AAAA") fd = os.open(str(path), os.O_RDONLY) try: ctx = linux_uring.Context(max_requests=8) op = linux_uring.Operation.read(4, fd, 0) ctx.submit(op) drain(ctx, 1) first_result = op.get_value() assert bytes(first_result) == b"AAAA" first_hash = hash(first_result) with open(str(path), "r+b") as f: f.write(b"BBBB") assert ctx.submit(op) == 0, "a completed one-shot Operation must not be resubmittable" retry = linux_uring.Operation.read(4, fd, 0) ctx.submit(retry) drain(ctx, 1) second_result = retry.get_value() assert bytes(second_result) == b"BBBB" assert bytes(first_result) == b"AAAA", ( "a rejected resubmit must not mutate a bytes object already handed out by a previous completion" ) assert hash(first_result) == first_hash assert first_result is not second_result, ( "a fresh Operation must use its own buffer, not reuse a previous completion's object" ) finally: os.close(fd) def test_uring_submit_overflow_does_not_strand_or_leak_prior_ops(tmp_path): """linux_uring only: when a single submit() call overflows the SQ ring partway through a batch, every op accepted *before* the overflow point must still be genuinely visible to the kernel (sq_tail/outstanding published) and reclaimable - not permanently stuck "in flight" with an unpublished SQE and an unreachable leaked reference. max_requests=1 deliberately: with a bigger capacity, more than one op from the batch could be accepted before the overflow point, and this test's own later `drain(ctx, 1)` calls (which only wait for *some* completion, not a specific one) could then race and count one of those other, unrelated ops' completions instead of the one actually being asserted on - flaky, not a real bug. Capacity 1 makes "the op accepted before the overflow point" unambiguously just `ops[0]`. """ linux_uring = pytest.importorskip("caio.linux_uring") with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = linux_uring.Context(max_requests=1) ops = [linux_uring.Operation.write(b"x", fd, i) for i in range(4 * 64)] with pytest.raises(OverflowError): ctx.submit(*ops) drain(ctx, 1, timeout=2.0) assert ops[0].get_value() == 1, "the first op must have been accepted, submitted, and completed" # One-shot: the completed op from the overflowed batch must not be # resubmittable itself, but a fresh Operation must submit and # complete cleanly - the overflow must not have left the context # itself in some stuck or capacity-exhausted state. assert ctx.submit(ops[0]) == 0, "a completed one-shot Operation must not be resubmittable" retry = linux_uring.Operation.write(b"x", fd, 0) assert ctx.submit(retry) == 1 drain(ctx, 1, timeout=2.0) assert retry.get_value() == 1 def test_uring_reentrant_callback_does_not_double_consume_completion(tmp_path): """linux_uring only: drain_cq() must fully commit the completion ring's state (head + outstanding) *before* invoking any completion callback - a callback that reentrantly calls process_events()/flush() must never be able to observe (and reprocess) a CQE the outer, still-running drain has already consumed. Reprocessing it would reconstruct the same "kernel-owned" reference a second time, causing a double-decref once both reconstructions eventually drop. """ linux_uring = pytest.importorskip("caio.linux_uring") with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = linux_uring.Context(max_requests=8) op = linux_uring.Operation.write(b"x", fd, 0) reentrant_results = [] op.set_callback(lambda _res: reentrant_results.append(ctx.process_events())) ctx.submit(op) refcount_before = sys.getrefcount(op) # flush() both publishes the SQE to the kernel and drains whatever's # already completed inline - for a plain buffered write this is # *usually* immediate, but not guaranteed synchronous on every # kernel/filesystem. If it isn't, fall back to process_events() # with a real timeout: unlike flush(), it waits/polls the CQ ring # even when there's nothing new to submit (flush() has nothing left # to publish on a second call and returns 0 without even looking at # the CQ ring). ctx.flush() if not reentrant_results: ctx.process_events(max_requests=8, min_requests=1, timeout=5) assert reentrant_results, "operation never completed within the timeout" assert reentrant_results == [0], ( "a reentrant process_events() call from inside a completion callback " "must not see (and reprocess) the CQE the outer call just consumed" ) refcount_after = sys.getrefcount(op) assert refcount_after == refcount_before - 1, ( "drain must reclaim exactly one leaked reference for this completion, " "not be double-decremented by a reentrant re-consume" ) assert op.get_value() == 1 def test_linux_aio_submit_type_error_does_not_strand_earlier_ops(tmp_path): """linux_aio only: a wrong-typed argument later in the same submit() call must not leave an earlier, valid operation in that same call permanently stuck in_flight - it must never have been touched at all, since the whole call raises before anything reaches io_submit(). """ linux_aio = pytest.importorskip("caio.linux_aio") with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = linux_aio.Context(max_requests=4) op = linux_aio.Operation.write(b"x", fd, 0) with pytest.raises(TypeError): ctx.submit(op, object()) # Must not be stuck "in flight" - a normal submit must still work. submitted = ctx.submit(op) assert submitted == 1, "op from the rejected batch must not be permanently stuck in_flight" drain(ctx, 1, timeout=2.0) assert op.get_value() == 1 def test_linux_aio_process_events_max_requests_is_bounded(backend): """linux_aio only: process_events(max_requests=...) sizes an internal events buffer directly from this caller-controlled argument - an absurdly large value must raise cleanly instead of attempting a huge allocation (u32::MAX would be roughly 128 GiB). Runs in a subprocess: an unbounded attempt at that allocation could abort the whole process (Rust's global alloc-error handler, not a catchable panic) rather than just failing this one call, which would take the whole test session down with it. """ if backend.__name__ != "caio.linux_aio": pytest.skip("linux_aio-only") code = ( "import caio.linux_aio as m\n" "ctx = m.Context(max_requests=8)\n" "try:\n" " ctx.process_events(max_requests=2**32 - 1)\n" " print('RESULT: unexpectedly-succeeded')\n" "except OverflowError:\n" " print('RESULT: raised OverflowError')\n" ) proc = subprocess.run( [sys.executable, "-c", code], capture_output=True, text=True, timeout=30, check=False, ) assert proc.returncode == 0, ( f"process aborted/crashed (returncode={proc.returncode}); " f"stdout={proc.stdout!r} stderr={proc.stderr!r}" ) assert "RESULT: raised OverflowError" in proc.stdout, ( f"stdout={proc.stdout!r} stderr={proc.stderr!r}" ) @pytest.mark.flaky(reruns=3) def test_process_events_negative_timeout_waits_indefinitely(tmp_path, polling_backend): """process_events(timeout<0) means "wait indefinitely" - matching the native convention each backend's own blocking primitive already uses for exactly this (linux_aio's io_getevents() takes a NULL timespec; linux_uring's new eventfd-based wait passes -1 to poll()) - rather than being rejected outright, which was the previous behavior. Verified two ways: it must still be genuinely blocked with nothing submitted yet (not return early or misinterpret negative as "don't wait"), and it must actually wake up and return once a real completion satisfies min_requests, not stay stuck forever once given something to wait for. """ with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = polling_backend.Context(max_requests=8) result = {} def wait(): result["n"] = ctx.process_events(min_requests=1, timeout=-1) t = threading.Thread(target=wait) t.start() try: t.join(timeout=0.3) assert t.is_alive(), ( "process_events(timeout=-1) returned before any operation " "was even submitted - it must block, not treat a negative " "timeout as \"don't wait\"" ) op = polling_backend.Operation.write(b"x" * 4, fd, 0) ctx.submit(op) if hasattr(ctx, "flush"): ctx.flush() t.join(timeout=5.0) assert not t.is_alive(), ( "process_events(timeout=-1) never woke up for a real " "completion - it must not block forever once satisfied" ) finally: t.join(timeout=5.0) assert result.get("n", 0) >= 1 def test_uring_process_events_max_requests_bounds_callbacks_not_just_return_value(tmp_path): """linux_uring only: process_events(max_requests=N) must not run more than N callbacks synchronously, even though poll() itself always drains the *entire* CQ ring in one go (no peek-without-consume support) - anything beyond N must be held back (in a bridge-side pending queue) for a later call, not delivered anyway while only under-reporting the count the return value claims. Five writes, each blocked on its own full pipe (nothing reading), are submitted and flush()ed while still guaranteed not to have completed - flush() itself delivers nothing yet, confirmed below, so nothing has been delivered via any path before process_events() runs. All five are then unblocked together (every pipe drained), so they become real completions at roughly the same time, deterministically - letting max_requests bound exactly how many of them get delivered per call, not just how many get reported. """ linux_uring = pytest.importorskip("caio.linux_uring") import fcntl # Unix-only; this whole test is uring-only (Linux-only) F_SETPIPE_SZ = 1031 pipes = [os.pipe() for _ in range(5)] try: for r_fd, w_fd in pipes: fcntl.fcntl(w_fd, F_SETPIPE_SZ, 4096) os.write(w_fd, b"f" * 4096) # fill each pipe so the next write blocks ctx = linux_uring.Context(max_requests=16) called = [] ops = [] for i, (_r_fd, w_fd) in enumerate(pipes): # -1 (io_uring's convention for non-seekable files, same as # plain write(2)) - a pipe rejects an explicit offset outright. op = linux_uring.Operation.write(b"x", w_fd, 0xFFFFFFFFFFFFFFFF) op.set_callback(lambda _r, i=i: called.append(i)) ops.append(op) ctx.submit(*ops) found = ctx.flush() assert found == 0 and not called, ( "test setup bug: all 5 writes must still be blocked (pipes full) at this point" ) for r_fd, _w_fd in pipes: os.read(r_fd, 8192) time.sleep(0.05) # let the kernel actually post the completions first = ctx.process_events(max_requests=1, min_requests=0, timeout=0) assert first == 1, f"expected exactly 1 reported, got {first}" assert len(called) == 1, ( f"process_events(max_requests=1) must invoke exactly 1 callback, not {len(called)}" ) second = ctx.process_events(max_requests=10, min_requests=0, timeout=0) assert second == 4, f"expected the remaining 4 held over from before, got {second}" assert len(called) == 5, f"expected all 5 eventually delivered, got {len(called)}" finally: for r_fd, w_fd in pipes: os.close(r_fd) os.close(w_fd) def test_uring_process_events_min_requests_ignores_cancel_sentinel(tmp_path): """linux_uring only: process_events(min_requests=N) must wait for N *real* completions, not N raw CQEs - a cancel's own ASYNC_CANCEL sentinel CQE satisfies io_uring's own notion of "something completed" without being a real completion poll() would ever hand back. Without this, a stray sentinel already sitting in the ring lets the wait loop exit on its very first check (before real completions are even checked), well before a real completion that would actually satisfy min_requests has had a chance to arrive. Uses the same blocked-pipe-write + background-thread-drains-it-late technique as the Drop-reap sentinel test above: a write genuinely still in flight when process_events() is called, made to complete ~0.2s later. With the bug, the stray sentinel already in the ring would satisfy min_requests=1 immediately, so the call returns 0 real completions almost instantly, well before that 0.2s delay elapses. """ linux_uring = pytest.importorskip("caio.linux_uring") import fcntl # Unix-only; this whole test is uring-only (Linux-only) F_SETPIPE_SZ = 1031 with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() r_fd, w_fd = os.pipe() try: fcntl.fcntl(w_fd, F_SETPIPE_SZ, 4096) os.write(w_fd, b"f" * 4096) # fill the pipe so the next write blocks ctx = linux_uring.Context(max_requests=16) # A real op, completed and drained first, purely to generate a # cancel sentinel below - cancelling an already-finished op # still posts an ASYNC_CANCEL completion CQE for it. done_op = linux_uring.Operation.write(b"z", fd, 0) ctx.submit(done_op) drain(ctx, 1, timeout=5.0) ctx.cancel(done_op) time.sleep(0.05) assert ctx.poll() > 0, "cancel's own sentinel completion never got posted" # Genuinely still in flight when process_events() is called # below - the pipe is full and nothing is reading it. Offset # must be -1 (io_uring's convention for non-seekable files) - # a pipe rejects an explicit offset outright. blocked_op = linux_uring.Operation.write(b"pending", w_fd, 0xFFFFFFFFFFFFFFFF) ctx.submit(blocked_op) ctx.flush() drained_before_call_returned = threading.Event() def drain_pipe_late(): time.sleep(0.2) drained_before_call_returned.set() os.read(r_fd, 8192) t = threading.Thread(target=drain_pipe_late) t.start() try: n = ctx.process_events(max_requests=8, min_requests=1, timeout=5) observed_drained_flag = drained_before_call_returned.is_set() finally: t.join(timeout=5.0) assert n == 1, f"expected the one real completion, got n={n}" assert observed_drained_flag, ( "process_events(min_requests=1) returned before the real completion actually " "happened - a stray cancel sentinel must have satisfied min_requests early " "instead of being filtered out" ) finally: os.close(r_fd) os.close(w_fd) def test_uring_context_usable_from_a_different_thread(tmp_path): """linux_uring only: a Context constructed on one thread must be fully usable (submit/flush) from a different thread - this backend's Python API doesn't pin a Context to the thread that created it, so io_uring_setup() must not either. IORING_SETUP_SINGLE_ISSUER, on kernels that support it, does exactly that: it pins the ring to whichever task's io_uring_setup()/io_uring_enter() call created it and rejects submission from any other task with -EEXIST. """ linux_uring = pytest.importorskip("caio.linux_uring") with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = linux_uring.Context(max_requests=8) # created on the main thread op = linux_uring.Operation.write(b"hello", fd, 0) result = {} errors = [] def worker(): try: ctx.submit(op) drain(ctx, 1, timeout=5.0) result["bytes_written"] = op.get_value() except Exception as e: # noqa: BLE001 errors.append(e) t = threading.Thread(target=worker) t.start() t.join(timeout=10.0) assert not t.is_alive(), "worker thread did not finish in time" assert not errors, f"submit()/flush() from a different thread raised: {errors!r}" assert result.get("bytes_written") == 5 assert os.pread(fd, 5, 0) == b"hello" def test_uring_context_drop_does_not_invoke_callbacks(tmp_path): """Context teardown reaps outstanding operations (see the sentinel-CQE test above) to avoid unmapping the rings out from under still-pending kernel I/O, but it must not invoke Python callbacks while doing so - a destructor is a risky place to run arbitrary user code (interpreter shutdown ordering, reentrancy into the object being destroyed), and nothing about "waiting for the kernel to finish with this memory" requires it. The completion should still be applied (result/error available if some other reference to the Operation survives), just silently, with no callback call. """ linux_uring = pytest.importorskip("caio.linux_uring") with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = linux_uring.Context(max_requests=8) op = linux_uring.Operation.write(b"hello", fd, 0) invoked = [] op.set_callback(lambda res: invoked.append(res)) ctx.submit(op) del ctx gc.collect() assert not invoked, "Context teardown must not invoke operation callbacks" def test_thread_aio_drop_waits_for_a_genuinely_blocked_worker(tmp_path): """thread_aio only: dropping a Context whose single worker is still blocked in a real syscall (a `read()` on an empty pipe, not merely a task still sitting in the queue) must wait for that worker to actually finish, not give up the instant shutdown is signaled - the fix for the opposite bug (an *unbounded* wait that could hang forever if a worker never returned at all) must not have swung to the other extreme and started abandoning genuinely in-flight work immediately. A background thread makes the blocking read return shortly after Drop starts (proving it's genuinely still running, not merely queued), and teardown must not return before that write happens, but also must return well within a few seconds - nowhere near the full drop-reap deadline - once it does. """ pytest.importorskip("caio.thread_aio") import caio.thread_aio as m r_fd, w_fd = os.pipe() try: ctx = m.Context(max_requests=4, pool_size=1) op = m.Operation.read(1, r_fd, 0) ctx.submit(op) # Give the single worker a moment to actually enter the blocking # read() before Drop runs - otherwise it could still be sitting # in the queue and get cancelled per the "not yet started" path # instead of genuinely blocking. time.sleep(0.05) written_before_drop_returned = threading.Event() def write_after_delay(): time.sleep(0.2) written_before_drop_returned.set() os.write(w_fd, b"x") t = threading.Thread(target=write_after_delay) t.start() try: start = time.monotonic() del ctx gc.collect() elapsed = time.monotonic() - start finally: t.join(timeout=5.0) assert written_before_drop_returned.is_set(), ( "Context teardown returned before the blocked read actually completed - " "it must wait for genuinely in-flight work, not give up immediately" ) assert elapsed < 5.0, ( f"teardown took {elapsed:.2f}s - should return shortly after the real " "completion, not anywhere near the full drop-reap deadline" ) finally: os.close(r_fd) os.close(w_fd) def test_linux_aio_cancel_from_wrong_context_is_rejected(tmp_path): """linux_aio only: cancelling an Operation through a Context it was never submitted through must be rejected, not silently routed to a same-numbered but completely unrelated request in that other Context. Each Context's own request-ID sequence starts at 0 independently, so the very first operation submitted through any two Contexts both get local RequestId(0) - a bare RequestId alone can't tell them apart. caio-core pairs every ID with the ContextId that issued it, and checks it in Engine::cancel() before ever touching that Context's own registry. """ linux_aio = pytest.importorskip("caio.linux_aio") path_a = tmp_path / "a.bin" path_b = tmp_path / "b.bin" fd_a = os.open(str(path_a), os.O_WRONLY | os.O_CREAT, 0o600) fd_b = os.open(str(path_b), os.O_WRONLY | os.O_CREAT, 0o600) try: ctx_a = linux_aio.Context(max_requests=8) ctx_b = linux_aio.Context(max_requests=8) op_a = linux_aio.Operation.write(b"a" * 4096, fd_a, 0) op_b = linux_aio.Operation.write(b"b" * 4096, fd_b, 0) assert ctx_a.submit(op_a) == 1 assert ctx_b.submit(op_b) == 1 with pytest.raises(ValueError): ctx_b.cancel(op_a) # op_b, ctx_b's own unrelated request, must be completely # unaffected by the rejected cross-context cancel attempt. drain(ctx_b, 1) assert op_b.get_value() == 4096 drain(ctx_a, 1) assert op_a.get_value() == 4096 finally: os.close(fd_a) os.close(fd_b) def test_uring_cancel_from_wrong_context_does_not_disturb_other_context(tmp_path): """linux_uring only: same scenario as test_linux_aio_cancel_from_wrong_context_is_rejected, but linux_uring's cancel() is fire-and-forget (always returns 0, never raises) - so the only observable way to confirm the fix is that the *other* Context's own, same-numbered request completes normally and is left untouched by a cancel() call made through a different Context. """ linux_uring = pytest.importorskip("caio.linux_uring") path_a = tmp_path / "a.bin" path_b = tmp_path / "b.bin" fd_a = os.open(str(path_a), os.O_WRONLY | os.O_CREAT, 0o600) fd_b = os.open(str(path_b), os.O_WRONLY | os.O_CREAT, 0o600) try: ctx_a = linux_uring.Context(max_requests=8) ctx_b = linux_uring.Context(max_requests=8) op_a = linux_uring.Operation.write(b"a" * 4096, fd_a, 0) op_b = linux_uring.Operation.write(b"b" * 4096, fd_b, 0) assert ctx_a.submit(op_a) == 1 assert ctx_b.submit(op_b) == 1 # No manual flush() here - drain() below already calls it, and a # write against a regular file can complete inline during flush() # (see flush()'s own doc comment); flushing twice risks drain() # waiting forever for a completion flush() already delivered. assert ctx_b.cancel(op_a) == 0 drain(ctx_b, 1) assert op_b.get_value() == 4096, "op_b must complete normally, undisturbed by a cross-context cancel" drain(ctx_a, 1) assert op_a.get_value() == 4096 finally: os.close(fd_a) os.close(fd_b) caio-0.12.3/tests/test_thread_aio_specific.py000066400000000000000000000053511523515553700212460ustar00rootroot00000000000000""" Behavior specific to thread_aio's own native worker-thread-pool design - not a bug pattern shared with the other backends, so these don't belong in the cross-backend parametrized suite. Skipped outright wherever thread_aio itself isn't available. """ import threading import pytest from caio import thread_aio if thread_aio is None: pytest.skip("thread_aio backend not available on this platform", allow_module_level=True) def test_queue_overflow_allows_retry_with_original_data(tmp_path): """An operation rejected because the worker pool's queue is full must remain fully retryable afterward - not permanently stuck (as "already in progress" forever) and not silently missing its original payload on the later, successful resubmit. Submits 5 ops at once with capacity=1 (not just 2): the single worker thread *can* race to dequeue the very first one almost immediately (dequeuing is a plain C-side queue pop, not gated by the GIL at all) - but it can't dequeue a second one until the first fully *completes*, which needs the GIL this whole submit() call holds throughout. So at most one op can ever be raced away like that, making the LAST op in a 5-op batch guaranteed to overflow regardless of exactly how that race resolves for the first one. """ with open(str(tmp_path / "temp.bin"), "wb+") as f: fd = f.fileno() ctx = thread_aio.Context(max_requests=1, pool_size=1) payload = b"hello" ops = [thread_aio.Operation.write(payload, fd, i * len(payload)) for i in range(5)] # ops[0] is deterministically the very first one checked, so it's # always accepted (capacity=1, nothing else has run yet) - # retrying the rejected one only has anywhere to go once this one # actually finishes and the worker is free again. first_done = threading.Event() ops[0].set_callback(lambda _r: first_done.set()) with pytest.raises(RuntimeError): ctx.submit(*ops) assert first_done.wait(timeout=30.0), "the first op should have been accepted and completed" rejected = ops[-1] done = threading.Event() rejected.set_callback(lambda _r: done.set()) resubmitted = ctx.submit(rejected) assert resubmitted == 1, "a queue-rejected operation must not be permanently stuck" assert done.wait(timeout=30.0), "retried operation must actually run" assert rejected.result == len(payload), f"expected a full write, got result={rejected.result}" with open(str(tmp_path / "temp.bin"), "rb") as rf: rf.seek(4 * len(payload)) assert rf.read(len(payload)) == payload, ( "resubmitted operation must write its ORIGINAL payload, not lost/empty data" ) caio-0.12.3/tests/test_thread_aio_windows.py000066400000000000000000000060411523515553700211500ustar00rootroot00000000000000""" Windows-specific behavior of thread_aio's CRT interop (src/thread_aio/platform_io.rs). Everything here is skipped outright on non-Windows platforms. """ import ctypes import os import sys import threading import pytest def test_invalid_parameter_handler_is_not_process_wide(tmp_path): """The MSVC CRT invalid-parameter handler thread_aio installs around `_get_osfhandle` (needed so a closed/bogus fd raises a catchable error instead of aborting the process) must be scoped to the calling thread only, and restored immediately after - it must never replace the process-*wide* handler, which could otherwise silently change CRT error behavior for every other thread/extension in the process for as long as caio is imported. Installs a custom process-wide handler via ctypes first (simulating "some other code in the process already set one up"), then runs a normal thread_aio operation, then checks - still via ctypes - that the process-wide handler slot is untouched. """ if sys.platform != "win32": pytest.skip("Windows-only (thread_aio's CRT invalid-parameter-handler path)") from caio import thread_aio if thread_aio is None: pytest.skip("thread_aio backend not available on this platform") handler_t = ctypes.CFUNCTYPE( None, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint, ctypes.c_size_t, ) # The function lives in the modern Universal CRT (ucrtbase.dll), not the # legacy msvcrt.dll - matching what the Rust side actually links against # via a plain `extern "C"` declaration on current MSVC toolchains. ucrt = ctypes.CDLL("ucrtbase") ucrt._set_invalid_parameter_handler.restype = handler_t ucrt._set_invalid_parameter_handler.argtypes = [handler_t] @handler_t def my_handler(expression, function, file, line, reserved): pass previous = ucrt._set_invalid_parameter_handler(my_handler) try: path = tmp_path / "data.bin" path.write_bytes(b"hello") fd = os.open(str(path), os.O_RDONLY) try: ctx = thread_aio.Context(max_requests=4) op = thread_aio.Operation.read(5, fd, 0) done = threading.Event() op.set_callback(lambda _r: done.set()) ctx.submit(op) assert done.wait(timeout=5.0), "operation never completed" assert bytes(op.get_value()) == b"hello" finally: os.close(fd) # Read back the CURRENT process-wide handler (re-installing # my_handler as a side effect, which is fine - we restore the real # previous one in `finally` regardless). current = ucrt._set_invalid_parameter_handler(my_handler) current_ptr = ctypes.cast(current, ctypes.c_void_p).value my_handler_ptr = ctypes.cast(my_handler, ctypes.c_void_p).value assert current_ptr == my_handler_ptr, ( "a thread_aio operation must not replace the process-wide invalid-parameter handler" ) finally: ucrt._set_invalid_parameter_handler(previous) caio-0.12.3/uv.lock000066400000000000000000003123361523515553700140370ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.11'", "python_full_version < '3.11'", ] [[package]] name = "aiomisc" version = "17.10.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "colorlog" }, { name = "logging-journald", marker = "sys_platform == 'linux'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/c1/c3f0838d23e60eb7695a0054ac9a964db6d2b5f0649d96068e0515f5bb0b/aiomisc-17.10.3.tar.gz", hash = "sha256:db891af7cd96c78064d93956bb0ea9bdf44b878ee5f1026f1c3fac73e2cec556", size = 466598, upload-time = "2026-01-13T10:04:21.684Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/30/33/548305964fc871edc9f579a5a880946d09393cd966ba06ad6416449f3925/aiomisc-17.10.3-py3-none-any.whl", hash = "sha256:f05ca9fd3d8c3ff0cded91c7bfb439da87486c47fd94abcec3f3dac53734c15f", size = 92402, upload-time = "2026-01-13T10:04:19.779Z" }, ] [[package]] name = "aiomisc" version = "18.0.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", ] dependencies = [ { name = "aiothreads" }, { name = "colorlog" }, { name = "logging-journald", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2d/3f/8533fe0b8483f04c2289cec3da83f48ddbebfd5bd8618d006770e68bfe3d/aiomisc-18.0.6.tar.gz", hash = "sha256:bd282a1e5606010c787a2adecb37e7641115a8e23c5ec4a509de6d8800cef38d", size = 459177, upload-time = "2026-02-09T00:34:22.433Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/90/cb/0ebc7a2228764fb91bfb58a34397e1fb39d715cfd77b3b976a774f165680/aiomisc-18.0.6-py3-none-any.whl", hash = "sha256:2496e2842ed9f705801c623035d3676ea2a6b24208939cf6baf4f45d7fca10e7", size = 94603, upload-time = "2026-02-09T00:34:23.467Z" }, ] [[package]] name = "aiomisc-pytest" version = "1.3.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "aiomisc", version = "17.10.3", source = { registry = "https://pypi.org/simple" } }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/31/29/961940714a4c747a0454e511427959e114a819602250e1e934905f76d1f9/aiomisc_pytest-1.3.4.tar.gz", hash = "sha256:f4e7f5a5251c322221933ece5b97ab178a0395ffc006469a98304f83bf966c11", size = 11528, upload-time = "2025-07-30T09:42:58.711Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/11/08/829087efffff531bd825c6cc781b50a43022c0e740d99ed75ec6b8dc1e3f/aiomisc_pytest-1.3.4-py3-none-any.whl", hash = "sha256:ab3eba9515759330ab0b74a8f7432e9b1b00019df98bdcbfffebb5116bd10bed", size = 10622, upload-time = "2025-07-30T09:42:57.948Z" }, ] [[package]] name = "aiomisc-pytest" version = "2.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", ] dependencies = [ { name = "aiomisc", version = "18.0.6", source = { registry = "https://pypi.org/simple" } }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/41/083ee3de2aafdf20895bac41a535f3425852098a5130ed1681ce2a57ef9e/aiomisc_pytest-2.0.0.tar.gz", hash = "sha256:71b62b90ee984523c57e18e0757cee540db063947212659c1be9b2199bb54c65", size = 6032, upload-time = "2026-02-09T00:09:06.732Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/9c/552232841a802575d001774ea8dbc551f20567d51bfa1845a866b44029f4/aiomisc_pytest-2.0.0-py3-none-any.whl", hash = "sha256:63a7865ad08430c4d8e7ed6fb8559c434ab205601710eab1f2c39b6cba984581", size = 5304, upload-time = "2026-02-09T00:09:07.931Z" }, ] [[package]] name = "aiothreads" version = "1.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/32/08/7e65a6bf0baa504587cb28fd4430778ddbdf8b7ccd0f5d92ddd156799cdf/aiothreads-1.1.1.tar.gz", hash = "sha256:cb2fcecc5177251c014d89fb5bf52a5b3acf9b64e8ef5e35a7981f196d610684", size = 116313, upload-time = "2026-03-03T17:25:57.251Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/57/6e/53744d2d9b7856c5f8bd8a7c6958edcb70b755a2cad29188a9784c61a8f5/aiothreads-1.1.1-py3-none-any.whl", hash = "sha256:2c94d9cb364a0b4ee65845691049d06f5d43751766b21550a063f3d84347d640", size = 22852, upload-time = "2026-03-03T17:25:55.678Z" }, ] [[package]] name = "annotated-doc" version = "0.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] name = "caio" version = "0.10.2" source = { editable = "." } [package.optional-dependencies] develop = [ { name = "aiomisc-pytest", version = "1.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "aiomisc-pytest", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "coveralls" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pytest-cov" }, { name = "pytest-rerunfailures" }, { name = "setuptools" }, ] [package.dev-dependencies] dev = [ { name = "rich" }, ] [package.metadata] requires-dist = [ { name = "aiomisc-pytest", marker = "extra == 'develop'" }, { name = "coveralls", marker = "extra == 'develop'" }, { name = "pytest", marker = "extra == 'develop'" }, { name = "pytest-cov", marker = "extra == 'develop'" }, { name = "pytest-rerunfailures", marker = "extra == 'develop'" }, { name = "setuptools", marker = "extra == 'develop'" }, ] provides-extras = ["develop"] [package.metadata.requires-dev] dev = [{ name = "rich" }] [[package]] name = "certifi" version = "2026.2.25" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[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 = "colorlog" version = "6.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, ] [[package]] name = "coverage" version = "7.13.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "coveralls" version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "requests" }, { name = "typer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/16/aa263cb5450de470234f5176ce42664c5e0535aab1a2094b739447065797/coveralls-4.1.0.tar.gz", hash = "sha256:dab364025ba80cbb95ce56c6fc62cd9172d7fd637060ea235dde99d9b46a4494", size = 12736, upload-time = "2026-02-28T15:16:47.591Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/60/f1/5a884d2e8e3e7e7d854d3032b777bd22aad1598161cf9aa77ac37d684908/coveralls-4.1.0-py3-none-any.whl", hash = "sha256:bfacfda2d443c24fc90d67035027cec15015fff2dbd036427e8bf8f4953dda2e", size = 14168, upload-time = "2026-02-28T15:16:46.181Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] [[package]] name = "idna" version = "3.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "logging-journald" version = "0.6.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/07/77567e0001d3acffddc80470497f0eec3ccb544b72e672550b3c1f02e586/logging_journald-0.6.11.tar.gz", hash = "sha256:1741ae42cf8953e435489a82acf0e88bc21ad23351e867ca4453a354fc6d09e1", size = 5432, upload-time = "2024-12-13T08:17:10.147Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/7b/033a15aea43108af5049d8c0a09c3ea30519295aee1f3ed74c9efa1ee4cb/logging_journald-0.6.11-py3-none-any.whl", hash = "sha256:4d280eb0bddb4bf616297ee2428592b037618b6f0169c873f655aff92496c232", size = 5619, upload-time = "2024-12-13T08:17:09.127Z" }, ] [[package]] name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] 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 = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[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 = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, { name = "tomli" }, ] 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" version = "9.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", ] dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] 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 = "pytest-rerunfailures" version = "16.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/60/a90ca1cc6cffcb97b4260ed0ad2b7934b999d7c48abe4ea0840344862a3b/pytest_rerunfailures-16.4.tar.gz", hash = "sha256:8222d17c37eb7b9e4d6fc96a3c724ff4e1a5c97a5cc7cbb2c19e9282cfd21a11", size = 36635, upload-time = "2026-07-01T06:30:56.813Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ce/93/3cdcc4033444e822e01b573414b03fd37fd5533070c750477b8f5fa5224b/pytest_rerunfailures-16.4-py3-none-any.whl", hash = "sha256:f69b5beb39622c90d1e44bd945d826eff6db545dcf0b68f52b7e4ad15eaf6d6c", size = 16955, upload-time = "2026-07-01T06:30:55.333Z" }, ] [[package]] name = "requests" version = "2.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, ] [[package]] name = "rich" version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] name = "setuptools" version = "82.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "tomli" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "typer" version = "0.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "click" }, { name = "rich" }, { name = "shellingham" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, ] [[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.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ]