diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffe20f82..5201a518 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - release-0.x pull_request: concurrency: @@ -14,8 +15,8 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v5 with: python-version: "3.12" - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 @@ -26,7 +27,7 @@ jobs: name: Lint Commit Messages runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6 @@ -41,6 +42,8 @@ jobs: - "3.11" - "3.12" - "3.13" + - "3.14" + - "3.14t" - "pypy-3.9" - "pypy-3.10" os: @@ -65,11 +68,11 @@ jobs: python-version: "pypy-3.10" runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 - name: Install poetry run: pipx install poetry - name: Set up Python - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v5 with: python-version: ${{ matrix.python-version }} cache: "poetry" @@ -87,16 +90,16 @@ jobs: - name: Test with Pytest run: poetry run pytest --durations=20 --timeout=60 -v --cov=zeroconf --cov-branch --cov-report xml --cov-report html --cov-report term-missing tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5 with: token: ${{ secrets.CODECOV_TOKEN }} benchmark: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 - name: Setup Python 3.13 - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v5 with: python-version: 3.13 - uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 @@ -105,10 +108,11 @@ jobs: REQUIRE_CYTHON=1 poetry install --only=main,dev shell: bash - name: Run benchmarks - uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3 + uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2 # v3 with: token: ${{ secrets.CODSPEED_TOKEN }} run: poetry run pytest --no-cov -vvvvv --codspeed tests/benchmarks + mode: instrumentation release: needs: @@ -128,28 +132,28 @@ jobs: newest_release_tag: ${{ steps.release.outputs.tag }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 with: fetch-depth: 0 ref: ${{ github.head_ref || github.ref_name }} # Do a dry run of PSR - name: Test release - uses: python-semantic-release/python-semantic-release@26bb37cfab71a5a372e3db0f48a6eac57519a4a6 # v9.21.0 + uses: python-semantic-release/python-semantic-release@350c48fcb3ffcdfd2e0a235206bc2ecea6b69df0 # v10.5.3 if: github.ref_name != 'master' with: - root_options: --noop + no_operation_mode: true # On main branch: actual PSR + upload to PyPI & GitHub - name: Release - uses: python-semantic-release/python-semantic-release@26bb37cfab71a5a372e3db0f48a6eac57519a4a6 # v9.21.0 + uses: python-semantic-release/python-semantic-release@350c48fcb3ffcdfd2e0a235206bc2ecea6b69df0 # v10.5.3 id: release if: github.ref_name == 'master' with: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 if: steps.release.outputs.released == 'true' - name: Publish package distributions to GitHub Releases @@ -170,7 +174,7 @@ jobs: [ ubuntu-24.04-arm, ubuntu-latest, - windows-2019, + windows-latest, macos-13, macos-latest, ] @@ -204,6 +208,14 @@ jobs: qemu: armv7l musl: "musllinux" pyver: cp313 + - os: ubuntu-latest + qemu: armv7l + musl: "musllinux" + pyver: cp314 + - os: ubuntu-latest + qemu: armv7l + musl: "musllinux" + pyver: cp314t # qemu is slow, make a single # runner per Python version - os: ubuntu-latest @@ -226,20 +238,28 @@ jobs: qemu: armv7l musl: "" pyver: cp313 + - os: ubuntu-latest + qemu: armv7l + musl: "" + pyver: cp314 + - os: ubuntu-latest + qemu: armv7l + musl: "" + pyver: cp314t steps: - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 with: fetch-depth: 0 ref: "master" # Used to host cibuildwheel - name: Set up Python - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v5 with: python-version: "3.12" - name: Set up QEMU if: ${{ matrix.qemu }} - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 with: platforms: all # This should be temporary @@ -262,20 +282,20 @@ jobs: echo "CIBW_BUILD=${{ matrix.pyver }}*" >> $GITHUB_ENV fi - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 with: ref: ${{ needs.release.outputs.newest_release_tag }} fetch-depth: 0 - name: Build wheels ${{ matrix.musl }} (${{ matrix.qemu }}) - uses: pypa/cibuildwheel@d04cacbc9866d432033b1d09142936e6a0e2121a # v2.23.2 + uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0 # to supply options, put them in 'env', like: env: CIBW_SKIP: cp36-* cp37-* pp36-* pp37-* pp38-* cp38-* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} CIBW_BEFORE_ALL_LINUX: apt install -y gcc || yum install -y gcc || apk add gcc REQUIRE_CYTHON: 1 - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v4 with: path: ./wheelhouse/*.whl name: wheels-${{ matrix.os }}-${{ matrix.musl }}-${{ matrix.qemu }}-${{ matrix.pyver }} @@ -288,7 +308,7 @@ jobs: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v4 with: # unpacks default artifact into dist/ # if `name: artifact` is omitted, the action will create extra parent dir @@ -297,4 +317,4 @@ jobs: merge-multiple: true - uses: - pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf19bfa2..7bc839b8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,12 +9,12 @@ ci: repos: - repo: https://github.com/commitizen-tools/commitizen - rev: v4.4.1 + rev: v4.13.10 hooks: - id: commitizen stages: [commit-msg] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-builtin-literals - id: check-case-conflict @@ -35,31 +35,31 @@ repos: args: ["--tab-width", "2"] files: ".(css|html|js|json|md|toml|yaml)$" - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.2 + rev: v0.15.12 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/codespell-project/codespell - rev: v2.4.1 + rev: v2.4.2 hooks: - id: codespell - repo: https://github.com/PyCQA/flake8 - rev: 7.1.2 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 + rev: v1.20.2 hooks: - id: mypy additional_dependencies: [ifaddr] - repo: https://github.com/MarcoGorelli/cython-lint - rev: v0.16.6 + rev: v0.19.0 hooks: - id: cython-lint - id: double-quote-cython-strings diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ffa0f63..d34c53e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,87 @@ # CHANGELOG +## v0.147.3 (2025-10-04) + +### Bug Fixes + +- Update poetry to v2 ([#1623](https://github.com/python-zeroconf/python-zeroconf/pull/1623), + [`2c3c296`](https://github.com/python-zeroconf/python-zeroconf/commit/2c3c29655bd365213a7e0a4360b8dd860d833470)) + + +## v0.147.2 (2025-09-05) + +### Bug Fixes + +- Missing wheel builds for Windows + ([#1613](https://github.com/python-zeroconf/python-zeroconf/pull/1613), + [`f8e2381`](https://github.com/python-zeroconf/python-zeroconf/commit/f8e2381a500c78dcefeba3772822d5d3ec5f6060)) + + +## v0.147.1 (2025-09-05) + +### Bug Fixes + +- Increase check time and add random wait to avoid service collisions + ([#1611](https://github.com/python-zeroconf/python-zeroconf/pull/1611), + [`8c382ee`](https://github.com/python-zeroconf/python-zeroconf/commit/8c382eedc6da80031d9a7a42f299f95f115b7e47)) + +Co-authored-by: J. Nick Koston + + +## v0.147.0 (2025-05-03) + +### Features + +- Add cython 3.1 support ([#1580](https://github.com/python-zeroconf/python-zeroconf/pull/1580), + [`1d9c94a`](https://github.com/python-zeroconf/python-zeroconf/commit/1d9c94a82d8da16b8f5355131e6167b69293da6c)) + +- Cython 3.1 support ([#1578](https://github.com/python-zeroconf/python-zeroconf/pull/1578), + [`daaf8d6`](https://github.com/python-zeroconf/python-zeroconf/commit/daaf8d6981c778fe4ba0a63371d9368cf217891a)) + +- Cython 3.11 support ([#1579](https://github.com/python-zeroconf/python-zeroconf/pull/1579), + [`1569383`](https://github.com/python-zeroconf/python-zeroconf/commit/1569383c6cf8ce8977427cfdaf5c7104ce52ab08)) + + +## v0.146.5 (2025-04-14) + +### Bug Fixes + +- Address non-working socket configuration + ([#1563](https://github.com/python-zeroconf/python-zeroconf/pull/1563), + [`cc0f835`](https://github.com/python-zeroconf/python-zeroconf/commit/cc0f8350c30c82409b1a9bfecb19ff9b3368d6a7)) + +Co-authored-by: J. Nick Koston + + +## v0.146.4 (2025-04-14) + +### Bug Fixes + +- Avoid loading adapter list twice + ([#1564](https://github.com/python-zeroconf/python-zeroconf/pull/1564), + [`8359488`](https://github.com/python-zeroconf/python-zeroconf/commit/83594887521507cf77bfc0a397becabaaab287c2)) + + +## v0.146.3 (2025-04-02) + +### Bug Fixes + +- Correctly override question type flag for requests + ([#1558](https://github.com/python-zeroconf/python-zeroconf/pull/1558), + [`bd643a2`](https://github.com/python-zeroconf/python-zeroconf/commit/bd643a227bc4d6a949d558850ad1431bc2940d74)) + +* fix: correctly override question type flag for requests + +Currently even when setting the explicit question type flag, the implementation ignores it for + subsequent queries. This commit ensures that all queries respect the explicit question type flag. + +* chore(tests): add test for explicit question type flag + +Add unit test to validate that the explicit question type flag is set correctly in outgoing + requests. + + ## v0.146.2 (2025-04-01) ### Bug Fixes diff --git a/build_ext.py b/build_ext.py index ff088f83..412bff3c 100644 --- a/build_ext.py +++ b/build_ext.py @@ -56,7 +56,7 @@ def build(setup_kwargs: Any) -> None: if os.environ.get("SKIP_CYTHON"): return try: - from Cython.Build import cythonize + from Cython.Build import cythonize # noqa: PLC0415 setup_kwargs.update( { diff --git a/poetry.lock b/poetry.lock index 845974d6..12f2f34f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -27,6 +27,19 @@ files = [ [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] + [[package]] name = "certifi" version = "2025.1.31" @@ -236,75 +249,100 @@ files = [ [[package]] name = "coverage" -version = "7.6.12" +version = "7.10.6" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, - {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, - {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, - {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, - {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, - {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, - {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, - {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, - {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, - {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, - {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, - {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, - {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, - {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, - {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, - {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, - {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, + {file = "coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356"}, + {file = "coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301"}, + {file = "coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460"}, + {file = "coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd"}, + {file = "coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb"}, + {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6"}, + {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945"}, + {file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e"}, + {file = "coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1"}, + {file = "coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528"}, + {file = "coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f"}, + {file = "coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc"}, + {file = "coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a"}, + {file = "coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a"}, + {file = "coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62"}, + {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153"}, + {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5"}, + {file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619"}, + {file = "coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba"}, + {file = "coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e"}, + {file = "coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c"}, + {file = "coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea"}, + {file = "coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634"}, + {file = "coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6"}, + {file = "coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9"}, + {file = "coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c"}, + {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a"}, + {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5"}, + {file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972"}, + {file = "coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d"}, + {file = "coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629"}, + {file = "coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80"}, + {file = "coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6"}, + {file = "coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80"}, + {file = "coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003"}, + {file = "coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27"}, + {file = "coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4"}, + {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d"}, + {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc"}, + {file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc"}, + {file = "coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e"}, + {file = "coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32"}, + {file = "coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2"}, + {file = "coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b"}, + {file = "coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393"}, + {file = "coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27"}, + {file = "coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df"}, + {file = "coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb"}, + {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282"}, + {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4"}, + {file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21"}, + {file = "coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0"}, + {file = "coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5"}, + {file = "coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b"}, + {file = "coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e"}, + {file = "coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb"}, + {file = "coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034"}, + {file = "coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1"}, + {file = "coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a"}, + {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb"}, + {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d"}, + {file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747"}, + {file = "coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5"}, + {file = "coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713"}, + {file = "coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32"}, + {file = "coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65"}, + {file = "coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6"}, + {file = "coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0"}, + {file = "coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e"}, + {file = "coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5"}, + {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7"}, + {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5"}, + {file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0"}, + {file = "coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7"}, + {file = "coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930"}, + {file = "coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b"}, + {file = "coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352"}, + {file = "coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612"}, + {file = "coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b"}, + {file = "coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144"}, + {file = "coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b"}, + {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862"}, + {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2"}, + {file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78"}, + {file = "coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c"}, + {file = "coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf"}, + {file = "coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3"}, + {file = "coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90"}, ] [package.dependencies] @@ -315,76 +353,51 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cython" -version = "3.0.12" +version = "3.2.4" description = "The Cython compiler for writing C extensions in the Python language." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "Cython-3.0.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba67eee9413b66dd9fbacd33f0bc2e028a2a120991d77b5fd4b19d0b1e4039b9"}, - {file = "Cython-3.0.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee2717e5b5f7d966d0c6e27d2efe3698c357aa4d61bb3201997c7a4f9fe485a"}, - {file = "Cython-3.0.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cffc3464f641c8d0dda942c7c53015291beea11ec4d32421bed2f13b386b819"}, - {file = "Cython-3.0.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d3a8f81980ffbd74e52f9186d8f1654e347d0c44bfea6b5997028977f481a179"}, - {file = "Cython-3.0.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8d32856716c369d01f2385ad9177cdd1a11079ac89ea0932dc4882de1aa19174"}, - {file = "Cython-3.0.12-cp310-cp310-win32.whl", hash = "sha256:712c3f31adec140dc60d064a7f84741f50e2c25a8edd7ae746d5eb4d3ef7072a"}, - {file = "Cython-3.0.12-cp310-cp310-win_amd64.whl", hash = "sha256:d6945694c5b9170cfbd5f2c0d00ef7487a2de7aba83713a64ee4ebce7fad9e05"}, - {file = "Cython-3.0.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feb86122a823937cc06e4c029d80ff69f082ebb0b959ab52a5af6cdd271c5dc3"}, - {file = "Cython-3.0.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfdbea486e702c328338314adb8e80f5f9741f06a0ae83aaec7463bc166d12e8"}, - {file = "Cython-3.0.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563de1728c8e48869d2380a1b76bbc1b1b1d01aba948480d68c1d05e52d20c92"}, - {file = "Cython-3.0.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d4576c1e1f6316282aa0b4a55139254fbed965cba7813e6d9900d3092b128"}, - {file = "Cython-3.0.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1e5eadef80143026944ea8f9904715a008f5108d1d644a89f63094cc37351e73"}, - {file = "Cython-3.0.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a93cbda00a5451175b97dea5a9440a3fcee9e54b4cba7a7dbcba9a764b22aec"}, - {file = "Cython-3.0.12-cp311-cp311-win32.whl", hash = "sha256:3109e1d44425a2639e9a677b66cd7711721a5b606b65867cb2d8ef7a97e2237b"}, - {file = "Cython-3.0.12-cp311-cp311-win_amd64.whl", hash = "sha256:d4b70fc339adba1e2111b074ee6119fe9fd6072c957d8597bce9a0dd1c3c6784"}, - {file = "Cython-3.0.12-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fe030d4a00afb2844f5f70896b7f2a1a0d7da09bf3aa3d884cbe5f73fff5d310"}, - {file = "Cython-3.0.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7fec4f052b8fe173fe70eae75091389955b9a23d5cec3d576d21c5913b49d47"}, - {file = "Cython-3.0.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0faa5e39e5c8cdf6f9c3b1c3f24972826e45911e7f5b99cf99453fca5432f45e"}, - {file = "Cython-3.0.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d53de996ed340e9ab0fc85a88aaa8932f2591a2746e1ab1c06e262bd4ec4be7"}, - {file = "Cython-3.0.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea3a0e19ab77266c738aa110684a753a04da4e709472cadeff487133354d6ab8"}, - {file = "Cython-3.0.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c151082884be468f2f405645858a857298ac7f7592729e5b54788b5c572717ba"}, - {file = "Cython-3.0.12-cp312-cp312-win32.whl", hash = "sha256:3083465749911ac3b2ce001b6bf17f404ac9dd35d8b08469d19dc7e717f5877a"}, - {file = "Cython-3.0.12-cp312-cp312-win_amd64.whl", hash = "sha256:c0b91c7ebace030dd558ea28730de8c580680b50768e5af66db2904a3716c3e3"}, - {file = "Cython-3.0.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4ee6f1ea1bead8e6cbc4e64571505b5d8dbdb3b58e679d31f3a84160cebf1a1a"}, - {file = "Cython-3.0.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57aefa6d3341109e46ec1a13e3a763aaa2cbeb14e82af2485b318194be1d9170"}, - {file = "Cython-3.0.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:879ae9023958d63c0675015369384642d0afb9c9d1f3473df9186c42f7a9d265"}, - {file = "Cython-3.0.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36fcd584dae547de6f095500a380f4a0cce72b7a7e409e9ff03cb9beed6ac7a1"}, - {file = "Cython-3.0.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62b79dcc0de49efe9e84b9d0e2ae0a6fc9b14691a65565da727aa2e2e63c6a28"}, - {file = "Cython-3.0.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4aa255781b093a8401109d8f2104bbb2e52de7639d5896aefafddc85c30e0894"}, - {file = "Cython-3.0.12-cp313-cp313-win32.whl", hash = "sha256:77d48f2d4bab9fe1236eb753d18f03e8b2619af5b6f05d51df0532a92dfb38ab"}, - {file = "Cython-3.0.12-cp313-cp313-win_amd64.whl", hash = "sha256:86c304b20bd57c727c7357e90d5ba1a2b6f1c45492de2373814d7745ef2e63b4"}, - {file = "Cython-3.0.12-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ff5c0b6a65b08117d0534941d404833d516dac422eee88c6b4fd55feb409a5ed"}, - {file = "Cython-3.0.12-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:680f1d6ed4436ae94805db264d6155ed076d2835d84f20dcb31a7a3ad7f8668c"}, - {file = "Cython-3.0.12-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc24609613fa06d0d896309f7164ba168f7e8d71c1e490ed2a08d23351c3f41"}, - {file = "Cython-3.0.12-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1879c073e2b34924ce9b7ca64c212705dcc416af4337c45f371242b2e5f6d32"}, - {file = "Cython-3.0.12-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:bfb75123dd4ff767baa37d7036da0de2dfb6781ff256eef69b11b88b9a0691d1"}, - {file = "Cython-3.0.12-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:f39640f8df0400cde6882e23c734f15bb8196de0a008ae5dc6c8d1ec5957d7c8"}, - {file = "Cython-3.0.12-cp36-cp36m-win32.whl", hash = "sha256:8c9efe9a0895abee3cadfdad4130b30f7b5e57f6e6a51ef2a44f9fc66a913880"}, - {file = "Cython-3.0.12-cp36-cp36m-win_amd64.whl", hash = "sha256:63d840f2975e44d74512f8f34f1f7cb8121c9428e26a3f6116ff273deb5e60a2"}, - {file = "Cython-3.0.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:75c5acd40b97cff16fadcf6901a91586cbca5dcdba81f738efaf1f4c6bc8dccb"}, - {file = "Cython-3.0.12-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62564457851db1c40399bd95a5346b9bb99e17a819bf583b362f418d8f3457a"}, - {file = "Cython-3.0.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ccd1228cc203b1f1b8a3d403f5a20ad1c40e5879b3fbf5851ce09d948982f2c"}, - {file = "Cython-3.0.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25529ee948f44d9a165ff960c49d4903267c20b5edf2df79b45924802e4cca6e"}, - {file = "Cython-3.0.12-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:90cf599372c5a22120609f7d3a963f17814799335d56dd0dcf8fe615980a8ae1"}, - {file = "Cython-3.0.12-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9f8c48748a9c94ea5d59c26ab49ad0fad514d36f894985879cf3c3ca0e600bf4"}, - {file = "Cython-3.0.12-cp37-cp37m-win32.whl", hash = "sha256:3e4fa855d98bc7bd6a2049e0c7dc0dcf595e2e7f571a26e808f3efd84d2db374"}, - {file = "Cython-3.0.12-cp37-cp37m-win_amd64.whl", hash = "sha256:120681093772bf3600caddb296a65b352a0d3556e962b9b147efcfb8e8c9801b"}, - {file = "Cython-3.0.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:731d719423e041242c9303c80cae4327467299b90ffe62d4cc407e11e9ea3160"}, - {file = "Cython-3.0.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3238a29f37999e27494d120983eca90d14896b2887a0bd858a381204549137a"}, - {file = "Cython-3.0.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b588c0a089a9f4dd316d2f9275230bad4a7271e5af04e1dc41d2707c816be44b"}, - {file = "Cython-3.0.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab9f5198af74eb16502cc143cdde9ca1cbbf66ea2912e67440dd18a36e3b5fa"}, - {file = "Cython-3.0.12-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8ee841c0e114efa1e849c281ac9b8df8aa189af10b4a103b1c5fd71cbb799679"}, - {file = "Cython-3.0.12-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:43c48b5789398b228ea97499f5b864843ba9b1ab837562a9227c6f58d16ede8b"}, - {file = "Cython-3.0.12-cp38-cp38-win32.whl", hash = "sha256:5e5f17c48a4f41557fbcc7ee660ccfebe4536a34c557f553b6893c1b3c83df2d"}, - {file = "Cython-3.0.12-cp38-cp38-win_amd64.whl", hash = "sha256:309c081057930bb79dc9ea3061a1af5086c679c968206e9c9c2ec90ab7cb471a"}, - {file = "Cython-3.0.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54115fcc126840926ff3b53cfd2152eae17b3522ae7f74888f8a41413bd32f25"}, - {file = "Cython-3.0.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629db614b9c364596d7c975fa3fb3978e8c5349524353dbe11429896a783fc1e"}, - {file = "Cython-3.0.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af081838b0f9e12a83ec4c3809a00a64c817f489f7c512b0e3ecaf5f90a2a816"}, - {file = "Cython-3.0.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:34ce459808f7d8d5d4007bc5486fe50532529096b43957af6cbffcb4d9cc5c8d"}, - {file = "Cython-3.0.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d6c6cd6a75c8393e6805d17f7126b96a894f310a1a9ea91c47d141fb9341bfa8"}, - {file = "Cython-3.0.12-cp39-cp39-win32.whl", hash = "sha256:a4032e48d4734d2df68235d21920c715c451ac9de15fa14c71b378e8986b83be"}, - {file = "Cython-3.0.12-cp39-cp39-win_amd64.whl", hash = "sha256:dcdc3e5d4ce0e7a4af6903ed580833015641e968d18d528d8371e2435a34132c"}, - {file = "Cython-3.0.12-py2.py3-none-any.whl", hash = "sha256:0038c9bae46c459669390e53a1ec115f8096b2e4647ae007ff1bf4e6dee92806"}, - {file = "cython-3.0.12.tar.gz", hash = "sha256:b988bb297ce76c671e28c97d017b95411010f7c77fa6623dd0bb47eed1aee1bc"}, + {file = "cython-3.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb0cc0f23b9874ad262d7d2b9560aed9c7e2df07b49b920bda6f2cc9cb505e"}, + {file = "cython-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f136f379a4a54246facd0eb6f1ee15c3837cb314ce87b677582ec014db4c6845"}, + {file = "cython-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35ab0632186057406ec729374c737c37051d2eacad9d515d94e5a3b3e58a9b02"}, + {file = "cython-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ca2399dc75796b785f74fb85c938254fa10c80272004d573c455f9123eceed86"}, + {file = "cython-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff9af2134c05e3734064808db95b4dd7341a39af06e8945d05ea358e1741aaed"}, + {file = "cython-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67922c9de058a0bfb72d2e75222c52d09395614108c68a76d9800f150296ddb3"}, + {file = "cython-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b362819d155fff1482575e804e43e3a8825332d32baa15245f4642022664a3f4"}, + {file = "cython-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a64a112a34ec719b47c01395647e54fb4cf088a511613f9a3a5196694e8e382"}, + {file = "cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9"}, + {file = "cython-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:869487ea41d004f8b92171f42271fbfadb1ec03bede3158705d16cd570d6b891"}, + {file = "cython-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55b6c44cd30821f0b25220ceba6fe636ede48981d2a41b9bbfe3c7902ce44ea7"}, + {file = "cython-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:767b143704bdd08a563153448955935844e53b852e54afdc552b43902ed1e235"}, + {file = "cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0"}, + {file = "cython-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03893c88299a2c868bb741ba6513357acd104e7c42265809fd58dce1456a36fc"}, + {file = "cython-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f81eda419b5ada7b197bbc3c5f4494090e3884521ffd75a3876c93fbf66c9ca8"}, + {file = "cython-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:83266c356c13c68ffe658b4905279c993d8a5337bb0160fa90c8a3e297ea9a2e"}, + {file = "cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa"}, + {file = "cython-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3b5ac54e95f034bc7fb07313996d27cbf71abc17b229b186c1540942d2dc28e"}, + {file = "cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f43be4eaa6afd58ce20d970bb1657a3627c44e1760630b82aa256ba74b4acb"}, + {file = "cython-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:983f9d2bb8a896e16fa68f2b37866ded35fa980195eefe62f764ddc5f9f5ef8e"}, + {file = "cython-3.2.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55eb425c0baf1c8a46aa4424bc35b709db22f3c8a1de33adb3ecb8a3d54ea42a"}, + {file = "cython-3.2.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f583cad7a7eed109f0babb5035e92d0c1260598f53add626a8568b57246b62c3"}, + {file = "cython-3.2.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:72e6c0bbd978e2678b45351395f6825b9b8466095402eae293f4f7a73e9a3e85"}, + {file = "cython-3.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:14dae483ca2838b287085ff98bc206abd7a597b7bb16939a092f8e84d9062842"}, + {file = "cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf"}, + {file = "cython-3.2.4-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6d5267f22b6451eb1e2e1b88f6f78a2c9c8733a6ddefd4520d3968d26b824581"}, + {file = "cython-3.2.4-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b6e58f73a69230218d5381817850ce6d0da5bb7e87eb7d528c7027cbba40b06"}, + {file = "cython-3.2.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e71efb20048358a6b8ec604a0532961c50c067b5e63e345e2e359fff72feaee8"}, + {file = "cython-3.2.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:28b1e363b024c4b8dcf52ff68125e635cb9cb4b0ba997d628f25e32543a71103"}, + {file = "cython-3.2.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:31a90b4a2c47bb6d56baeb926948348ec968e932c1ae2c53239164e3e8880ccf"}, + {file = "cython-3.2.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e65e4773021f8dc8532010b4fbebe782c77f9a0817e93886e518c93bd6a44e9d"}, + {file = "cython-3.2.4-cp39-abi3-win32.whl", hash = "sha256:2b1f12c0e4798293d2754e73cd6f35fa5bbdf072bdc14bc6fc442c059ef2d290"}, + {file = "cython-3.2.4-cp39-abi3-win_arm64.whl", hash = "sha256:3b8e62049afef9da931d55de82d8f46c9a147313b69d5ff6af6e9121d545ce7a"}, + {file = "cython-3.2.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f8d685a70bce39acc1d62ec3916d9b724b5ef665b0ce25ae55e1c85ee09747fc"}, + {file = "cython-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca578c9cb872c7ecffbe14815dc4590a003bc13339e90b2633540c7e1a252839"}, + {file = "cython-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b84d4e3c875915545f77c88dba65ad3741afd2431e5cdee6c9a20cefe6905647"}, + {file = "cython-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:fdfdd753ad7e18e5092b413e9f542e8d28b8a08203126090e1c15f7783b7fe57"}, + {file = "cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c"}, + {file = "cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6"}, ] [[package]] @@ -461,7 +474,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["dev", "docs"] -markers = "python_version < \"3.10\"" +markers = "python_version == \"3.9\"" files = [ {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, @@ -674,42 +687,44 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" pluggy = ">=1.5,<2" +pygments = ">=2.7.2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.26.0" +version = "1.2.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"}, - {file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"}, + {file = "pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99"}, + {file = "pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57"}, ] [package.dependencies] +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} pytest = ">=8.2,<9" -typing-extensions = {version = ">=4.12", markers = "python_version < \"3.10\""} +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] @@ -717,24 +732,28 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-codspeed" -version = "3.2.0" +version = "4.4.0" description = "Pytest plugin to create CodSpeed benchmarks" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5165774424c7ab8db7e7acdb539763a0e5657996effefdf0664d7fd95158d34"}, - {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bd55f92d772592c04a55209950c50880413ae46876e66bd349ef157075ca26c"}, - {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf6f56067538f4892baa8d7ab5ef4e45bb59033be1ef18759a2c7fc55b32035"}, - {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a687b05c3d145642061b45ea78e47e12f13ce510104d1a2cda00eee0e36f58"}, - {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46a1afaaa1ac4c2ca5b0700d31ac46d80a27612961d031067d73c6ccbd8d3c2b"}, - {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48ce3af3dfa78413ed3d69d1924043aa1519048dbff46edccf8f35a25dab3c2"}, - {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66692506d33453df48b36a84703448cb8b22953eea51f03fbb2eb758dc2bdc4f"}, - {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:479774f80d0bdfafa16112700df4dbd31bf2a6757fac74795fd79c0a7b3c389b"}, - {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:109f9f4dd1088019c3b3f887d003b7d65f98a7736ca1d457884f5aa293e8e81c"}, - {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2f69a03b52c9bb041aec1b8ee54b7b6c37a6d0a948786effa4c71157765b6da"}, - {file = "pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39"}, - {file = "pytest_codspeed-3.2.0.tar.gz", hash = "sha256:f9d1b1a3b2c69cdc0490a1e8b1ced44bffbd0e8e21d81a7160cfdd923f6e8155"}, + {file = "pytest_codspeed-4.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3ae6f4053042c3a9ae3b05416fb42253c5e514e89391eb25e9c9e3ac8de8677"}, + {file = "pytest_codspeed-4.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83479a6719598d2910969a60cc410c7283c262c876422a9157dca2f2ab42fa1d"}, + {file = "pytest_codspeed-4.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29b1bf8a36e18d11641a5e610e23a94036b04185e3099978d81a873a5bd3635c"}, + {file = "pytest_codspeed-4.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06943110e7a8a4b54f4b13aaa3ff8db39caa02b2f61705916887649e36b9713a"}, + {file = "pytest_codspeed-4.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a5c1d51e7ca72ffe247c99b9a97a54191185e8f7a27528e2200d7416da2a68b"}, + {file = "pytest_codspeed-4.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:215170441e57bfcbefd179dfd86ccd54ed0ee235e0602a068ce4448b35f13cb2"}, + {file = "pytest_codspeed-4.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee3e1964446011ca192eebf0350227df231a5b88af57e518f2a4328fc8ca5131"}, + {file = "pytest_codspeed-4.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340dbb1cc5a21434e0e29bd68ab03c7dc7ad9bfde09d1980b7161352c4c2f048"}, + {file = "pytest_codspeed-4.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:413666266762f9cef1321ba971a9e127b97a1f1dad40ddfd2184c2bc5ac157f9"}, + {file = "pytest_codspeed-4.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e258e6c3d5a8a02ae02a64831be3acd44c19210ffbf13321bdbb8c111c5c6fe4"}, + {file = "pytest_codspeed-4.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d5dd94dcb69460f916acb9c69865d0171b98acec3ce256645d0c0275b553d7"}, + {file = "pytest_codspeed-4.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33c38e0e797c74506004f231fc53eab0e412987de281755f714018334381aa3a"}, + {file = "pytest_codspeed-4.4.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4afa9455a9b198c5e898224c751182fcf53f67f11fb27c2c3346284da1baa018"}, + {file = "pytest_codspeed-4.4.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98524af4ddd6006ea064791bb15a43957d78fab040cb6f499ca73a369da373e6"}, + {file = "pytest_codspeed-4.4.0-py3-none-any.whl", hash = "sha256:a6aab2fa73523f538e7729c20ccf4a1e8e921324c9877a816b05334135950fd9"}, + {file = "pytest_codspeed-4.4.0.tar.gz", hash = "sha256:edb7c101d9c50439a42cf02cfa9c0ac92da618841636bbebf87c3fa54669442a"}, ] [package.dependencies] @@ -745,38 +764,37 @@ rich = ">=13.8.1" [package.extras] compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"] -lint = ["mypy (>=1.11.2,<1.12.0)", "ruff (>=0.6.5,<0.7.0)"] -test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"] [[package]] name = "pytest-cov" -version = "6.0.0" +version = "7.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, - {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, ] [package.dependencies] -coverage = {version = ">=7.5", extras = ["toml"]} -pytest = ">=4.6" +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +testing = ["process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-timeout" -version = "2.3.1" +version = "2.4.0" description = "pytest plugin to abort hanging tests" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, - {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, + {file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}, + {file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}, ] [package.dependencies] @@ -784,19 +802,19 @@ pytest = ">=7.0.0" [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" @@ -826,24 +844,24 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "78.1.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" +version = "82.0.1" +description = "Most extensible Python build backend with support for C/C++ extension modules" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8"}, - {file = "setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54"}, + {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"}, + {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] [[package]] name = "snowballstemmer" @@ -896,19 +914,19 @@ test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools [[package]] name = "sphinx-rtd-theme" -version = "3.0.2" +version = "3.1.0" description = "Read the Docs theme for Sphinx" optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, - {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, + {file = "sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89"}, + {file = "sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c"}, ] [package.dependencies] -docutils = ">0.18,<0.22" -sphinx = ">=6,<9" +docutils = ">0.18,<0.23" +sphinx = ">=6,<10" sphinxcontrib-jquery = ">=4,<5" [package.extras] @@ -1079,7 +1097,7 @@ description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version < \"3.13\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1087,21 +1105,21 @@ files = [ [[package]] name = "urllib3" -version = "2.3.0" +version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "zipp" @@ -1110,7 +1128,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["dev", "docs"] -markers = "python_version < \"3.10\"" +markers = "python_version == \"3.9\"" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, @@ -1127,4 +1145,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "e3c96e694e9c149b96323081d51675d7a9d5ad8243f4338ff149e643a65417cb" +content-hash = "0e59529d7b9b577841ad42ddab051f6f4666e21fe3064319ff9ae9659f4e3364" diff --git a/pyproject.toml b/pyproject.toml index b2850113..a3c3114c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,28 @@ -[tool.poetry] +[build-system] +requires = ['setuptools>=77.0', 'Cython>=3.0.8', "poetry-core>=2.1.0"] +build-backend = "poetry.core.masonry.api" + +[project] name = "zeroconf" -version = "0.146.2" -description = "A pure python implementation of multicast DNS service discovery" -authors = ["Paul Scott-Murphy", "William McBrine", "Jakub Stasiak", "J. Nick Koston"] +version = "1.0.0" license = "LGPL-2.1-or-later" +description = "A pure python implementation of multicast DNS service discovery" readme = "README.rst" -repository = "https://github.com/python-zeroconf/python-zeroconf" -documentation = "https://python-zeroconf.readthedocs.io" +authors = [ + { name = "Paul Scott-Murphy" }, + { name = "William McBrine" }, + { name = "Jakub Stasiak" }, + { name = "J. Nick Koston" }, +] +requires-python = ">=3.9" + +[project.urls] +"Repository" = "https://github.com/python-zeroconf/python-zeroconf" +"Documentation" = "https://python-zeroconf.readthedocs.io" +"Bug Tracker" = "https://github.com/python-zeroconf/python-zeroconf/issues" +"Changelog" = "https://github.com/python-zeroconf/python-zeroconf/blob/master/CHANGELOG.md" + +[tool.poetry] classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -15,11 +31,6 @@ classifiers=[ 'Operating System :: POSIX :: Linux', 'Operating System :: MacOS :: MacOS X', 'Topic :: Software Development :: Libraries', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ] @@ -35,17 +46,13 @@ include = [ # Make sure we don't package temporary C files generated by the build process exclude = [ "**/*.c" ] -[tool.poetry.urls] -"Bug Tracker" = "https://github.com/python-zeroconf/python-zeroconf/issues" -"Changelog" = "https://github.com/python-zeroconf/python-zeroconf/blob/master/CHANGELOG.md" - [tool.poetry.build] generate-setup-file = true script = "build_ext.py" [tool.semantic_release] branch = "master" -version_toml = ["pyproject.toml:tool.poetry.version"] +version_toml = ["pyproject.toml:project.version"] version_variables = [ "src/zeroconf/__init__.py:__version__" ] @@ -74,16 +81,16 @@ ifaddr = ">=0.1.7" [tool.poetry.group.dev.dependencies] pytest = ">=7.2,<9.0" -pytest-cov = ">=4,<7" -pytest-asyncio = ">=0.20.3,<0.27.0" -cython = "^3.0.5" -setuptools = ">=65.6.3,<79.0.0" +pytest-cov = ">=4,<8" +pytest-asyncio = ">=0.20.3,<1.3.0" +cython = "^3.2.4" +setuptools = ">=65.6.3,<83.0.0" pytest-timeout = "^2.1.0" -pytest-codspeed = "^3.1.0" +pytest-codspeed = ">=4.4.0,<5.0" [tool.poetry.group.docs.dependencies] sphinx = "^7.4.7 || ^8.1.3" -sphinx-rtd-theme = "^3.0.2" +sphinx-rtd-theme = "^3.1.0" [tool.ruff] target-version = "py39" @@ -266,11 +273,6 @@ allow_untyped_defs = true module = "bench.*" ignore_errors = true -[build-system] -# 1.5.2 required for https://github.com/python-poetry/poetry/issues/7505 -requires = ['setuptools>=65.4.1', 'wheel', 'Cython>=3.0.8', "poetry-core>=1.5.2"] -build-backend = "poetry.core.masonry.api" - [tool.codespell] ignore-words-list = ["additionals", "HASS"] diff --git a/src/zeroconf/__init__.py b/src/zeroconf/__init__.py index 01496e22..27591398 100644 --- a/src/zeroconf/__init__.py +++ b/src/zeroconf/__init__.py @@ -88,7 +88,7 @@ __author__ = "Paul Scott-Murphy, William McBrine" __maintainer__ = "Jakub Stasiak " -__version__ = "0.146.2" +__version__ = "1.0.0" __license__ = "LGPL" diff --git a/src/zeroconf/_cache.pxd b/src/zeroconf/_cache.pxd index 273d46c3..05a40c0f 100644 --- a/src/zeroconf/_cache.pxd +++ b/src/zeroconf/_cache.pxd @@ -83,5 +83,5 @@ cdef class DNSCache: self, DNSRecord record, double now, - cython.float ttl + unsigned int ttl ) diff --git a/src/zeroconf/_cache.py b/src/zeroconf/_cache.py index c8e2686e..c7ca8472 100644 --- a/src/zeroconf/_cache.py +++ b/src/zeroconf/_cache.py @@ -317,7 +317,7 @@ def async_mark_unique_records_older_than_1s_to_expire( # Expire in 1s self._async_set_created_ttl(record, now, 1) - def _async_set_created_ttl(self, record: DNSRecord, now: _float, ttl: _float) -> None: + def _async_set_created_ttl(self, record: DNSRecord, now: _float, ttl: _int) -> None: """Set the created time and ttl of a record.""" # It would be better if we made a copy instead of mutating the record # in place, but records currently don't have a copy method. diff --git a/src/zeroconf/_core.py b/src/zeroconf/_core.py index 5e3a7f46..71e2c2f4 100644 --- a/src/zeroconf/_core.py +++ b/src/zeroconf/_core.py @@ -24,6 +24,7 @@ import asyncio import logging +import random import sys import threading from collections.abc import Awaitable @@ -544,6 +545,11 @@ async def async_check_service( instance_name = instance_name_from_service_info(info, strict=strict) if cooperating_responders: return + + # Wait a random amount of time up avoid collisions and avoid + # a thundering herd when multiple services are started on the network + await self.async_wait(random.randint(150, 250)) # noqa: S311 + next_instance_number = 2 next_time = now = current_time_millis() i = 0 diff --git a/src/zeroconf/_dns.pxd b/src/zeroconf/_dns.pxd index 5ff98a8d..7ef1dbec 100644 --- a/src/zeroconf/_dns.pxd +++ b/src/zeroconf/_dns.pxd @@ -44,10 +44,10 @@ cdef class DNSQuestion(DNSEntry): cdef class DNSRecord(DNSEntry): - cdef public cython.float ttl + cdef public unsigned int ttl cdef public double created - cdef _fast_init_record(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, double created) + cdef _fast_init_record(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, double created) cdef bint _suppressed_by_answer(self, DNSRecord answer) @@ -66,7 +66,7 @@ cdef class DNSRecord(DNSEntry): cpdef bint is_recent(self, double now) - cdef _set_created_ttl(self, double now, cython.float ttl) + cdef _set_created_ttl(self, double now, unsigned int ttl) cdef class DNSAddress(DNSRecord): @@ -74,7 +74,7 @@ cdef class DNSAddress(DNSRecord): cdef public bytes address cdef public object scope_id - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, bytes address, object scope_id, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, bytes address, object scope_id, double created) cdef bint _eq(self, DNSAddress other) @@ -87,7 +87,7 @@ cdef class DNSHinfo(DNSRecord): cdef public str cpu cdef public str os - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, str cpu, str os, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, str cpu, str os, double created) cdef bint _eq(self, DNSHinfo other) @@ -99,7 +99,7 @@ cdef class DNSPointer(DNSRecord): cdef public str alias cdef public str alias_key - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, str alias, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, str alias, double created) cdef bint _eq(self, DNSPointer other) @@ -110,7 +110,7 @@ cdef class DNSText(DNSRecord): cdef public cython.int _hash cdef public bytes text - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, bytes text, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, bytes text, double created) cdef bint _eq(self, DNSText other) @@ -125,7 +125,7 @@ cdef class DNSService(DNSRecord): cdef public str server cdef public str server_key - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, cython.uint priority, cython.uint weight, cython.uint port, str server, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, cython.uint priority, cython.uint weight, cython.uint port, str server, double created) cdef bint _eq(self, DNSService other) @@ -137,7 +137,7 @@ cdef class DNSNsec(DNSRecord): cdef public str next_name cdef public cython.list rdtypes - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, str next_name, cython.list rdtypes, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, str next_name, cython.list rdtypes, double created) cdef bint _eq(self, DNSNsec other) diff --git a/src/zeroconf/_dns.py b/src/zeroconf/_dns.py index 591eb018..93069eb3 100644 --- a/src/zeroconf/_dns.py +++ b/src/zeroconf/_dns.py @@ -63,7 +63,7 @@ class DNSQuestionType(enum.Enum): QM = 2 -class DNSEntry: +class DNSEntry: # noqa: PLW1641 """A DNS entry""" __slots__ = ("class_", "key", "name", "type", "unique") @@ -161,23 +161,22 @@ def __repr__(self) -> str: ) -class DNSRecord(DNSEntry): +class DNSRecord(DNSEntry): # noqa: PLW1641 """A DNS record - like a DNS entry, but has a TTL""" __slots__ = ("created", "ttl") - # TODO: Switch to just int ttl def __init__( self, name: str, type_: int, class_: int, - ttl: float | int, + ttl: _int, created: float | None = None, ) -> None: self._fast_init_record(name, type_, class_, ttl, created or current_time_millis()) - def _fast_init_record(self, name: str, type_: _int, class_: _int, ttl: _float, created: _float) -> None: + def _fast_init_record(self, name: str, type_: _int, class_: _int, ttl: _int, created: _float) -> None: """Fast init for reuse.""" self._fast_init_entry(name, type_, class_) self.ttl = ttl @@ -227,7 +226,7 @@ def is_recent(self, now: _float) -> bool: """Returns true if the record more than one quarter of its TTL remaining.""" return self.created + (_RECENT_TIME_MS * self.ttl) > now - def _set_created_ttl(self, created: _float, ttl: float | int) -> None: + def _set_created_ttl(self, created: _float, ttl: _int) -> None: """Set the created and ttl of a record.""" # It would be better if we made a copy instead of mutating the record # in place, but records currently don't have a copy method. @@ -266,7 +265,7 @@ def _fast_init( name: str, type_: _int, class_: _int, - ttl: _float, + ttl: _int, address: bytes, scope_id: _int | None, created: _float, @@ -327,7 +326,7 @@ def __init__( self._fast_init(name, type_, class_, ttl, cpu, os, created or current_time_millis()) def _fast_init( - self, name: str, type_: _int, class_: _int, ttl: _float, cpu: str, os: str, created: _float + self, name: str, type_: _int, class_: _int, ttl: _int, cpu: str, os: str, created: _float ) -> None: """Fast init for reuse.""" self._fast_init_record(name, type_, class_, ttl, created) @@ -374,7 +373,7 @@ def __init__( self._fast_init(name, type_, class_, ttl, alias, created or current_time_millis()) def _fast_init( - self, name: str, type_: _int, class_: _int, ttl: _float, alias: str, created: _float + self, name: str, type_: _int, class_: _int, ttl: _int, alias: str, created: _float ) -> None: self._fast_init_record(name, type_, class_, ttl, created) self.alias = alias @@ -429,7 +428,7 @@ def __init__( self._fast_init(name, type_, class_, ttl, text, created or current_time_millis()) def _fast_init( - self, name: str, type_: _int, class_: _int, ttl: _float, text: bytes, created: _float + self, name: str, type_: _int, class_: _int, ttl: _int, text: bytes, created: _float ) -> None: self._fast_init_record(name, type_, class_, ttl, created) self.text = text @@ -468,7 +467,7 @@ def __init__( name: str, type_: int, class_: int, - ttl: float | int, + ttl: int, priority: int, weight: int, port: int, @@ -484,7 +483,7 @@ def _fast_init( name: str, type_: _int, class_: _int, - ttl: _float, + ttl: _int, priority: _int, weight: _int, port: _int, @@ -539,7 +538,7 @@ def __init__( name: str, type_: int, class_: int, - ttl: int | float, + ttl: _int, next_name: str, rdtypes: list[int], created: float | None = None, @@ -551,7 +550,7 @@ def _fast_init( name: str, type_: _int, class_: _int, - ttl: _float, + ttl: _int, next_name: str, rdtypes: list[_int], created: _float, diff --git a/src/zeroconf/_handlers/record_manager.pxd b/src/zeroconf/_handlers/record_manager.pxd index 37232b13..b9bde975 100644 --- a/src/zeroconf/_handlers/record_manager.pxd +++ b/src/zeroconf/_handlers/record_manager.pxd @@ -8,7 +8,7 @@ from .._updates cimport RecordUpdateListener from .._utils.time cimport current_time_millis from .._record_update cimport RecordUpdate -cdef cython.float _DNS_PTR_MIN_TTL +cdef unsigned int _DNS_PTR_MIN_TTL cdef cython.uint _TYPE_PTR cdef object _ADDRESS_RECORD_TYPES cdef bint TYPE_CHECKING diff --git a/src/zeroconf/_listener.pxd b/src/zeroconf/_listener.pxd index 20084b47..4cbc5d00 100644 --- a/src/zeroconf/_listener.pxd +++ b/src/zeroconf/_listener.pxd @@ -50,7 +50,7 @@ cdef class AsyncListener: cpdef _respond_query( self, - object msg, + DNSIncoming msg, object addr, object port, object transport, diff --git a/src/zeroconf/_services/browser.py b/src/zeroconf/_services/browser.py index ab8c050d..897b5dd6 100644 --- a/src/zeroconf/_services/browser.py +++ b/src/zeroconf/_services/browser.py @@ -99,7 +99,7 @@ heappush = heapq.heappush -class _ScheduledPTRQuery: +class _ScheduledPTRQuery: # noqa: PLW1641 __slots__ = ( "alias", "cancelled", @@ -394,9 +394,8 @@ def _schedule_ptr_refresh( refresh_time_millis: float_, ) -> None: """Schedule a query for a pointer.""" - ttl = int(pointer.ttl) if isinstance(pointer.ttl, float) else pointer.ttl scheduled_ptr_query = _ScheduledPTRQuery( - pointer.alias, pointer.name, ttl, expire_time_millis, refresh_time_millis + pointer.alias, pointer.name, pointer.ttl, expire_time_millis, refresh_time_millis ) self._schedule_ptr_query(scheduled_ptr_query) diff --git a/src/zeroconf/_services/info.py b/src/zeroconf/_services/info.py index 9cd8df16..9b38de9d 100644 --- a/src/zeroconf/_services/info.py +++ b/src/zeroconf/_services/info.py @@ -577,7 +577,7 @@ def _process_record_threadsafe(self, zc: Zeroconf, record: DNSRecord, now: float def dns_addresses( self, - override_ttl: int | None = None, + override_ttl: int_ | None = None, version: IPVersion = IPVersion.All, ) -> list[DNSAddress]: """Return matching DNSAddress from ServiceInfo.""" @@ -585,7 +585,7 @@ def dns_addresses( def _dns_addresses( self, - override_ttl: int | None, + override_ttl: int_ | None, version: IPVersion, ) -> list[DNSAddress]: """Return matching DNSAddress from ServiceInfo.""" @@ -611,11 +611,11 @@ def _dns_addresses( self._dns_address_cache = records return records - def dns_pointer(self, override_ttl: int | None = None) -> DNSPointer: + def dns_pointer(self, override_ttl: int_ | None = None) -> DNSPointer: """Return DNSPointer from ServiceInfo.""" return self._dns_pointer(override_ttl) - def _dns_pointer(self, override_ttl: int | None) -> DNSPointer: + def _dns_pointer(self, override_ttl: int_ | None) -> DNSPointer: """Return DNSPointer from ServiceInfo.""" cacheable = override_ttl is None if self._dns_pointer_cache is not None and cacheable: @@ -632,11 +632,11 @@ def _dns_pointer(self, override_ttl: int | None) -> DNSPointer: self._dns_pointer_cache = record return record - def dns_service(self, override_ttl: int | None = None) -> DNSService: + def dns_service(self, override_ttl: int_ | None = None) -> DNSService: """Return DNSService from ServiceInfo.""" return self._dns_service(override_ttl) - def _dns_service(self, override_ttl: int | None) -> DNSService: + def _dns_service(self, override_ttl: int_ | None) -> DNSService: """Return DNSService from ServiceInfo.""" cacheable = override_ttl is None if self._dns_service_cache is not None and cacheable: @@ -659,11 +659,11 @@ def _dns_service(self, override_ttl: int | None) -> DNSService: self._dns_service_cache = record return record - def dns_text(self, override_ttl: int | None = None) -> DNSText: + def dns_text(self, override_ttl: int_ | None = None) -> DNSText: """Return DNSText from ServiceInfo.""" return self._dns_text(override_ttl) - def _dns_text(self, override_ttl: int | None) -> DNSText: + def _dns_text(self, override_ttl: int_ | None) -> DNSText: """Return DNSText from ServiceInfo.""" cacheable = override_ttl is None if self._dns_text_cache is not None and cacheable: @@ -680,11 +680,11 @@ def _dns_text(self, override_ttl: int | None) -> DNSText: self._dns_text_cache = record return record - def dns_nsec(self, missing_types: list[int], override_ttl: int | None = None) -> DNSNsec: + def dns_nsec(self, missing_types: list[int], override_ttl: int_ | None = None) -> DNSNsec: """Return DNSNsec from ServiceInfo.""" return self._dns_nsec(missing_types, override_ttl) - def _dns_nsec(self, missing_types: list[int], override_ttl: int | None) -> DNSNsec: + def _dns_nsec(self, missing_types: list[int], override_ttl: int_ | None) -> DNSNsec: """Return DNSNsec from ServiceInfo.""" return DNSNsec( self._name, @@ -696,11 +696,11 @@ def _dns_nsec(self, missing_types: list[int], override_ttl: int | None) -> DNSNs 0.0, ) - def get_address_and_nsec_records(self, override_ttl: int | None = None) -> set[DNSRecord]: + def get_address_and_nsec_records(self, override_ttl: int_ | None = None) -> set[DNSRecord]: """Build a set of address records and NSEC records for non-present record types.""" return self._get_address_and_nsec_records(override_ttl) - def _get_address_and_nsec_records(self, override_ttl: int | None) -> set[DNSRecord]: + def _get_address_and_nsec_records(self, override_ttl: int_ | None) -> set[DNSRecord]: """Build a set of address records and NSEC records for non-present record types.""" cacheable = override_ttl is None if self._get_address_and_nsec_records_cache is not None and cacheable: @@ -859,7 +859,7 @@ async def async_request( if last <= now: return False if next_ <= now: - this_question_type = question_type or QU_QUESTION if first_request else QM_QUESTION + this_question_type = question_type or (QU_QUESTION if first_request else QM_QUESTION) out = self._generate_request_query(zc, now, this_question_type) first_request = False if out.questions: diff --git a/src/zeroconf/_utils/net.py b/src/zeroconf/_utils/net.py index b4f3ef77..e67edf78 100644 --- a/src/zeroconf/_utils/net.py +++ b/src/zeroconf/_utils/net.py @@ -28,7 +28,8 @@ import socket import struct import sys -from collections.abc import Sequence +import warnings +from collections.abc import Iterable, Sequence from typing import Any, Union, cast import ifaddr @@ -73,19 +74,39 @@ def _encode_address(address: str) -> bytes: return socket.inet_pton(address_family, address) -def get_all_addresses() -> list[str]: - return list({addr.ip for iface in ifaddr.get_adapters() for addr in iface.ips if addr.is_IPv4}) # type: ignore[misc] +def get_all_addresses_ipv4(adapters: Iterable[ifaddr.Adapter]) -> list[str]: + return list({addr.ip for iface in adapters for addr in iface.ips if addr.is_IPv4}) # type: ignore[misc] -def get_all_addresses_v6() -> list[tuple[tuple[str, int, int], int]]: +def get_all_addresses_ipv6(adapters: Iterable[ifaddr.Adapter]) -> list[tuple[tuple[str, int, int], int]]: # IPv6 multicast uses positive indexes for interfaces # TODO: What about multi-address interfaces? return list( - {(addr.ip, iface.index) for iface in ifaddr.get_adapters() for addr in iface.ips if addr.is_IPv6} # type: ignore[misc] + {(addr.ip, iface.index) for iface in adapters for addr in iface.ips if addr.is_IPv6} # type: ignore[misc] + ) + + +def get_all_addresses() -> list[str]: + warnings.warn( + "get_all_addresses is deprecated, and will be removed in a future version. Use ifaddr" + "directly instead to get a list of adapters.", + DeprecationWarning, + stacklevel=2, ) + return get_all_addresses_ipv4(ifaddr.get_adapters()) -def ip6_to_address_and_index(adapters: list[ifaddr.Adapter], ip: str) -> tuple[tuple[str, int, int], int]: +def get_all_addresses_v6() -> list[tuple[tuple[str, int, int], int]]: + warnings.warn( + "get_all_addresses_v6 is deprecated, and will be removed in a future version. Use ifaddr" + "directly instead to get a list of adapters.", + DeprecationWarning, + stacklevel=2, + ) + return get_all_addresses_ipv6(ifaddr.get_adapters()) + + +def ip6_to_address_and_index(adapters: Iterable[ifaddr.Adapter], ip: str) -> tuple[tuple[str, int, int], int]: if "%" in ip: ip = ip[: ip.index("%")] # Strip scope_id. ipaddr = ipaddress.ip_address(ip) @@ -102,7 +123,7 @@ def ip6_to_address_and_index(adapters: list[ifaddr.Adapter], ip: str) -> tuple[t raise RuntimeError(f"No adapter found for IP address {ip}") -def interface_index_to_ip6_address(adapters: list[ifaddr.Adapter], index: int) -> tuple[str, int, int]: +def interface_index_to_ip6_address(adapters: Iterable[ifaddr.Adapter], index: int) -> tuple[str, int, int]: for adapter in adapters: if adapter.index == index: for adapter_ip in adapter.ips: @@ -147,15 +168,25 @@ def normalize_interface_choice( result: list[str | tuple[tuple[str, int, int], int]] = [] if choice is InterfaceChoice.Default: if ip_version != IPVersion.V4Only: - # IPv6 multicast uses interface 0 to mean the default - result.append((("", 0, 0), 0)) + # IPv6 multicast uses interface 0 to mean the default. However, + # the default interface can't be used for outgoing IPv6 multicast + # requests. In a way, interface choice default isn't really working + # with IPv6. Inform the user accordingly. + message = ( + "IPv6 multicast requests can't be sent using default interface. " + "Use V4Only, InterfaceChoice.All or an explicit list of interfaces." + ) + log.error(message) + warnings.warn(message, DeprecationWarning, stacklevel=2) + result.append((("::", 0, 0), 0)) if ip_version != IPVersion.V6Only: result.append("0.0.0.0") elif choice is InterfaceChoice.All: + adapters = ifaddr.get_adapters() if ip_version != IPVersion.V4Only: - result.extend(get_all_addresses_v6()) + result.extend(get_all_addresses_ipv6(adapters)) if ip_version != IPVersion.V6Only: - result.extend(get_all_addresses()) + result.extend(get_all_addresses_ipv4(adapters)) if not result: raise RuntimeError( f"No interfaces to listen on, check that any interfaces have IP version {ip_version}" @@ -198,28 +229,33 @@ def set_so_reuseport_if_available(s: socket.socket) -> None: raise -def set_mdns_port_socket_options_for_ip_version( +def set_respond_socket_multicast_options( s: socket.socket, - bind_addr: tuple[str] | tuple[str, int, int], ip_version: IPVersion, ) -> None: - """Set ttl/hops and loop for mdns port.""" - if ip_version != IPVersion.V6Only: - ttl = struct.pack(b"B", 255) - loop = struct.pack(b"B", 1) + """Set ttl/hops and loop for mDNS respond socket.""" + if ip_version == IPVersion.V4Only: # OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and # IP_MULTICAST_LOOP socket options as an unsigned char. - try: - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop) - except OSError as e: - if bind_addr[0] != "" or get_errno(e) != errno.EINVAL: # Fails to set on MacOS - raise - - if ip_version != IPVersion.V4Only: + ttl = struct.pack(b"B", 255) + loop = struct.pack(b"B", 1) + s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) + s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop) + elif ip_version == IPVersion.V6Only: # However, char doesn't work here (at least on Linux) s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255) s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, True) + else: + # A shared sender socket is not really possible, especially with link-local + # multicast addresses (ff02::/16), the kernel needs to know which interface + # to use for routing. + # + # It seems that macOS even refuses to take IPv4 socket options if this is an + # AF_INET6 socket. + # + # In theory we could reconfigure the socket on each send, but that is not + # really practical for Python Zerconf. + raise RuntimeError("Dual-stack responder socket not supported") def new_socket( @@ -244,14 +280,12 @@ def new_socket( s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) set_so_reuseport_if_available(s) - if port == _MDNS_PORT: - set_mdns_port_socket_options_for_ip_version(s, bind_addr, ip_version) - if apple_p2p: # SO_RECV_ANYIF = 0x1104 # https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/socket.h s.setsockopt(socket.SOL_SOCKET, 0x1104, 1) + # Bind expects (address, port) for AF_INET and (address, port, flowinfo, scope_id) for AF_INET6 bind_tup = (bind_addr[0], port, *bind_addr[1:]) try: s.bind(bind_tup) @@ -370,15 +404,27 @@ def add_multicast_member( def new_respond_socket( interface: str | tuple[tuple[str, int, int], int], apple_p2p: bool = False, + unicast: bool = False, ) -> socket.socket | None: + """Create interface specific socket for responding to multicast queries.""" is_v6 = isinstance(interface, tuple) + + # For response sockets: + # - Bind explicitly to the interface address + # - Use ephemeral ports if in unicast mode + # - Create socket according to the interface IP type (IPv4 or IPv6) respond_socket = new_socket( + bind_addr=cast(tuple[tuple[str, int, int], int], interface)[0] if is_v6 else (cast(str, interface),), + port=0 if unicast else _MDNS_PORT, ip_version=(IPVersion.V6Only if is_v6 else IPVersion.V4Only), apple_p2p=apple_p2p, - bind_addr=cast(tuple[tuple[str, int, int], int], interface)[0] if is_v6 else (cast(str, interface),), ) + if unicast: + return respond_socket + if not respond_socket: return None + log.debug("Configuring socket %s with multicast interface %s", respond_socket, interface) if is_v6: iface_bin = struct.pack("@I", cast(int, interface[1])) @@ -389,6 +435,7 @@ def new_respond_socket( socket.IP_MULTICAST_IF, socket.inet_aton(cast(str, interface)), ) + set_respond_socket_multicast_options(respond_socket, IPVersion.V6Only if is_v6 else IPVersion.V4Only) return respond_socket @@ -401,33 +448,27 @@ def create_sockets( if unicast: listen_socket = None else: - listen_socket = new_socket(ip_version=ip_version, apple_p2p=apple_p2p, bind_addr=("",)) + listen_socket = new_socket(bind_addr=("",), ip_version=ip_version, apple_p2p=apple_p2p) normalized_interfaces = normalize_interface_choice(interfaces, ip_version) - # If we are using InterfaceChoice.Default we can use + # If we are using InterfaceChoice.Default with only IPv4 or only IPv6, we can use # a single socket to listen and respond. - if not unicast and interfaces is InterfaceChoice.Default: - for i in normalized_interfaces: - add_multicast_member(cast(socket.socket, listen_socket), i) + if not unicast and interfaces is InterfaceChoice.Default and ip_version != IPVersion.All: + for interface in normalized_interfaces: + add_multicast_member(cast(socket.socket, listen_socket), interface) + # Sent responder socket options to the dual-use listen socket + set_respond_socket_multicast_options(cast(socket.socket, listen_socket), ip_version) return listen_socket, [cast(socket.socket, listen_socket)] respond_sockets = [] - for i in normalized_interfaces: - if not unicast: - if add_multicast_member(cast(socket.socket, listen_socket), i): - respond_socket = new_respond_socket(i, apple_p2p=apple_p2p) - else: - respond_socket = None - else: - is_v6 = isinstance(i, tuple) - respond_socket = new_socket( - port=0, - ip_version=IPVersion.V6Only if is_v6 else IPVersion.V4Only, - apple_p2p=apple_p2p, - bind_addr=cast(tuple[tuple[str, int, int], int], i)[0] if is_v6 else (cast(str, i),), - ) + for interface in normalized_interfaces: + # Only create response socket if unicast or becoming multicast member was successful + if not unicast and not add_multicast_member(cast(socket.socket, listen_socket), interface): + continue + + respond_socket = new_respond_socket(interface, apple_p2p=apple_p2p, unicast=unicast) if respond_socket is not None: respond_sockets.append(respond_socket) diff --git a/src/zeroconf/const.py b/src/zeroconf/const.py index 3b4b3abc..1db39a46 100644 --- a/src/zeroconf/const.py +++ b/src/zeroconf/const.py @@ -28,7 +28,7 @@ # Some timing constants _UNREGISTER_TIME = 125 # ms -_CHECK_TIME = 175 # ms +_CHECK_TIME = 500 # ms _REGISTER_TIME = 225 # ms _LISTENER_TIME = 200 # ms _BROWSER_TIME = 10000 # ms @@ -57,7 +57,7 @@ # ServiceBrowsers generating excessive queries refresh queries. # Apple uses a 15s minimum TTL, however we do not have the same # level of rate limit and safe guards so we use 1/4 of the recommended value -_DNS_PTR_MIN_TTL = _DNS_OTHER_TTL / 4 +_DNS_PTR_MIN_TTL = 1125 _DNS_PACKET_HEADER_LEN = 12 diff --git a/tests/services/test_browser.py b/tests/services/test_browser.py index d57568f4..e9135bb6 100644 --- a/tests/services/test_browser.py +++ b/tests/services/test_browser.py @@ -866,7 +866,6 @@ class LegacyRecordUpdateListener(r.RecordUpdateListener): """A RecordUpdateListener that does not implement update_records.""" def update_record(self, zc: Zeroconf, now: float, record: r.DNSRecord) -> None: - nonlocal updates updates.append(record) listener = LegacyRecordUpdateListener() @@ -923,7 +922,6 @@ def test_service_browser_is_aware_of_port_changes(): # dummy service callback def on_service_state_change(zeroconf, service_type, state_change, name): """Dummy callback.""" - nonlocal callbacks if name == registration_name: callbacks.append((service_type, state_change, name)) @@ -985,17 +983,14 @@ def test_service_browser_listeners_update_service(): class MyServiceListener(r.ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("update", type_, name)) @@ -1050,12 +1045,10 @@ def test_service_browser_listeners_no_update_service(): class MyServiceListener(r.ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) @@ -1374,17 +1367,14 @@ def test_service_browser_matching(): class MyServiceListener(r.ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("update", type_, name)) @@ -1465,17 +1455,14 @@ def test_service_browser_expire_callbacks(): class MyServiceListener(r.ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("update", type_, name)) diff --git a/tests/services/test_info.py b/tests/services/test_info.py index 3d4c5302..660b56d2 100644 --- a/tests/services/test_info.py +++ b/tests/services/test_info.py @@ -17,6 +17,7 @@ import zeroconf as r from zeroconf import DNSAddress, RecordUpdate, const +from zeroconf._protocol.outgoing import DNSOutgoing from zeroconf._services import info from zeroconf._services.info import ServiceInfo from zeroconf._utils.net import IPVersion @@ -1871,3 +1872,23 @@ async def test_address_resolver_ipv6(): aiozc.zeroconf.async_send(outgoing) assert await resolve_task assert resolver.ip_addresses_by_version(IPVersion.All) == [ip_address("fe80::52e:c2f2:bc5f:e9c6")] + + +@pytest.mark.asyncio +async def test_unicast_flag_if_requested() -> None: + """Verify we try four times even with the random delay.""" + type_ = "_typethatisnothere._tcp.local." + aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) + + def async_send(out: DNSOutgoing, addr: str | None = None, port: int = const._MDNS_PORT) -> None: + """Sends an outgoing packet.""" + for question in out.questions: + assert question.unicast + + # patch the zeroconf send + with patch.object(aiozc.zeroconf, "async_send", async_send): + await aiozc.async_get_service_info( + f"willnotbefound.{type_}", type_, question_type=r.DNSQuestionType.QU + ) + + await aiozc.async_close() diff --git a/tests/services/test_types.py b/tests/services/test_types.py index 63292246..10056c1e 100644 --- a/tests/services/test_types.py +++ b/tests/services/test_types.py @@ -91,6 +91,10 @@ def test_integration_with_listener_v6_records(disable_duplicate_packet_suppressi @unittest.skipIf(not has_working_ipv6() or sys.platform == "win32", "Requires IPv6") @unittest.skipIf(os.environ.get("SKIP_IPV6"), "IPv6 tests disabled") +@unittest.skipIf( + sys.platform == "darwin" and os.environ.get("GITHUB_ACTIONS") == "true", + "IPv6 multicast not working on macOS GitHub Actions", +) def test_integration_with_listener_ipv6(disable_duplicate_packet_suppression): type_ = "_test-listenv6ip-type._tcp.local." name = "xxxyyy" diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 40ecf816..fe24b148 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -940,17 +940,14 @@ async def test_service_browser_instantiation_generates_add_events_from_cache(): class MyServiceListener(ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("update", type_, name)) @@ -1084,7 +1081,8 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): # The rest of the startup questions should have # known answers for answer_list in answers[1:-2]: - assert len(answer_list) == 1 + # Allow 0 or 1 answers due to random delays and timing + assert len(answer_list) <= 1 # Once the TTL is reached, the last question should have no known answers assert len(answers[-1]) == 0 @@ -1191,17 +1189,14 @@ async def test_service_browser_ignores_unrelated_updates(): class MyServiceListener(ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("update", type_, name)) @@ -1349,15 +1344,12 @@ async def test_update_with_uppercase_names(run_isolated): class MyServiceListener(ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks callbacks.append(("update", type_, name)) listener = MyServiceListener() diff --git a/tests/test_core.py b/tests/test_core.py index fcfdf424..8c53d207 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -11,6 +11,7 @@ import time import unittest import unittest.mock +import warnings from typing import cast from unittest.mock import AsyncMock, Mock, patch @@ -87,16 +88,26 @@ def test_close_multiple_times(self): def test_launch_and_close_v4_v6(self): rv = r.Zeroconf(interfaces=r.InterfaceChoice.All, ip_version=r.IPVersion.All) rv.close() - rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.All) - rv.close() + with warnings.catch_warnings(record=True) as warned: + rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.All) + rv.close() + first_warning = warned[0] + assert "IPv6 multicast requests can't be sent using default interface" in str( + first_warning.message + ) @unittest.skipIf(not has_working_ipv6(), "Requires IPv6") @unittest.skipIf(os.environ.get("SKIP_IPV6"), "IPv6 tests disabled") def test_launch_and_close_v6_only(self): rv = r.Zeroconf(interfaces=r.InterfaceChoice.All, ip_version=r.IPVersion.V6Only) rv.close() - rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.V6Only) - rv.close() + with warnings.catch_warnings(record=True) as warned: + rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.V6Only) + rv.close() + first_warning = warned[0] + assert "IPv6 multicast requests can't be sent using default interface" in str( + first_warning.message + ) @unittest.skipIf(sys.platform == "darwin", reason="apple_p2p failure path not testable on mac") def test_launch_and_close_apple_p2p_not_mac(self): diff --git a/tests/test_handlers.py b/tests/test_handlers.py index ffa4ff88..31354980 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -1863,6 +1863,7 @@ async def test_response_aggregation_random_delay(): addresses=[socket.inet_aton("10.0.1.2")], ) mocked_zc = unittest.mock.MagicMock() + mocked_zc.loop = asyncio.get_running_loop() outgoing_queue = MulticastOutgoingQueue(mocked_zc, 0, 500) now = current_time_millis() @@ -1930,6 +1931,7 @@ async def test_future_answers_are_removed_on_send(): addresses=[socket.inet_aton("10.0.1.3")], ) mocked_zc = unittest.mock.MagicMock() + mocked_zc.loop = asyncio.get_running_loop() outgoing_queue = MulticastOutgoingQueue(mocked_zc, 0, 0) now = current_time_millis() diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 08d7e600..edd87c2e 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -196,7 +196,7 @@ def test_suppress_answer(self): "testname2.local.", const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, - const._DNS_HOST_TTL / 2, + int(const._DNS_HOST_TTL / 2), 0, 0, 80, diff --git a/tests/test_updates.py b/tests/test_updates.py index ec1296f7..d8b16083 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -48,7 +48,6 @@ class LegacyRecordUpdateListener(r.RecordUpdateListener): """A RecordUpdateListener that does not implement update_records.""" def update_record(self, zc: Zeroconf, now: float, record: r.DNSRecord) -> None: - nonlocal updates updates.append(record) listener = LegacyRecordUpdateListener() diff --git a/tests/utils/test_net.py b/tests/utils/test_net.py index ad8648de..e55a8cb4 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -6,23 +6,25 @@ import socket import sys import unittest -from unittest.mock import MagicMock, Mock, patch +import warnings +from unittest.mock import MagicMock, Mock, call, patch import ifaddr import pytest import zeroconf as r +from zeroconf import get_all_addresses, get_all_addresses_v6 from zeroconf._utils import net as netutils def _generate_mock_adapters(): mock_lo0 = Mock(spec=ifaddr.Adapter) mock_lo0.nice_name = "lo0" - mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0")] + mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0"), ifaddr.IP(("::1", 0, 0), 128, "lo")] mock_lo0.index = 0 mock_eth0 = Mock(spec=ifaddr.Adapter) mock_eth0.nice_name = "eth0" - mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0")] + mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0"), ifaddr.IP(("fd00:db8::", 1, 1), 8, "eth0")] mock_eth0.index = 1 mock_eth1 = Mock(spec=ifaddr.Adapter) mock_eth1.nice_name = "eth1" @@ -35,6 +37,40 @@ def _generate_mock_adapters(): return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] +def test_get_all_addresses() -> None: + """Test public get_all_addresses API.""" + with ( + patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), + warnings.catch_warnings(record=True) as warned, + ): + addresses = get_all_addresses() + assert isinstance(addresses, list) + assert len(addresses) == 3 + assert len(warned) == 1 + first_warning = warned[0] + assert "get_all_addresses is deprecated" in str(first_warning.message) + + +def test_get_all_addresses_v6() -> None: + """Test public get_all_addresses_v6 API.""" + with ( + patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), + warnings.catch_warnings(record=True) as warned, + ): + addresses = get_all_addresses_v6() + assert isinstance(addresses, list) + assert len(addresses) == 3 + assert len(warned) == 1 + first_warning = warned[0] + assert "get_all_addresses_v6 is deprecated" in str(first_warning.message) + + def test_ip6_to_address_and_index(): """Test we can extract from mocked adapters.""" adapters = _generate_mock_adapters() @@ -84,8 +120,8 @@ def test_ip6_addresses_to_indexes(): def test_normalize_interface_choice_errors(): """Test we generate exception on invalid input.""" with ( - patch("zeroconf._utils.net.get_all_addresses", return_value=[]), - patch("zeroconf._utils.net.get_all_addresses_v6", return_value=[]), + patch("zeroconf._utils.net.get_all_addresses_ipv4", return_value=[]), + patch("zeroconf._utils.net.get_all_addresses_ipv6", return_value=[]), pytest.raises(RuntimeError), ): netutils.normalize_interface_choice(r.InterfaceChoice.All) @@ -127,11 +163,10 @@ def test_disable_ipv6_only_or_raise(): errors_logged = [] def _log_error(*args): - nonlocal errors_logged errors_logged.append(args) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) with ( + socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock, pytest.raises(OSError), patch.object(netutils.log, "error", _log_error), patch("socket.socket.setsockopt", side_effect=OSError), @@ -147,100 +182,109 @@ def _log_error(*args): @pytest.mark.skipif(not hasattr(socket, "SO_REUSEPORT"), reason="System does not have SO_REUSEPORT") def test_set_so_reuseport_if_available_is_present(): """Test that setting socket.SO_REUSEPORT only OSError errno.ENOPROTOOPT is trapped.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError): - netutils.set_so_reuseport_if_available(sock) + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError): + netutils.set_so_reuseport_if_available(sock) - with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOPROTOOPT, None)): - netutils.set_so_reuseport_if_available(sock) + with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOPROTOOPT, None)): + netutils.set_so_reuseport_if_available(sock) @pytest.mark.skipif(hasattr(socket, "SO_REUSEPORT"), reason="System has SO_REUSEPORT") def test_set_so_reuseport_if_available_not_present(): """Test that we do not try to set SO_REUSEPORT if it is not present.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - with patch("socket.socket.setsockopt", side_effect=OSError): + with ( + socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock, + patch("socket.socket.setsockopt", side_effect=OSError), + ): netutils.set_so_reuseport_if_available(sock) -def test_set_mdns_port_socket_options_for_ip_version(): +def test_set_respond_socket_multicast_options(): """Test OSError with errno with EINVAL and bind address ''. from setsockopt IP_MULTICAST_TTL does not raise.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - - # Should raise on EPERM always - with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError(errno.EPERM, None)): - netutils.set_mdns_port_socket_options_for_ip_version(sock, ("",), r.IPVersion.V4Only) - - # Should raise on EINVAL always when bind address is not '' - with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)): - netutils.set_mdns_port_socket_options_for_ip_version(sock, ("127.0.0.1",), r.IPVersion.V4Only) + # Should raise on EINVAL always + with ( + socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock, + pytest.raises(OSError), + patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)), + ): + netutils.set_respond_socket_multicast_options(sock, r.IPVersion.V4Only) - # Should not raise on EINVAL when bind address is '' - with patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)): - netutils.set_mdns_port_socket_options_for_ip_version(sock, ("",), r.IPVersion.V4Only) + with pytest.raises(RuntimeError): + netutils.set_respond_socket_multicast_options(sock, r.IPVersion.All) def test_add_multicast_member(caplog: pytest.LogCaptureFixture) -> None: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - interface = "127.0.0.1" - - # EPERM should always raise - with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError(errno.EPERM, None)): - netutils.add_multicast_member(sock, interface) - - # EADDRINUSE should return False - with patch("socket.socket.setsockopt", side_effect=OSError(errno.EADDRINUSE, None)): - assert netutils.add_multicast_member(sock, interface) is False - - # EADDRNOTAVAIL should return False - with patch("socket.socket.setsockopt", side_effect=OSError(errno.EADDRNOTAVAIL, None)): - assert netutils.add_multicast_member(sock, interface) is False - - # EINVAL should return False - with patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)): - assert netutils.add_multicast_member(sock, interface) is False - - # ENOPROTOOPT should return False - with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOPROTOOPT, None)): - assert netutils.add_multicast_member(sock, interface) is False - - # ENODEV should raise for ipv4 - with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError(errno.ENODEV, None)): - assert netutils.add_multicast_member(sock, interface) is False - - # ENODEV should return False for ipv6 - with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENODEV, None)): - assert netutils.add_multicast_member(sock, ("2001:db8::", 1, 1)) is False # type: ignore[arg-type] - - # No IPv6 support should return False for IPv6 - with patch("socket.inet_pton", side_effect=OSError()): - assert netutils.add_multicast_member(sock, ("2001:db8::", 1, 1)) is False # type: ignore[arg-type] - - # No error should return True - with patch("socket.socket.setsockopt"): - assert netutils.add_multicast_member(sock, interface) is True - - # Ran out of IGMP memberships is forgiving and logs about igmp_max_memberships on linux - caplog.clear() - with ( - patch.object(sys, "platform", "linux"), - patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOBUFS, "No buffer space available")), - ): - assert netutils.add_multicast_member(sock, interface) is False - assert "No buffer space available" in caplog.text - assert "net.ipv4.igmp_max_memberships" in caplog.text - - # Ran out of IGMP memberships is forgiving and logs - caplog.clear() - with ( - patch.object(sys, "platform", "darwin"), - patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOBUFS, "No buffer space available")), - ): - assert netutils.add_multicast_member(sock, interface) is False - assert "No buffer space available" in caplog.text - assert "net.ipv4.igmp_max_memberships" not in caplog.text + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + interface = "127.0.0.1" + + # EPERM should always raise + with ( + pytest.raises(OSError), + patch("socket.socket.setsockopt", side_effect=OSError(errno.EPERM, None)), + ): + netutils.add_multicast_member(sock, interface) + + # EADDRINUSE should return False + with patch("socket.socket.setsockopt", side_effect=OSError(errno.EADDRINUSE, None)): + assert netutils.add_multicast_member(sock, interface) is False + + # EADDRNOTAVAIL should return False + with patch("socket.socket.setsockopt", side_effect=OSError(errno.EADDRNOTAVAIL, None)): + assert netutils.add_multicast_member(sock, interface) is False + + # EINVAL should return False + with patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)): + assert netutils.add_multicast_member(sock, interface) is False + + # ENOPROTOOPT should return False + with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOPROTOOPT, None)): + assert netutils.add_multicast_member(sock, interface) is False + + # ENODEV should raise for ipv4 + with ( + pytest.raises(OSError), + patch("socket.socket.setsockopt", side_effect=OSError(errno.ENODEV, None)), + ): + assert netutils.add_multicast_member(sock, interface) is False + + # ENODEV should return False for ipv6 + with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENODEV, None)): + assert netutils.add_multicast_member(sock, ("2001:db8::", 1, 1)) is False # type: ignore[arg-type] + + # No IPv6 support should return False for IPv6 + with patch("socket.inet_pton", side_effect=OSError()): + assert netutils.add_multicast_member(sock, ("2001:db8::", 1, 1)) is False # type: ignore[arg-type] + + # No error should return True + with patch("socket.socket.setsockopt"): + assert netutils.add_multicast_member(sock, interface) is True + + # Ran out of IGMP memberships is forgiving and logs about igmp_max_memberships on linux + caplog.clear() + with ( + patch.object(sys, "platform", "linux"), + patch( + "socket.socket.setsockopt", side_effect=OSError(errno.ENOBUFS, "No buffer space available") + ), + ): + assert netutils.add_multicast_member(sock, interface) is False + assert "No buffer space available" in caplog.text + assert "net.ipv4.igmp_max_memberships" in caplog.text + + # Ran out of IGMP memberships is forgiving and logs + caplog.clear() + with ( + patch.object(sys, "platform", "darwin"), + patch( + "socket.socket.setsockopt", side_effect=OSError(errno.ENOBUFS, "No buffer space available") + ), + ): + assert netutils.add_multicast_member(sock, interface) is False + assert "No buffer space available" in caplog.text + assert "net.ipv4.igmp_max_memberships" not in caplog.text def test_bind_raises_skips_address(): @@ -300,8 +344,8 @@ def test_new_respond_socket_new_socket_returns_none(): assert netutils.new_respond_socket(("0.0.0.0", 0)) is None # type: ignore[arg-type] -def test_create_sockets(): - """Test create_sockets with unicast and IPv4.""" +def test_create_sockets_interfaces_all_unicast(): + """Test create_sockets with unicast.""" with ( patch("zeroconf._utils.net.new_socket") as mock_new_socket, @@ -313,7 +357,7 @@ def test_create_sockets(): mock_socket = Mock(spec=socket.socket) mock_new_socket.return_value = mock_socket - listen_socket, respond_sockets = r.create_sockets( + listen_socket, _respond_sockets = r.create_sockets( interfaces=r.InterfaceChoice.All, unicast=True, ip_version=r.IPVersion.All ) @@ -330,3 +374,62 @@ def test_create_sockets(): apple_p2p=False, bind_addr=("192.168.1.5",), ) + + +def test_create_sockets_interfaces_all() -> None: + """Test create_sockets with all interfaces. + + Tests if a responder socket is created for every successful multicast + join. + """ + adapters = _generate_mock_adapters() + + # Additional IPv6 addresses usually fail to add membership + failure_interface = ("fd00:db8::", 1, 1) + + expected_calls = [] + for adapter in adapters: + for ip in adapter.ips: + if ip.ip == failure_interface: + continue + + if ip.is_IPv4: + bind_addr = (ip.ip,) + ip_version = r.IPVersion.V4Only + else: + bind_addr = ip.ip + ip_version = r.IPVersion.V6Only + + expected_calls.append( + call( + port=5353, + ip_version=ip_version, + apple_p2p=False, + bind_addr=bind_addr, + ) + ) + + def _patched_add_multicast_member(sock, interface): + return interface[0] != failure_interface + + with ( + patch("zeroconf._utils.net.new_socket") as mock_new_socket, + patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=adapters, + ), + patch("zeroconf._utils.net.add_multicast_member", side_effect=_patched_add_multicast_member), + ): + mock_socket = Mock(spec=socket.socket) + mock_new_socket.return_value = mock_socket + + r.create_sockets(interfaces=r.InterfaceChoice.All, ip_version=r.IPVersion.All) + + def call_to_tuple(c): + return (c.args, tuple(sorted(c.kwargs.items()))) + + # Exclude first new_socket call as this is the listen socket + actual_calls_set = {call_to_tuple(c) for c in mock_new_socket.call_args_list[1:]} + expected_calls_set = {call_to_tuple(c) for c in expected_calls} + + assert actual_calls_set == expected_calls_set