pax_global_header00006660000000000000000000000064152047326750014525gustar00rootroot0000000000000052 comment=1cc791eddb460469f237ce3e9119da2b0ef199dc Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/000077500000000000000000000000001520473267500222715ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.all-contributorsrc000066400000000000000000000004661520473267500261300ustar00rootroot00000000000000{ "projectName": "bluetooth-auto-recovery", "projectOwner": "bluetooth-devices", "repoType": "github", "repoHost": "https://github.com", "files": ["README.md"], "imageSize": 80, "commit": true, "commitConvention": "angular", "contributors": [], "contributorsPerLine": 7, "skipCi": true } Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.editorconfig000066400000000000000000000004441520473267500247500ustar00rootroot00000000000000# http://editorconfig.org root = true [*] indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 end_of_line = lf [*.bat] indent_style = tab end_of_line = crlf [LICENSE] insert_final_newline = false [Makefile] indent_style = tab Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/000077500000000000000000000000001520473267500236315ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/ISSUE_TEMPLATE/000077500000000000000000000000001520473267500260145ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/ISSUE_TEMPLATE/1-bug_report.md000066400000000000000000000004221520473267500306420ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve labels: bug --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: **Additional context** Add any other context about the problem here. Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/ISSUE_TEMPLATE/2-feature-request.md000066400000000000000000000006721520473267500316230ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project labels: enhancement --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Additional context** Add any other context or screenshots about the feature request here. Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/dependabot.yml000066400000000000000000000013511520473267500264610ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" commit-message: prefix: "chore(deps-ci): " groups: github-actions: patterns: - "*" - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/labels.toml000066400000000000000000000035151520473267500257740ustar00rootroot00000000000000[breaking] color = "ffcc00" name = "breaking" description = "Breaking change." [bug] color = "d73a4a" name = "bug" description = "Something isn't working" [dependencies] color = "0366d6" name = "dependencies" description = "Pull requests that update a dependency file" [github_actions] color = "000000" name = "github_actions" description = "Update of github actions" [documentation] color = "1bc4a5" name = "documentation" description = "Improvements or additions to documentation" [duplicate] color = "cfd3d7" name = "duplicate" description = "This issue or pull request already exists" [enhancement] color = "a2eeef" name = "enhancement" description = "New feature or request" ["good first issue"] color = "7057ff" name = "good first issue" description = "Good for newcomers" ["help wanted"] color = "008672" name = "help wanted" description = "Extra attention is needed" [invalid] color = "e4e669" name = "invalid" description = "This doesn't seem right" [nochangelog] color = "555555" name = "nochangelog" description = "Exclude pull requests from changelog" [question] color = "d876e3" name = "question" description = "Further information is requested" [removed] color = "e99695" name = "removed" description = "Removed piece of functionalities." [tests] color = "bfd4f2" name = "tests" description = "CI, CD and testing related changes" [wontfix] color = "ffffff" name = "wontfix" description = "This will not be worked on" [discussion] color = "c2e0c6" name = "discussion" description = "Some discussion around the project" [hacktoberfest] color = "ffa663" name = "hacktoberfest" description = "Good issues for Hacktoberfest" [answered] color = "0ee2b6" name = "answered" description = "Automatically closes as answered after a delay" [waiting] color = "5f7972" name = "waiting" description = "Automatically closes if no answer after a delay" Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/workflows/000077500000000000000000000000001520473267500256665ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/workflows/ci.yml000066400000000000000000000050361520473267500270100ustar00rootroot00000000000000name: CI on: push: branches: - main pull_request: concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: "3.10" - uses: pre-commit/action@v3.0.1 # Make sure commit messages follow the conventional commits convention: # https://www.conventionalcommits.org commitlint: name: Lint Commit Messages runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v6 test: strategy: fail-fast: false matrix: python-version: - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" os: - ubuntu-latest - windows-latest - macos-latest runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - uses: snok/install-poetry@v1 - name: Install Dependencies shell: bash run: poetry install - name: Test with Pytest shell: bash run: poetry run pytest --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} release: runs-on: ubuntu-latest environment: release if: github.ref == 'refs/heads/main' needs: - test - lint - commitlint permissions: id-token: write contents: write steps: - uses: actions/checkout@v6 with: fetch-depth: 0 # Run semantic release: # - Update CHANGELOG.md # - Update version in code # - Create git tag # - Create GitHub release - name: Python Semantic Release id: release uses: python-semantic-release/python-semantic-release@v10.5.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 if: steps.release.outputs.released == 'true' - name: Upload Github Release Assets uses: python-semantic-release/publish-action@v10.5.3 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ steps.release.outputs.tag }} Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/workflows/hacktoberfest.yml000066400000000000000000000005341520473267500312370ustar00rootroot00000000000000name: Hacktoberfest on: schedule: # Run every day in October - cron: "0 0 * 10 *" # Run on the 1st of November to revert - cron: "0 13 1 11 *" jobs: hacktoberfest: runs-on: ubuntu-latest steps: - uses: browniebroke/hacktoberfest-labeler-action@v2.6.0 with: github_token: ${{ secrets.GH_PAT }} Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/workflows/issue-manager.yml000066400000000000000000000013401520473267500311470ustar00rootroot00000000000000name: Issue Manager on: schedule: - cron: "0 0 * * *" issue_comment: types: - created issues: types: - labeled pull_request_target: types: - labeled workflow_dispatch: jobs: issue-manager: runs-on: ubuntu-latest steps: - uses: tiangolo/issue-manager@0.6.0 with: token: ${{ secrets.GITHUB_TOKEN }} config: > { "answered": { "message": "Assuming the original issue was solved, it will be automatically closed now." }, "waiting": { "message": "Automatically closing. To re-open, please provide the additional information requested." } } Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/workflows/labels.yml000066400000000000000000000007741520473267500276630ustar00rootroot00000000000000name: Sync Github labels on: push: branches: - main paths: - ".github/**" jobs: labels: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: 3.8 - name: Install labels run: pip install labels - name: Sync config with Github run: labels -u ${{ github.repository_owner }} -t ${{ secrets.GITHUB_TOKEN }} sync -f .github/labels.toml Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.gitignore000066400000000000000000000040661520473267500242670ustar00rootroot00000000000000# Created by .ignore support plugin (hsz.mobi) ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.gitpod.yml000066400000000000000000000003061520473267500243570ustar00rootroot00000000000000tasks: - command: | pip install poetry PIP_USER=false poetry install - command: | pip install pre-commit pre-commit install PIP_USER=false pre-commit install-hooks Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.idea/000077500000000000000000000000001520473267500232515ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.idea/bluetooth-auto-recovery.iml000066400000000000000000000005151520473267500305640ustar00rootroot00000000000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.idea/watcherTasks.xml000066400000000000000000000052531520473267500264430ustar00rootroot00000000000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.idea/workspace.xml000066400000000000000000000027431520473267500257770ustar00rootroot00000000000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.pre-commit-config.yaml000066400000000000000000000027541520473267500265620ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks exclude: "CHANGELOG.md" default_stages: [pre-commit] ci: autofix_commit_msg: "chore(pre-commit.ci): auto fixes" autoupdate_commit_msg: "chore(pre-commit.ci): pre-commit autoupdate" repos: - repo: https://github.com/commitizen-tools/commitizen rev: v4.16.2 hooks: - id: commitizen stages: [commit-msg] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: debug-statements - id: check-builtin-literals - id: check-case-conflict - id: check-docstring-first - id: check-json - id: check-toml - id: check-xml - id: check-yaml - id: detect-private-key - id: end-of-file-fixer - id: trailing-whitespace - id: debug-statements - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 hooks: - id: prettier args: ["--tab-width", "2"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.13 hooks: - id: ruff-check args: [--fix] - id: ruff-format - repo: https://github.com/codespell-project/codespell rev: v2.4.2 hooks: - id: codespell - repo: https://github.com/pre-commit/mirrors-mypy rev: v2.1.0 hooks: - id: mypy additional_dependencies: [] - repo: https://github.com/PyCQA/bandit rev: 1.9.4 hooks: - id: bandit args: [-x, tests] Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.readthedocs.yml000066400000000000000000000010051520473267500253530ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py # Set the version of Python and other tools you might need build: os: ubuntu-20.04 tools: python: "3.10" # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . extra_requirements: - docs Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/CHANGELOG.md000066400000000000000000000760531520473267500241150ustar00rootroot00000000000000# CHANGELOG ## v1.6.0 (2026-05-24) ### Chores - **pre-commit.ci**: Pre-commit autoupdate ([#108](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/108), [`9dd8068`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/9dd8068424aad267543c7e2a27a1644335fc93f6)) ### Features - Drop Python 3.9 support ([#120](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/120), [`b6f14f7`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/b6f14f739e2509f64d2f4d14b58545faa833d71d)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> ## v1.5.3 (2025-09-13) ### Bug Fixes - Bluetooth management socket communication on certain kernels ([#107](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/107), [`4e5e994`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/4e5e994e7b9f2bda7a26037fcbc114091d1d138c)) ### Chores - **pre-commit.ci**: Pre-commit autoupdate ([#94](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/94), [`10815e0`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/10815e08ec975459bf75d85c69e0cc555f830758)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> ## v1.5.2 (2025-05-21) ### Bug Fixes - Update poetry to v2 ([#95](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/95), [`5902b0c`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/5902b0c5a9bcaae2db7c16964be88314f3952ba7)) ### Chores - **pre-commit.ci**: Pre-commit autoupdate ([#93](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/93), [`d8a9cc3`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/d8a9cc3d08b90d239411867655d6621fd378c63b)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> ## v1.5.1 (2025-05-03) ### Bug Fixes - Ensure public signature includes gone_silent ([#92](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/92), [`176502b`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/176502b2d5e64d798b9f7979df29f5007e18d561)) ## v1.5.0 (2025-05-03) ### Chores - Update dependabot.yml to include GHA ([`e942f61`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/e942f61bb833dcadeeeb40f2b75f07510854fa0f)) - Update deps to fix CI ([#91](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/91), [`8d45f38`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/8d45f380a0e9b096c106a7039396e487ff67fdce)) - **deps**: Bump jinja2 from 3.1.5 to 3.1.6 ([#84](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/84), [`ef86b2e`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/ef86b2eea4300a0363420a5d871ec0b5850692b2)) - **deps-ci**: Bump the github-actions group with 8 updates ([#90](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/90), [`1c99b89`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/1c99b895c1dbeee550f06a0a44cbe1a90b46887b)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston - **deps-dev**: Bump pytest from 8.3.4 to 8.3.5 ([#82](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/82), [`e4988e8`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/e4988e8af4dc5d21a61ea3f84768a3a48bcec2e8)) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.4 to 8.3.5. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.4...8.3.5) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump pytest-asyncio from 0.25.3 to 0.26.0 ([#85](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/85), [`a05b39a`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/a05b39af200ae8f207ed8ac739a019d9024b4fbe)) Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.25.3 to 0.26.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.25.3...v0.26.0) --- updated-dependencies: - dependency-name: pytest-asyncio dependency-version: 0.26.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump pytest-cov from 6.0.0 to 6.1.1 ([#87](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/87), [`26ddaf4`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/26ddaf4fb6464be27558d3343dedbcdfb34cbd64)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#81](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/81), [`5ba466a`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/5ba466a21fd7ed63e35095bff1d49789022810bd)) updates: - [github.com/commitizen-tools/commitizen: v4.2.1 → v4.4.1](https://github.com/commitizen-tools/commitizen/compare/v4.2.1...v4.4.1) - [github.com/PyCQA/isort: 6.0.0 → 6.0.1](https://github.com/PyCQA/isort/compare/6.0.0...6.0.1) - [github.com/PyCQA/flake8: 7.1.2 → 7.2.0](https://github.com/PyCQA/flake8/compare/7.1.2...7.2.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#86](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/86), [`c3f76f8`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/c3f76f877511e0adbbb92da03cee7d9cefdfdc34)) updates: - [github.com/commitizen-tools/commitizen: v4.4.1 → v4.5.0](https://github.com/commitizen-tools/commitizen/compare/v4.4.1...v4.5.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#88](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/88), [`c7a1de1`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/c7a1de10f3932c8f0f3acb84109bf5a45c152e03)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> ### Features - Try USB reset if the adapter has gone silent ([#89](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/89), [`c615af1`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/c615af120941a1d38fc082f4eab2597c55c3f4d2)) ## v1.4.5 (2025-03-13) ### Bug Fixes - Downgrade power on success log message to debug ([#83](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/83), [`da6ca83`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/da6ca83c1c55b4c154b46b18afacbf3ab7d9b063)) ## v1.4.4 (2025-02-19) ### Bug Fixes - Handle case where adapter moves to index 0 after USB reset ([#79](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/79), [`34517d3`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/34517d37e426cba26cceb0bcdc8da2f98e63610b)) ## v1.4.3 (2025-02-19) ### Bug Fixes - Rfkill unblocking when adapter idx is 0 ([#78](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/78), [`f6dbba0`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/f6dbba060307aca0657ce6a967153c2e373b6aaa)) ### Chores - Create dependabot.yml ([`e412f97`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/e412f97b02cdf64be5cad1347d5ec68eac1a6fb4)) - **deps**: Bump aiohttp from 3.9.5 to 3.10.11 ([#67](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/67), [`95b906e`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/95b906e26681e488043d33dd7b6630a792f5d4ee)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump async-timeout from 4.0.3 to 5.0.1 ([#68](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/68), [`7bd0f38`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/7bd0f3852b394b9ec0909fb7e615a353c48e577e)) - **deps**: Bump certifi from 2024.6.2 to 2024.7.4 ([#65](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/65), [`a9ba476`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/a9ba476e51ba0aee227a8a50cae8d49a45e698af)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump jinja2 from 3.1.4 to 3.1.5 ([#64](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/64), [`65f2b3d`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/65f2b3da77fa70927b4737dfa8a11d1aa21cec0c)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump myst-parser from 0.18.1 to 1.0.0 ([#61](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/61), [`8f2c913`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/8f2c91376ca173a41403a68a5b8fe290cc4823ba)) Bumps [myst-parser](https://github.com/executablebooks/MyST-Parser) from 0.18.1 to 1.0.0. - [Release notes](https://github.com/executablebooks/MyST-Parser/releases) - [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/executablebooks/MyST-Parser/compare/v0.18.1...v1.0.0) --- updated-dependencies: - dependency-name: myst-parser dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump myst-parser from 1.0.0 to 3.0.1 ([#72](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/72), [`6dfd5ac`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/6dfd5acffeabcda0e465d23ea63855c46cedf038)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump sphinx from 5.3.0 to 6.2.1 ([#69](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/69), [`7442426`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/7442426226bf7bb07b9d57890aed55f778c2362f)) - **deps**: Bump sphinx from 6.2.1 to 7.4.7 ([#75](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/75), [`c34f36b`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/c34f36bc83edef560930d610f6e66192a832561f)) - **deps**: Bump sphinx-rtd-theme from 1.3.0 to 2.0.0 ([#60](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/60), [`9e29600`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/9e296002f07bc99424a6f5b0fd80507cbf350e81)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps**: Bump sphinx-rtd-theme from 2.0.0 to 3.0.2 ([#71](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/71), [`7678868`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/7678868d956b92c8cd75296692b1684d8f69017e)) - **deps-dev**: Bump pytest from 7.4.4 to 8.3.4 ([#59](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/59), [`caa4b28`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/caa4b2894db891105fc398a1bf4eac4871c3c71c)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump pytest-asyncio from 0.23.7 to 0.25.2 ([#66](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/66), [`f06eb67`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/f06eb67bba4d1ed172034fc8873419ecbaec578e)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **deps-dev**: Bump pytest-asyncio from 0.25.2 to 0.25.3 ([#74](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/74), [`b216503`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/b21650347fbe316002fe1e18f84cb5755173a99e)) - **deps-dev**: Bump pytest-cov from 3.0.0 to 6.0.0 ([#63](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/63), [`69ceaa5`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/69ceaa579c6880fb9921124ec9d470f2eb724c93)) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#47](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/47), [`e25b028`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/e25b0288577fa4ab4ca3cc57d64787e27bdf3489)) * chore(pre-commit.ci): pre-commit autoupdate updates: - [github.com/commitizen-tools/commitizen: v2.31.0 → v3.27.0](https://github.com/commitizen-tools/commitizen/compare/v2.31.0...v3.27.0) - [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.6.0) - [github.com/pre-commit/mirrors-prettier: v2.7.1 → v4.0.0-alpha.8](https://github.com/pre-commit/mirrors-prettier/compare/v2.7.1...v4.0.0-alpha.8) - [github.com/asottile/pyupgrade: v2.37.3 → v3.16.0](https://github.com/asottile/pyupgrade/compare/v2.37.3...v3.16.0) - [github.com/PyCQA/isort: 5.12.0 → 5.13.2](https://github.com/PyCQA/isort/compare/5.12.0...5.13.2) - [github.com/psf/black: 22.6.0 → 24.4.2](https://github.com/psf/black/compare/22.6.0...24.4.2) - [github.com/codespell-project/codespell: v2.2.1 → v2.3.0](https://github.com/codespell-project/codespell/compare/v2.2.1...v2.3.0) - [github.com/PyCQA/flake8: 5.0.4 → 7.1.0](https://github.com/PyCQA/flake8/compare/5.0.4...7.1.0) - [github.com/pre-commit/mirrors-mypy: v0.931 → v1.10.1](https://github.com/pre-commit/mirrors-mypy/compare/v0.931...v1.10.1) - [github.com/PyCQA/bandit: 1.7.4 → 1.7.9](https://github.com/PyCQA/bandit/compare/1.7.4...1.7.9) * chore(pre-commit.ci): auto fixes * fix: lint --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston - **pre-commit.ci**: Pre-commit autoupdate ([#48](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/48), [`15d2101`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/15d2101864699b2ab8d694298b2b2626422e7c95)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#49](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/49), [`f54eccd`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/f54eccdf3638b1ce070dd822491156b9179bb908)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#50](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/50), [`d04be3e`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/d04be3e25f87df77075d6a4ae09f6bec1604a3eb)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#51](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/51), [`a0bbc33`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/a0bbc33a5e1d0bd2fdee57344615c37f68dca1bf)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#52](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/52), [`59ecbda`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/59ecbdaae20dad595f70dc45bdb428e2904eded6)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#53](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/53), [`c9319c6`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/c9319c6f9c1a5691437057a5d856bca9e64622f9)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston - **pre-commit.ci**: Pre-commit autoupdate ([#54](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/54), [`2263e37`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/2263e3706559bc3052a1377571079a08d05f75ce)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#57](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/57), [`0e9084a`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/0e9084a152b802060a1e869b1f3cf2e74952028a)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#58](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/58), [`b369f20`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/b369f20fe8bd608645806495af02d43fea51d64e)) - **pre-commit.ci**: Pre-commit autoupdate ([#70](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/70), [`eef87a0`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/eef87a05c749aca5c367629c21b76141dc472ca9)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#73](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/73), [`db65606`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/db656061f7de78635d8aa81224c3d9dd4b91de9f)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#76](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/76), [`e0d8d14`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/e0d8d143ad032d648ac71e371e4cf18b4580e3a1)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> - **pre-commit.ci**: Pre-commit autoupdate ([#77](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/77), [`d87450a`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/d87450a3799f9164f67f5663296f26b945395b99)) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> ## v1.4.2 (2024-04-25) ### Bug Fixes - Ensure timeout does not raise cancellation ([#46](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/46), [`4575fdd`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/4575fdd52778e09dee5e6bce51e7636e9609aaac)) ## v1.4.1 (2024-04-18) ### Bug Fixes - Wait for connection made ([#45](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/45), [`70aa8df`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/70aa8df23ac7a604177af680b63fdb1f00e430b8)) ## v1.4.0 (2024-03-13) ### Features - Only import recovery code the first time the recovery is called ([#44](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/44), [`39372f0`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/39372f085e624a7fba8d06ed1ddc8a4a52c7bb7c)) ## v1.3.0 (2024-01-10) ### Features - Ensure library can be loaded on windows ([#43](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/43), [`dd234f8`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/dd234f847c54fe8471b51378ab03b2d1a9f2f497)) ## v1.2.3 (2023-09-09) ### Bug Fixes - Add missing async keyword to send timeout ([#42](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/42), [`1097e44`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/1097e44a8c6051aeb6e9f4d53631c8ecf1e47d54)) ## v1.2.2 (2023-09-07) ### Bug Fixes - Ensure timeouts work with py3.11 ([#41](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/41), [`99b9f48`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/99b9f48f8f742d6004720b26e25b9c1f6cd455e7)) ## v1.2.1 (2023-07-12) ### Bug Fixes - Make MGMTBluetoothCtl aware of down adapters ([#38](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/38), [`3c6bc12`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/3c6bc12e021611590e13c400aeedc665b582a9c3)) ### Chores - Fix ci ([#37](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/37), [`68b45f4`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/68b45f493a0ec67c11b055498a8208fc08fd640a)) - Fix ci ([#39](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/39), [`9f72572`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/9f725728223cb32af868429f9c2b35ecf22c068d)) ## v1.2.0 (2023-05-10) ### Features - Try to bounce the adapter if setting power state fails ([#36](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/36), [`11ec5e2`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/11ec5e2e5b8fc8e6d58d9b822dc333dbf89e6952)) ## v1.1.2 (2023-05-04) ### Bug Fixes - Proceed with reset when getting power state times out ([#34](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/34), [`aae8c84`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/aae8c848fb894686ba1076d395b402701b0cedc5)) ## v1.1.1 (2023-05-03) ### Bug Fixes - Pass on event types we do not know how to process ([#33](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/33), [`2bbca73`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/2bbca73867f6bbd599544fe47ff6b0c468c6436a)) ## v1.1.0 (2023-05-03) ### Chores - Fix ci ([#32](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/32), [`9445c54`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/9445c54fb1663aa4c2b308d51b6ff5035f0589ee)) ### Features - Do a down/up on the interface when resetting the adapter ([#31](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/31), [`ae3f63b`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/ae3f63b0b13672df6375e4c6ee5514439484a31f)) ## v1.0.3 (2022-12-15) ### Bug Fixes - Handle the btsocket being closed out from under us ([#29](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/29), [`1e0d878`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/1e0d87853379e1ca89b50ecd9698e8c61c37e398)) ### Chores - Add python 3.11 to the ci ([#30](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/30), [`7174b10`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/7174b1008d727a5658a1d0c9e4c3fadfdeccc9cd)) ## v1.0.2 (2022-12-15) ### Bug Fixes - Handle the case where a btsocket cannot be created ([#28](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/28), [`6e8e8e1`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/6e8e8e1b0b42a0c830a70cafafd8a25e3df631d5)) ## v1.0.1 (2022-12-15) ### Bug Fixes - Handle adapter moving to a new hci number after reset ([#27](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/27), [`662f710`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/662f710c30b07a0904cc9a3d00b39303ee43db4a)) ## v1.0.0 (2022-12-12) ### Features - Add support for being able to reset the adapter by mac address when the hci interface is lost ([#26](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/26), [`72d6114`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/72d6114a4c6b553fb574f43fc793fd0c7a969521)) BREAKING CHANGE: The mac address must now be passed to `recover_adapter` - Do not check for the BTLE bit since it can be missing when failed: If the adapter was fully unresponsive the BTLE bit may be missing so we should still try to reset the adapter anyways since we already know they managed to set it up. - Try to lookup the adapter by mac address since the hci interface may have disappeared and we can't reset an adapter we can no longer find. ### Breaking Changes - The mac address must now be passed to `recover_adapter` ## v0.5.5 (2022-12-09) ### Bug Fixes - Handle BluetoothSocketError and fallback to usb reset ([#25](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/25), [`5d6d1c3`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/5d6d1c390279fbe712f6330f8997dc87f981d5e7)) ## v0.5.4 (2022-12-02) ### Bug Fixes - Downgrade permission denied error logging when attempting usb reset ([#24](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/24), [`79cf457`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/79cf457f38071ba8265864c8b18acda184065f97)) ## v0.5.3 (2022-11-29) ### Bug Fixes - For rfkill not being readable ([#23](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/23), [`6c168a0`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/6c168a0704401d6dfcd95ade31b2df47cee03060)) ## v0.5.2 (2022-11-27) ### Bug Fixes - Ensure dbus wait always happens on success case ([#22](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/22), [`df8e7e0`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/df8e7e0647dd63aa173d8e815b6a5cbfaf40ff41)) ## v0.5.1 (2022-11-27) ### Bug Fixes - Bump usb-devices ([#21](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/21), [`06c2d05`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/06c2d05f530965fee9a7ea7d0cf3596ba65bddfe)) ## v0.5.0 (2022-11-27) ### Features - Implement generic usb reset ([#20](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/20), [`0d7f045`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/0d7f045703b192b0aa86de482d128a43f48e84bf)) ## v0.4.0 (2022-11-16) ### Features - Reduce overhead to find a response ([#18](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/18), [`219d3f7`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/219d3f77d20415c22412872a24896adee8eefb8e)) ## v0.3.6 (2022-10-19) ### Bug Fixes - Soft_block and hard_block were unbound when rfkill fails ([#15](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/15), [`9d2aa1a`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/9d2aa1a5245ceef07ecb0f1cbdf668782ff5ec81)) ## v0.3.5 (2022-10-19) ### Bug Fixes - Missing param in format string for rfkill timeout message ([#13](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/13), [`0022d8a`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/0022d8a28849f51abdc055e6cc1b3c19cbe6abdf)) ## v0.3.4 (2022-10-10) ### Bug Fixes - Ensure management socket is closed on failure to prevent a leak ([#12](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/12), [`4ab673f`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/4ab673fb989ae696327150337a4dfd4d1770ca9d)) I found this via dumb luck as I managed to knock a bluetooth adapter just out of the usb socket so it keeps disconnecting and reconnecting. Net results is a leak in python-btsocket which results in the bluetooth management socket not being closed if the stack doesn't respond so it leaves it open when it tries to reset it and leaks. Worse is the leak builds up over time if it happens again and if you have a busy systems its processing all the data while waiting for a response. Make BluetoothMGMTProtocol a context manger and an asyncio.Protocol to ensure if anything goes wrong the underlying bluetooth management socket gets closed. ## v0.3.3 (2022-09-11) ### Bug Fixes - Downgrade rfkill check logging to debug ([#11](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/11), [`80471e6`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/80471e6155d9d72a2ffa467d580aec4315968aaf)) ## v0.3.2 (2022-09-08) ### Bug Fixes - Downgrade rfkill check logging to debug ([#10](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/10), [`c7b9539`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/c7b95396c78e9d05a78ce1b9022c481f30a2b9e0)) ## v0.3.1 (2022-09-06) ### Bug Fixes - Handle invalid data in rfkill ([#9](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/9), [`31c1480`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/31c148013721b22f01d9a107d01a6d6cc576c815)) ## v0.3.0 (2022-08-30) ### Features - Handle no permission to check rfkill ([#8](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/8), [`fcda90d`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/fcda90dcdd104608fb2db4b9feca90a7b0e5c8d5)) ## v0.2.2 (2022-08-20) ### Bug Fixes - Give Dbus a bit more time to catch up if the adapter has been recovered ([#7](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/7), [`216ef1f`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/216ef1fd0e9ec4ed3b022f9f194282d7b2b359cf)) ## v0.2.1 (2022-08-20) ### Bug Fixes - Handle libc.so.6 missing ([#6](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/6), [`0d9f4cb`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/0d9f4cbc9bf2a422c2f7a889354b76cfe7c75620)) - Handle rfkill not being available in the container ([#5](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/5), [`7736c35`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/7736c35279351c8877514f3324f7428447bdaaea)) ## v0.2.0 (2022-08-20) ### Features - Give DBus some time to catch up to avoid spurious warnings ([#4](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/4), [`63188f6`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/63188f694b667ce5418736f5ca02db8484bc83b9)) ## v0.1.0 (2022-08-19) ### Chores - Initial commit ([`3b0adee`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/3b0adee4377dc2c52dbb8439fbaadec457af725f)) ### Features - First release ([#3](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/3), [`0109dde`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/0109dde7631e6c4e0b96733e37be8a98beae1822)) - Init repo ([#1](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/1), [`c82627f`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/c82627f75d78d41550e3e37a9e9a9ed35feec466)) - Port reset_bluetooth to asyncio ([#2](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/pull/2), [`e7ef901`](https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/commit/e7ef901017f34e57b8111004889c4b4891fb8515)) Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/CONTRIBUTING.md000066400000000000000000000075121520473267500245270ustar00rootroot00000000000000# Contributing Contributions are welcome, and they are greatly appreciated! Every little helps, and credit will always be given. You can contribute in many ways: ## Types of Contributions ### Report Bugs Report bugs to [our issue page][gh-issues]. If you are reporting a bug, please include: - Your operating system name and version. - Any details about your local setup that might be helpful in troubleshooting. - Detailed steps to reproduce the bug. ### Fix Bugs Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. ### Implement Features Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. ### Write Documentation Bluetooth Auto Recovery could always use more documentation, whether as part of the official Bluetooth Auto Recovery docs, in docstrings, or even on the web in blog posts, articles, and such. ### Submit Feedback The best way to send feedback [our issue page][gh-issues] on GitHub. If you are proposing a feature: - Explain in detail how it would work. - Keep the scope as narrow as possible, to make it easier to implement. - Remember that this is a volunteer-driven project, and that contributions are welcome 😊 ## Get Started! Ready to contribute? Here's how to set yourself up for local development. 1. Fork the repo on GitHub. 2. Clone your fork locally: ```shell $ git clone git@github.com:your_name_here/bluetooth-auto-recovery.git ``` 3. Install the project dependencies with [Poetry](https://python-poetry.org): ```shell $ poetry install ``` 4. Create a branch for local development: ```shell $ git checkout -b name-of-your-bugfix-or-feature ``` Now you can make your changes locally. 5. When you're done making changes, check that your changes pass our tests: ```shell $ poetry run pytest ``` 6. Linting is done through [pre-commit](https://pre-commit.com). Provided you have the tool installed globally, you can run them all as one-off: ```shell $ pre-commit run -a ``` Or better, install the hooks once and have them run automatically each time you commit: ```shell $ pre-commit install ``` 7. Commit your changes and push your branch to GitHub: ```shell $ git add . $ git commit -m "feat(something): your detailed description of your changes" $ git push origin name-of-your-bugfix-or-feature ``` Note: the commit message should follow [the conventional commits](https://www.conventionalcommits.org). We run [`commitlint` on CI](https://github.com/marketplace/actions/commit-linter) to validate it, and if you've installed pre-commit hooks at the previous step, the message will be checked at commit time. 8. Submit a pull request through the GitHub website or using the GitHub CLI (if you have it installed): ```shell $ gh pr create --fill ``` ## Pull Request Guidelines We like to have the pull request open as soon as possible, that's a great place to discuss any piece of work, even unfinished. You can use draft pull request if it's still a work in progress. Here are a few guidelines to follow: 1. Include tests for feature or bug fixes. 2. Update the documentation for significant features. 3. Ensure tests are passing on CI. ## Tips To run a subset of tests: ```shell $ pytest tests ``` ## Making a new release The deployment should be automated and can be triggered from the Semantic Release workflow in GitHub. The next version will be based on [the commit logs](https://python-semantic-release.readthedocs.io/en/latest/commit-log-parsing.html#commit-log-parsing). This is done by [python-semantic-release](https://python-semantic-release.readthedocs.io/en/latest/index.html) via a GitHub action. [gh-issues]: https://github.com/bluetooth-devices/bluetooth-auto-recovery/issues Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/LICENSE000066400000000000000000000020601520473267500232740ustar00rootroot00000000000000 MIT License Copyright (c) 2022 J. Nick Koston Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/README.md000066400000000000000000000074671520473267500235660ustar00rootroot00000000000000# Bluetooth Auto Recovery

CI Status Documentation Status Test coverage percentage

Poetry Ruff pre-commit

PyPI Version Supported Python versions License

Recover bluetooth adapters that are in an stuck state ## Credits The code in this repo originates from https://github.com/custom-components/ble_monitor/blob/master/custom_components/ble_monitor/bt_helpers.py ## Installation Install this via pip (or your favourite package manager): `pip install bluetooth-auto-recovery` ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! ## Credits This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the [browniebroke/cookiecutter-pypackage](https://github.com/browniebroke/cookiecutter-pypackage) project template. Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/commitlint.config.mjs000066400000000000000000000003621520473267500264300ustar00rootroot00000000000000export default { extends: ["@commitlint/config-conventional"], rules: { "header-max-length": [0, "always", Infinity], "body-max-line-length": [0, "always", Infinity], "footer-max-line-length": [0, "always", Infinity], }, }; Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/000077500000000000000000000000001520473267500232215ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/Makefile000066400000000000000000000011751520473267500246650ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/make.bat000066400000000000000000000013741520473267500246330ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/000077500000000000000000000000001520473267500245215ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/_static/000077500000000000000000000000001520473267500261475ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/_static/.gitkeep000066400000000000000000000000001520473267500275660ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/changelog.md000066400000000000000000000000451520473267500267710ustar00rootroot00000000000000```{include} ../../CHANGELOG.md ``` Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/conf.py000066400000000000000000000036611520473267500260260ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) from typing import Any # -- Project information ----------------------------------------------------- project = "Bluetooth Auto Recovery" copyright = "2020, J. Nick Koston" author = "J. Nick Koston" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "myst_parser", ] # The suffix of source filenames. source_suffix = [".rst", ".md"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns: list[Any] = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/contributing.md000066400000000000000000000000501520473267500275450ustar00rootroot00000000000000```{include} ../../CONTRIBUTING.md ``` Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/index.md000066400000000000000000000003671520473267500261600ustar00rootroot00000000000000# Welcome to Bluetooth Auto Recovery documentation! ```{toctree} :caption: Installation & Usage :maxdepth: 2 installation usage ``` ```{toctree} :caption: Project Info :maxdepth: 2 changelog contributing ``` ```{include} ../../README.md ``` Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/installation.md000066400000000000000000000003021520473267500275370ustar00rootroot00000000000000# Installation The package is published on [PyPI](https://pypi.org/project/deezer-python/) and can be installed with `pip` (or any equivalent): ```bash pip install bluetooth-auto-recovery ``` Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/usage.md000066400000000000000000000001551520473267500261500ustar00rootroot00000000000000# Usage To use this package, import it: ```python import bluetooth_auto_recovery ``` TODO: Document usage Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/examples/000077500000000000000000000000001520473267500241075ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/examples/reset_hci0.py000066400000000000000000000004431520473267500265070ustar00rootroot00000000000000import asyncio import logging from bluetooth_auto_recovery import recover_adapter logging.basicConfig(level=logging.INFO) logging.getLogger("bluetooth_auto_recovery").setLevel(logging.DEBUG) async def run() -> None: await recover_adapter(0, "00:1a:7d:da:71:13") asyncio.run(run()) Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/poetry.lock000066400000000000000000003714001520473267500244720ustar00rootroot00000000000000# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "aiooui" version = "0.1.9" description = "Async OUI lookups" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] files = [ {file = "aiooui-0.1.9-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:64d904b43f14dd1d8d9fcf1684d9e2f558bc5e0bd68dc10023c93355c9027907"}, {file = "aiooui-0.1.9-py3-none-any.whl", hash = "sha256:737a5e62d8726540218c2b70e5f966d9912121e4644f3d490daf8f3c18b182e5"}, {file = "aiooui-0.1.9.tar.gz", hash = "sha256:e8c8bc59ab352419e0747628b4cce7c4e04d492574c1971e223401126389c5d8"}, ] [[package]] name = "alabaster" version = "0.7.16" description = "A light, configurable Sphinx theme" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" groups = ["main"] markers = "python_version == \"3.10\"" files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [[package]] name = "babel" version = "2.17.0" description = "Internationalization utilities" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [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.10\"" 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 = "bleak" version = "1.1.1" description = "Bluetooth Low Energy platform Agnostic Klient" optional = false python-versions = ">=3.9" groups = ["main"] files = [ {file = "bleak-1.1.1-py3-none-any.whl", hash = "sha256:e601371396e357d95ee3c256db65b7da624c94ef6f051d47dfce93ea8361c22e"}, {file = "bleak-1.1.1.tar.gz", hash = "sha256:eeef18053eb3bd569a25bff62cd4eb9ee56be4d84f5321023a7c4920943e6ccb"}, ] [package.dependencies] async-timeout = {version = ">=3.0.0", markers = "python_version < \"3.11\""} dbus-fast = {version = ">=1.83.0", markers = "platform_system == \"Linux\""} pyobjc-core = {version = ">=10.3", markers = "platform_system == \"Darwin\""} pyobjc-framework-CoreBluetooth = {version = ">=10.3", markers = "platform_system == \"Darwin\""} pyobjc-framework-libdispatch = {version = ">=10.3", markers = "platform_system == \"Darwin\""} typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.12\""} winrt-runtime = {version = ">=3.1", markers = "platform_system == \"Windows\""} "winrt-Windows.Devices.Bluetooth" = {version = ">=3.1", markers = "platform_system == \"Windows\""} "winrt-Windows.Devices.Bluetooth.Advertisement" = {version = ">=3.1", markers = "platform_system == \"Windows\""} "winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = {version = ">=3.1", markers = "platform_system == \"Windows\""} "winrt-Windows.Devices.Enumeration" = {version = ">=3.1", markers = "platform_system == \"Windows\""} "winrt-Windows.Foundation" = {version = ">=3.1", markers = "platform_system == \"Windows\""} "winrt-Windows.Foundation.Collections" = {version = ">=3.1", markers = "platform_system == \"Windows\""} "winrt-Windows.Storage.Streams" = {version = ">=3.1", markers = "platform_system == \"Windows\""} [package.extras] pythonista = ["bleak-pythonista (>=0.1.1)"] [[package]] name = "bluetooth-adapters" version = "0.21.4" description = "Tools to enumerate and find Bluetooth Adapters" optional = false python-versions = ">=3.9" groups = ["main"] files = [ {file = "bluetooth_adapters-0.21.4-py3-none-any.whl", hash = "sha256:ce2e8139cc9d7b103c21654c6309507979e469aae3efebcaeee9923080b0569b"}, {file = "bluetooth_adapters-0.21.4.tar.gz", hash = "sha256:a5a809ef7ba95ee673a78704f90ce34612deb3696269d1a6fd61f98642b99dd3"}, ] [package.dependencies] aiooui = ">=0.1.1" async-timeout = {version = ">=3.0.0", markers = "python_version < \"3.11\""} bleak = ">=0.21.1" dbus-fast = {version = ">=1.21.0", markers = "platform_system == \"Linux\""} uart-devices = ">=0.1.0" usb-devices = ">=0.4.5" [package.extras] docs = ["Sphinx (>=5,<8)", "myst-parser (>=0.18,<3.1)", "sphinx-rtd-theme (>=1,<4)"] [[package]] name = "btsocket" version = "0.3.0" description = "Python library for BlueZ Bluetooth Management API" optional = false python-versions = "*" groups = ["main"] files = [ {file = "btsocket-0.3.0-py2.py3-none-any.whl", hash = "sha256:949821c1b580a88e73804ad610f5173d6ae258e7b4e389da4f94d614344f1a9c"}, {file = "btsocket-0.3.0.tar.gz", hash = "sha256:7ea495de0ff883f0d9f8eea59c72ca7fed492994df668fe476b84d814a147a0d"}, ] [package.extras] dev = ["bumpversion", "coverage", "pycodestyle", "pygments", "sphinx", "sphinx-rtd-theme", "twine"] docs = ["pygments", "sphinx", "sphinx-rtd-theme"] rel = ["bumpversion", "twine"] test = ["coverage", "pycodestyle"] [[package]] name = "certifi" version = "2025.4.26" description = "Python package for providing Mozilla's CA Bundle." optional = true python-versions = ">=3.6" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, ] [[package]] name = "charset-normalizer" version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = true python-versions = ">=3.7" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] markers = {main = "extra == \"docs\" and sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} [[package]] name = "coverage" version = "7.14.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ {file = "coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075"}, {file = "coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82"}, {file = "coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c"}, {file = "coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893"}, {file = "coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20"}, {file = "coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec"}, {file = "coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757"}, {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a"}, {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea"}, {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb"}, {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218"}, {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85"}, {file = "coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323"}, {file = "coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a"}, {file = "coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480"}, {file = "coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4"}, {file = "coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7"}, {file = "coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed"}, {file = "coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980"}, {file = "coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0"}, {file = "coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742"}, {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5"}, {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327"}, {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d"}, {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20"}, {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c"}, {file = "coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3"}, {file = "coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1"}, {file = "coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627"}, {file = "coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5"}, {file = "coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662"}, {file = "coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f"}, {file = "coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67"}, {file = "coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9"}, {file = "coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb"}, {file = "coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e"}, {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3"}, {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4"}, {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1"}, {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5"}, {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595"}, {file = "coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27"}, {file = "coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2"}, {file = "coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d"}, {file = "coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef"}, {file = "coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66"}, {file = "coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b"}, {file = "coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca"}, {file = "coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7"}, {file = "coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2"}, {file = "coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367"}, {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9"}, {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087"}, {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef"}, {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52"}, {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe"}, {file = "coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae"}, {file = "coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e"}, {file = "coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96"}, {file = "coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90"}, {file = "coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1"}, {file = "coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd"}, {file = "coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc"}, {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426"}, {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899"}, {file = "coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b"}, {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90"}, {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f"}, {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d"}, {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47"}, {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477"}, {file = "coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab"}, {file = "coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917"}, {file = "coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8"}, {file = "coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d"}, {file = "coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63"}, {file = "coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212"}, {file = "coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3"}, {file = "coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97"}, {file = "coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8"}, {file = "coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb"}, {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe"}, {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa"}, {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5"}, {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c"}, {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca"}, {file = "coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828"}, {file = "coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d"}, {file = "coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9"}, {file = "coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1"}, {file = "coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c"}, {file = "coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84"}, {file = "coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436"}, {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a"}, {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f"}, {file = "coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb"}, {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490"}, {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9"}, {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020"}, {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6"}, {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db"}, {file = "coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2"}, {file = "coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644"}, {file = "coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b"}, {file = "coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1"}, {file = "coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "dbus-fast" version = "2.44.1" description = "A faster version of dbus-next" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Linux\"" files = [ {file = "dbus_fast-2.44.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c78a004ba43aeaf203a19169d2b4be238375905645999da30cb0da730df80cf2"}, {file = "dbus_fast-2.44.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65a634286651398f3f1326e8200fc54289d52c2c00249d29cacfc691660a5da1"}, {file = "dbus_fast-2.44.1-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:0c4a128f8b29941307fc5722f37a1bb87ddcf733188d917ab374d9da0c6e1ce7"}, {file = "dbus_fast-2.44.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adaf459fbce22a63d3578f3ec782c6978edf975eb06d71fb5b7a690496cf6bbe"}, {file = "dbus_fast-2.44.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de871cf722c436bdcceb96b2a3af7084e1fa468f7916ae278ec8ec49a6fa7eef"}, {file = "dbus_fast-2.44.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b40863de172031bcc02f54c6f05cccb0b882dc2e1b09e11314a8ccf38c558760"}, {file = "dbus_fast-2.44.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b7ae16555df6b56d3befcc51e036779ef47c0e954fdb9fb0821ac25212aefe9"}, {file = "dbus_fast-2.44.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a220a28e88062a2548f0c6da9eb15fb7e3af70eae56729fc3795ce3e3fba057d"}, {file = "dbus_fast-2.44.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ec5db912bd4cfeadf7134163d6dde684271cd44cf26e3b4720107f3de406623"}, {file = "dbus_fast-2.44.1-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:6ad99f626837753b39a39e09facd2091ee4851ee1eb6ebec5fa9a9a231734254"}, {file = "dbus_fast-2.44.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7aa157f689a114bfb5367c55884d35e25d57cf25202a6590ce05010f929e7df"}, {file = "dbus_fast-2.44.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f961d8bcad80359f24c0156b3094f58a87d583d56139ee50922fe5894b6797cf"}, {file = "dbus_fast-2.44.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1f38fb5c31846c3ada8fc2b693d8d19953d376a9ea21079e3686e93faa1f8a0f"}, {file = "dbus_fast-2.44.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35e3cde53cc9180ce95c6c84a1e8d1ded429031e4a0a182606e8d22cf57d3294"}, {file = "dbus_fast-2.44.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f30fb09f1ea13658fb4316511e27d6b94f8363b16f2d093efe73e6e289b740"}, {file = "dbus_fast-2.44.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dd0f8d41f6ab9d4a782c116470bc319d690f9b50c97b6debc6d1fef08e4615a"}, {file = "dbus_fast-2.44.1-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9d6e386658343db380b9e4e81b3bf4e3c17135dbb5889173b1f2582b675b9a8c"}, {file = "dbus_fast-2.44.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bd27563c11219b6fde7a5458141d860d8445c2defb036bab360d1f9bf1dfae0"}, {file = "dbus_fast-2.44.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0272784aceac821dd63c8187a8860179061a850269617ff5c5bd25ca37bf9307"}, {file = "dbus_fast-2.44.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eed613a909a45f0e0a415c88b373024f007a9be56b1316812ed616d69a3b9161"}, {file = "dbus_fast-2.44.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0d4288f2cba4f8309dcfd9f4392e0f4f2b5be6c796dfdb0c5e03228b1ab649b1"}, {file = "dbus_fast-2.44.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50a9a4c6921f4b7446717fb4869750f54b561ce486b25b36550cb2a910c988d9"}, {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89dc5db158bf9838979f732acc39e0e1ecd7e3295a09fa8adb93b09c097615a4"}, {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:f11878c0c089d278861e48c02db8002496c2233b0f605b5630ef61f0b7fb0ea3"}, {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd81f483b3ffb71e88478cfabccc1fab8d7154fccb1c661bfafcff9b0cfd996"}, {file = "dbus_fast-2.44.1-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:ad499de96a991287232749c98a59f2436ed260f6fd9ad4cb3b04a4b1bbbef148"}, {file = "dbus_fast-2.44.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36c44286b11e83977cd29f9551b66b446bb6890dff04585852d975aa3a038ca2"}, {file = "dbus_fast-2.44.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:89f2f6eccbb0e464b90e5a8741deb9d6a91873eeb41a8c7b963962b39eb1e0cd"}, {file = "dbus_fast-2.44.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb74a227b071e1a7c517bf3a3e4a5a0a2660620084162e74f15010075534c9d5"}, {file = "dbus_fast-2.44.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e3719399e687359b0ef66af1b720661dd4f12059db1c4f506e678569a2256b4"}, {file = "dbus_fast-2.44.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:806450623ef3f8df846524da7e448edc8174261a01cfd5dfda92e3df89c0de10"}, {file = "dbus_fast-2.44.1-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:55ad499b7ef08cb76fce9c9fdcdd6589d2ebfc7e53b3d261d8f40c6d97a8d901"}, {file = "dbus_fast-2.44.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55d717865219ec2ae9977b6d067c05261cdc3ef6205c687c8bb92b3437886e58"}, {file = "dbus_fast-2.44.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39d4cc61e491e11912f76d70cc1c47387ab4f2e5b71f34bfa13eb11aa6026268"}, {file = "dbus_fast-2.44.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9b3b10151f1140f7b6dd47a89fc37edd05d6213be0a1748eadba82fc144c05c2"}, {file = "dbus_fast-2.44.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:33772c223f5cef1bacc298e83dc04b27b3a47065b245fde766fcc126e761dca7"}, {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e3f42f982af45bcfa0ff23e808f3aa54a45fe4bf43aadd3beb5ace816fba76"}, {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f29a81d86c9ce3020a5df8c1e5557edaa00e1e00c9804ec874d46c99d967a686"}, {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:5dec134715457601c0fa8df3040a56d319de1a152464ae4d4bfc53bbb5c02e04"}, {file = "dbus_fast-2.44.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893509b516f2f24b4e3f09a6b1f3a30f856cf237cd773cdc505ea7ab4fa3c863"}, {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:db81275d708774f6a17c89f2e063398c0deb358c4d22b663a3dd99861f6683a4"}, {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:161a3e6fc8783c30c9feb072e09604d96ec0c465b06bd35b6acc1a0316bd2a27"}, {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:67febe6454e714d85a532bd84969001ed948bbaf1699a7e1e4c6abb5508c9522"}, {file = "dbus_fast-2.44.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890f0fc046d5db66524ddedeca8c14b65739fbbf32d6488175c07428362bf250"}, {file = "dbus_fast-2.44.1.tar.gz", hash = "sha256:b027e96c39ed5622bb54d811dcdbbe9d9d6edec3454808a85a1ceb1867d9e25c"}, ] [[package]] name = "docutils" version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] name = "exceptiongroup" version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "idna" version = "3.15" description = "Internationalized Domain Names in Applications (IDNA)" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, ] [package.extras] all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] [[package]] name = "iniconfig" version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] name = "jinja2" version = "3.1.6" description = "A very fast and expressive template engine." optional = true python-versions = ">=3.7" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] code-style = ["pre-commit (>=3.0,<4.0)"] compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] name = "mdit-py-plugins" version = "0.4.2" description = "Collection of plugins for markdown-it-py" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, ] [package.dependencies] markdown-it-py = ">=1.0.0,<4.0.0" [package.extras] code-style = ["pre-commit"] rtd = ["myst-parser", "sphinx-book-theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" optional = true python-versions = ">=3.7" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] [[package]] name = "myst-parser" version = "3.0.1" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, ] [package.dependencies] docutils = ">=0.18,<0.22" jinja2 = "*" markdown-it-py = ">=3.0,<4.0" mdit-py-plugins = ">=0.4,<1.0" pyyaml = "*" sphinx = ">=6,<8" [package.extras] code-style = ["pre-commit (>=3.0,<4.0)"] linkify = ["linkify-it-py (>=2.0,<3.0)"] rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] [[package]] name = "packaging" version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] markers = {main = "extra == \"docs\""} [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pygments" version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] markers = {main = "extra == \"docs\""} [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyobjc-core" version = "12.1" description = "Python<->ObjC Interoperability Module" optional = false python-versions = ">=3.10" groups = ["main"] markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_core-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93418e79c1655f66b4352168f8c85c942707cb1d3ea13a1da3e6f6a143bacda7"}, {file = "pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6"}, {file = "pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962"}, {file = "pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a"}, {file = "pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc"}, {file = "pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43"}, {file = "pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882"}, {file = "pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21"}, ] [[package]] name = "pyobjc-framework-cocoa" version = "12.1" description = "Wrappers for the Cocoa frameworks on macOS" optional = false python-versions = ">=3.10" groups = ["main"] markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_cocoa-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9b880d3bdcd102809d704b6d8e14e31611443aa892d9f60e8491e457182fdd48"}, {file = "pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6"}, {file = "pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858"}, {file = "pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118"}, {file = "pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44"}, {file = "pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a"}, {file = "pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc"}, {file = "pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640"}, ] [package.dependencies] pyobjc-core = ">=12.1" [[package]] name = "pyobjc-framework-corebluetooth" version = "12.1" description = "Wrappers for the framework CoreBluetooth on macOS" optional = false python-versions = ">=3.10" groups = ["main"] markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_corebluetooth-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:937849f4d40a33afbcc56cbe90c8d1fbf30fb27a962575b9fb7e8e2c61d3c551"}, {file = "pyobjc_framework_corebluetooth-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:37e6456c8a076bd5a2bdd781d0324edd5e7397ef9ac9234a97433b522efb13cf"}, {file = "pyobjc_framework_corebluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe72c9732ee6c5c793b9543f08c1f5bdd98cd95dfc9d96efd5708ec9d6eeb213"}, {file = "pyobjc_framework_corebluetooth-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a894f695e6c672f0260327103a31ad8b98f8d4fb9516a0383db79a82a7e58dc"}, {file = "pyobjc_framework_corebluetooth-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1daf07a0047c3ed89fab84ad5f6769537306733b6a6e92e631581a0f419e3f32"}, {file = "pyobjc_framework_corebluetooth-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:15ba5207ca626dffe57ccb7c1beaf01f93930159564211cb97d744eaf0d812aa"}, {file = "pyobjc_framework_corebluetooth-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e5385195bd365a49ce70e2fb29953681eefbe68a7b15ecc2493981d2fb4a02b1"}, {file = "pyobjc_framework_corebluetooth-12.1.tar.gz", hash = "sha256:8060c1466d90bbb9100741a1091bb79975d9ba43911c9841599879fc45c2bbe0"}, ] [package.dependencies] pyobjc-core = ">=12.1" pyobjc-framework-Cocoa = ">=12.1" [[package]] name = "pyobjc-framework-libdispatch" version = "12.1" description = "Wrappers for libdispatch on macOS" optional = false python-versions = ">=3.10" groups = ["main"] markers = "platform_system == \"Darwin\"" files = [ {file = "pyobjc_framework_libdispatch-12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:50a81a29506f0e35b4dc313f97a9d469f7b668dae3ba597bb67bbab94de446bd"}, {file = "pyobjc_framework_libdispatch-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:954cc2d817b71383bd267cc5cd27d83536c5f879539122353ca59f1c945ac706"}, {file = "pyobjc_framework_libdispatch-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e9570d7a9a3136f54b0b834683bf3f206acd5df0e421c30f8fd4f8b9b556789"}, {file = "pyobjc_framework_libdispatch-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:58ffce5e6bcd7456b4311009480b195b9f22107b7682fb0835d4908af5a68ad0"}, {file = "pyobjc_framework_libdispatch-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e9f49517e253716e40a0009412151f527005eec0b9a2311ac63ecac1bdf02332"}, {file = "pyobjc_framework_libdispatch-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0ebfd9e4446ab6528126bff25cfb09e4213ddf992b3208978911cfd3152e45f5"}, {file = "pyobjc_framework_libdispatch-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:23fc9915cba328216b6a736c7a48438a16213f16dfb467f69506300b95938cc7"}, {file = "pyobjc_framework_libdispatch-12.1.tar.gz", hash = "sha256:4035535b4fae1b5e976f3e0e38b6e3442ffea1b8aa178d0ca89faa9b8ecdea41"}, ] [package.dependencies] pyobjc-core = ">=12.1" pyobjc-framework-Cocoa = ">=12.1" [[package]] name = "pyric" version = "0.1.6.3" description = "Python Wireless Library" optional = false python-versions = "*" groups = ["main"] files = [ {file = "PyRIC-0.1.6.3.tar.gz", hash = "sha256:b539b01cafebd2406c00097f94525ea0f8ecd1dd92f7731f43eac0ef16c2ccc9"}, ] [[package]] name = "pytest" version = "9.0.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, ] [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} iniconfig = ">=1.0.1" packaging = ">=22" 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", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "1.3.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, ] [package.dependencies] backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} pytest = ">=8.2,<10" typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" version = "7.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ {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.10.6", extras = ["toml"]} pluggy = ">=1.2" pytest = ">=7" [package.extras] testing = ["process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] name = "requests" version = "2.33.0" description = "Python HTTP for Humans." optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"}, {file = "requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"}, ] [package.dependencies] certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] test = ["PySocks (>=1.5.6,!=1.5.7)", "pytest (>=3)", "pytest-cov", "pytest-httpbin (==2.1.0)", "pytest-mock", "pytest-xdist"] use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = true python-versions = "*" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] [[package]] name = "sphinx" version = "7.4.7" description = "Python documentation generator" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, ] [package.dependencies] alabaster = ">=0.7.14,<0.8.0" babel = ">=2.13" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} docutils = ">=0.20,<0.22" imagesize = ">=1.3" Jinja2 = ">=3.1" packaging = ">=23.0" Pygments = ">=2.17" requests = ">=2.30.0" snowballstemmer = ">=2.2" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.9" tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] name = "sphinx-rtd-theme" version = "3.0.2" description = "Read the Docs theme for Sphinx" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"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"}, ] [package.dependencies] docutils = ">0.18,<0.22" sphinx = ">=6,<9" sphinxcontrib-jquery = ">=4,<5" [package.extras] dev = ["bump2version", "transifex-client", "twine", "wheel"] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jquery" version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = true python-versions = ">=2.7" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, ] [package.dependencies] Sphinx = ">=1.8" [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = true python-versions = ">=3.5" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] [package.extras] test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["defusedxml (>=0.7.1)", "pytest"] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = true python-versions = ">=3.9" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "tomli" version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] markers = {main = "extra == \"docs\" and python_version == \"3.10\"", dev = "python_full_version <= \"3.11.0a6\""} [[package]] name = "typing-extensions" version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] markers = {main = "python_version < \"3.12\" or platform_system == \"Windows\"", dev = "python_version < \"3.13\""} [[package]] name = "uart-devices" version = "0.1.1" description = "UART Devices for Linux" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] files = [ {file = "uart_devices-0.1.1-py3-none-any.whl", hash = "sha256:55bc8cce66465e90b298f0910e5c496bc7be021341c5455954cf61c6253dc123"}, {file = "uart_devices-0.1.1.tar.gz", hash = "sha256:3a52c4ae0f5f7400ebe1ae5f6e2a2d40cc0b7f18a50e895236535c4e53c6ed34"}, ] [[package]] name = "urllib3" version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"docs\"" files = [ {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] 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 = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "usb-devices" version = "0.4.5" description = "Tools for mapping, describing, and resetting USB devices" optional = false python-versions = ">=3.9,<4.0" groups = ["main"] files = [ {file = "usb_devices-0.4.5-py3-none-any.whl", hash = "sha256:8a415219ef1395e25aa0bddcad484c88edf9673acdeae8a07223ca7222a01dcf"}, {file = "usb_devices-0.4.5.tar.gz", hash = "sha256:9b5c7606df2bc791c6c45b7f76244a0cbed83cb6fa4c68791a143c03345e195d"}, ] [[package]] name = "winrt-runtime" version = "3.2.1" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Windows\"" files = [ {file = "winrt_runtime-3.2.1-cp310-cp310-win32.whl", hash = "sha256:25a2d1e2b45423742319f7e10fa8ca2e7063f01284b6e85e99d805c4b50bbfb3"}, {file = "winrt_runtime-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:dc81d5fb736bf1ddecf743928622253dce4d0aac9a57faad776d7a3834e13257"}, {file = "winrt_runtime-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:363f584b1e9fcb601e3e178636d8877e6f0537ac3c96ce4a96f06066f8ff0eae"}, {file = "winrt_runtime-3.2.1-cp311-cp311-win32.whl", hash = "sha256:9e9b64f1ba631cc4b9fe60b8ff16fef3f32c7ce2fcc84735a63129ff8b15c022"}, {file = "winrt_runtime-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:c0a9046ae416808420a358c51705af8ae100acd40bc578be57ddfdd51cbb0f9c"}, {file = "winrt_runtime-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:e94f3cb40ea2d723c44c82c16d715c03c6b3bd977d135b49535fdd5415fd9130"}, {file = "winrt_runtime-3.2.1-cp312-cp312-win32.whl", hash = "sha256:762b3d972a2f7037f7db3acbaf379dd6d8f6cda505f71f66c6b425d1a1eae2f1"}, {file = "winrt_runtime-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:06510db215d4f0dc45c00fbb1251c6544e91742a0ad928011db33b30677e1576"}, {file = "winrt_runtime-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:14562c29a087ccad38e379e585fef333e5c94166c807bdde67b508a6261aa195"}, {file = "winrt_runtime-3.2.1-cp313-cp313-win32.whl", hash = "sha256:44e2733bc709b76c554aee6c7fe079443b8306b2e661e82eecfebe8b9d71e4d1"}, {file = "winrt_runtime-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:3c1fdcaeedeb2920dc3b9039db64089a6093cad2be56a3e64acc938849245a6d"}, {file = "winrt_runtime-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:28f3dab083412625ff4d2b46e81246932e6bebddf67bea7f05e01712f54e6159"}, {file = "winrt_runtime-3.2.1-cp314-cp314-win32.whl", hash = "sha256:9b6298375468ac2f6815d0c008a059fc16508c8f587e824c7936ed9216480dad"}, {file = "winrt_runtime-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:e36e587ab5fd681ee472cd9a5995743f75107a1a84d749c64f7e490bc86bc814"}, {file = "winrt_runtime-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:35d6241a2ebd5598e4788e69768b8890ee1eee401a819865767a1fbdd3e9a650"}, {file = "winrt_runtime-3.2.1-cp39-cp39-win32.whl", hash = "sha256:07c0cb4a53a4448c2cb7597b62ae8c94343c289eeebd8f83f946eb2c817bde01"}, {file = "winrt_runtime-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1856325ca3354b45e0789cf279be9a882134085d34214946db76110d98391efa"}, {file = "winrt_runtime-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:cf237858de1d62e4c9b132c66b52028a7a3e8534e8ab90b0e29a68f24f7be39d"}, {file = "winrt_runtime-3.2.1.tar.gz", hash = "sha256:c8dca19e12b234ae6c3dadf1a4d0761b51e708457492c13beb666556958801ea"}, ] [package.dependencies] typing_extensions = ">=4.12.2" [[package]] name = "winrt-windows-devices-bluetooth" version = "3.2.1" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Windows\"" files = [ {file = "winrt_windows_devices_bluetooth-3.2.1-cp310-cp310-win32.whl", hash = "sha256:49489351037094a088a08fbdf0f99c94e3299b574edb211f717c4c727770af78"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:20f6a21029034c18ea6a6b6df399671813b071102a0d6d8355bb78cf4f547cdb"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:69c523814eab795bc1bf913292309cb1025ef0a67d5fc33863a98788995e551d"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp311-cp311-win32.whl", hash = "sha256:f4082a00b834c1e34b961e0612f3e581356bdb38c5798bd6842f88ec02e5152b"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:44277a3f2cc5ac32ce9b4b2d96c5c5f601d394ac5f02cc71bcd551f738660e2d"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:0803a417403a7d225316b9b0c4fe3f8446579d6a22f2f729a2c21f4befc74a80"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win32.whl", hash = "sha256:18c833ec49e7076127463679e85efc59f61785ade0dc185c852586b21be1f31c"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:9b6702c462b216c91e32388023a74d0f87210cef6fd5d93b7191e9427ce2faca"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:419fd1078c7749119f6b4bbf6be4e586e03a0ed544c03b83178f1d85f1b3d148"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win32.whl", hash = "sha256:12b0a16fb36ce0b42243ca81f22a6b53fbb344ed7ea07a6eeec294604f0505e4"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6703dfbe444ee22426738830fb305c96a728ea9ccce905acfdf811d81045fdb3"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2cf8a0bfc9103e32dc7237af15f84be06c791f37711984abdca761f6318bbdb2"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win32.whl", hash = "sha256:de36ded53ca3ba12fc6dd4deb14b779acc391447726543815df4800348aad63a"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3295d932cc93259d5ccb23a41e3a3af4c78ce5d6a6223b2b7638985f604fa34c"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1f61c178766a1bbce0669f44790c6161ff4669404c477b4aedaa576348f9e102"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp39-cp39-win32.whl", hash = "sha256:32fc355bfdc5d6b3b1875df16eaf12f9b9fc0445e01177833c27d9a4fc0d50b6"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b886ef1fc0ed49163ae6c2422dd5cb8dd4709da7972af26c8627e211872818d0"}, {file = "winrt_windows_devices_bluetooth-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:8643afa53f9fb8fe3b05967227f86f0c8e1d7b822289e60a848c6368acc977d2"}, {file = "winrt_windows_devices_bluetooth-3.2.1.tar.gz", hash = "sha256:db496d2d92742006d5a052468fc355bf7bb49e795341d695c374746113d74505"}, ] [package.dependencies] winrt-runtime = ">=3.2.1.0,<3.3.0.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Devices.Enumeration[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Devices.Radios[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Networking[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage.Streams[all] (>=3.2.1.0,<3.3.0.0)"] [[package]] name = "winrt-windows-devices-bluetooth-advertisement" version = "3.2.1" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Windows\"" files = [ {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp310-cp310-win32.whl", hash = "sha256:a758c5f81a98cc38347fdfb024ce62720969480e8c5b98e402b89d2b09b32866"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:f982ef72e729ddd60cdb975293866e84bb838798828933012a57ee4bf12b0ea1"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:e88a72e1e09c7ccc899a9e6d2ab3fc0f43b5dd4509bcc49ec4abf65b55ab015f"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp311-cp311-win32.whl", hash = "sha256:fe17c2cf63284646622e8b2742b064bf7970bbf53cfab02062136c67fa6b06c9"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:78e99dd48b4d89b71b7778c5085fdba64e754dd3ebc54fd09c200fe5222c6e09"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6d5d2295474deab444fc4311580c725a2ca8a814b0f3344d0779828891d75401"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win32.whl", hash = "sha256:901933cc40de5eb7e5f4188897c899dd0b0f577cb2c13eab1a63c7dfe89b08c4"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e6c66e7d4f4ca86d2c801d30efd2b9673247b59a2b4c365d9e11650303d68d89"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:447d19defd8982d39944642eb7ebe89e4e20259ec9734116cf88879fb2c514ff"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4122348ea525a914e85615647a0b54ae8b2f42f92cdbf89c5a12eea53ef6ed90"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:b66410c04b8dae634a7e4b615c3b7f8adda9c7d4d6902bcad5b253da1a684943"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:07af19b1d252ddb9dd3eb2965118bc2b7cabff4dda6e499341b765e5038ca61d"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win32.whl", hash = "sha256:2985565c265b3f9eab625361b0e40e88c94b03d89f5171f36146f2e88b3ee214"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:d102f3fac64fde32332e370969dfbc6f37b405d8cc055d9da30d14d07449a3c2"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:ffeb5e946cd42c32c6999a62e240d6730c653cdfb7b49c7839afba375e20a62a"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp39-cp39-win32.whl", hash = "sha256:6c4747d2e5b0e2ef24e9b84a848cf8fc50fb5b268a2086b5ee8680206d1e0197"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:18d4c5d8b80ee2d29cc13c2fc1353fdb3c0f620c8083701c9b9ecf5e6c503c8d"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:75dd856611d847299078d56aee60e319df52975b931c992cd1d32ad5143fe772"}, {file = "winrt_windows_devices_bluetooth_advertisement-3.2.1.tar.gz", hash = "sha256:0223852a7b7fa5c8dea3c6a93473bd783df4439b1ed938d9871f947933e574cc"}, ] [package.dependencies] winrt-runtime = ">=3.2.1.0,<3.3.0.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage.Streams[all] (>=3.2.1.0,<3.3.0.0)"] [[package]] name = "winrt-windows-devices-bluetooth-genericattributeprofile" version = "3.2.1" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Windows\"" files = [ {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp310-cp310-win32.whl", hash = "sha256:af4914d7b30b49232092cd3b934e3ed6f5d3b1715ba47238541408ee595b7f46"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:0e557dd52fc80392b8bd7c237e1153a50a164b3983838b4ac674551072efc9ed"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:64cff62baa6b7aadd6c206e61d149113fdcda17360feb6e9d05bc8bbda4b9fde"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp311-cp311-win32.whl", hash = "sha256:832cf65d035a11e6dbfef4fd66abdcc46be7e911ec96e2e72e98e12d8d5b9d3c"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:8179638a6c721b0bbf04ba251ef98d5e02d9a17f0cce377398e42c4fbb441415"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:70b7edfca3190b89ae38bf60972b11978311b6d933d3142ae45560c955dbf5c7"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win32.whl", hash = "sha256:ef894d21e0a805f3e114940254636a8045335fa9de766c7022af5d127dfad557"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:db05de95cd1b24a51abb69cb936a8b17e9214e015757d0b37e3a5e207ddceb3d"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d4e131cf3d15fc5ad81c1bcde3509ac171298217381abed6bdf687f29871984"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win32.whl", hash = "sha256:b1879c8dcf46bd2110b9ad4b0b185f4e2a5f95170d014539203a5fee2b2115f0"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d8d89f01e9b6931fb48217847caac3227a0aeb38a5b7782af71c2e7b262ec30"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:4e71207bb89798016b1795bb15daf78afe45529f2939b3b9e78894cfe650b383"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win32.whl", hash = "sha256:d5f83739ca370f0baf52b0400aebd6240ab80150081fbfba60fd6e7b2e7b4c5f"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:13786a5853a933de140d456cd818696e1121c7c296ae7b7af262fc5d2cffb851"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:5140682da2860f6a55eb6faf9e980724dc457c2e4b4b35a10e1cebd8fc97d892"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp39-cp39-win32.whl", hash = "sha256:963339a0161f9970b577a6193924be783978d11693da48b41a025f61b3c5562a"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:d43615c5dfa939dd30fe80dc0649434a13cc7cf0294ad0d7283d5a9f48c6ce86"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:8e70fa970997e2e67a8a4172bc00b0b2a79b5ff5bb2668f79cf10b3fd63d3974"}, {file = "winrt_windows_devices_bluetooth_genericattributeprofile-3.2.1.tar.gz", hash = "sha256:cdf6ddc375e9150d040aca67f5a17c41ceaf13a63f3668f96608bc1d045dde71"}, ] [package.dependencies] winrt-runtime = ">=3.2.1.0,<3.3.0.0" [package.extras] all = ["winrt-Windows.Devices.Bluetooth[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Devices.Enumeration[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage.Streams[all] (>=3.2.1.0,<3.3.0.0)"] [[package]] name = "winrt-windows-devices-enumeration" version = "3.2.1" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Windows\"" files = [ {file = "winrt_windows_devices_enumeration-3.2.1-cp310-cp310-win32.whl", hash = "sha256:40dac777d8f45b41449f3ff1ae70f0d457f1ede53f53962a6e2521b651533db5"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:a101ec3e0ad0a0783032fdcd5dc48e7cd68ee034cbde4f903a8c7b391532c71a"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:3296a3863ac086928ff3f3dc872b2a2fb971dab728817424264f3ca547504e9e"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp311-cp311-win32.whl", hash = "sha256:9f29465a6c6b0456e4330d4ad09eccdd53a17e1e97695c2e57db0d4666cc0011"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2a725d04b4cb43aa0e2af035f73a60d16a6c0ff165fcb6b763383e4e33a975fd"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6365ef5978d4add26678827286034acf474b6b133aa4054e76567d12194e6817"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win32.whl", hash = "sha256:1db22b0292b93b0688d11ad932ad1f3629d4f471310281a2fbfe187530c2c1f3"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:a73bc88d7f510af454f2b392985501c96f39b89fd987140708ccaec1588ceebc"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:2853d687803f0dd76ae1afe3648abc0453e09dff0e7eddbb84b792eddb0473ca"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win32.whl", hash = "sha256:14a71cdcc84f624c209cbb846ed6bd9767a9a9437b2bf26b48ac9a91599da6e9"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6ca40d334734829e178ad46375275c4f7b5d6d2d4fc2e8879690452cbfb36015"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2d14d187f43e4409c7814b7d1693c03a270e77489b710d92fcbbaeca5de260d4"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win32.whl", hash = "sha256:e087364273ed7c717cd0191fed4be9def6fdf229fe9b536a4b8d0228f7814106"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:0da1ddb8285d97a6775c36265d7157acf1bbcb88bcc9a7ce9a4549906c822472"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:09bf07e74e897e97a49a9275d0a647819254ddb74142806bbbcf4777ed240a22"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp39-cp39-win32.whl", hash = "sha256:986e8d651b769a0e60d2834834bdd3f6959f6a88caa0c9acb917797e6b43a588"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10da7d403ac4afd385fe13bd5808c9a5dd616a8ef31ca5c64cea3f87673661c1"}, {file = "winrt_windows_devices_enumeration-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:679e471d21ac22cb50de1bf4dfc4c0c3f5da9f3e3fbc7f08dcacfe9de9d6dd58"}, {file = "winrt_windows_devices_enumeration-3.2.1.tar.gz", hash = "sha256:df316899e39bfc0ffc1f3cb0f5ee54d04e1d167fbbcc1484d2d5121449a935cf"}, ] [package.dependencies] winrt-runtime = ">=3.2.1.0,<3.3.0.0" [package.extras] all = ["winrt-Windows.ApplicationModel.Background[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Security.Credentials[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage.Streams[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.UI.Popups[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.UI[all] (>=3.2.1.0,<3.3.0.0)"] [[package]] name = "winrt-windows-foundation" version = "3.2.1" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Windows\"" files = [ {file = "winrt_windows_foundation-3.2.1-cp310-cp310-win32.whl", hash = "sha256:677e98165dcbbf7a2367f905bc61090ef2c568b6e465f87cf7276df4734f3b0b"}, {file = "winrt_windows_foundation-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8f27b4f0fdb73ccc4a3e24bc8010a6607b2bdd722fa799eafce7daa87d19d39"}, {file = "winrt_windows_foundation-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:d900c6165fab4ea589811efa2feed27b532e1b6f505f63bf63e2052b8cb6bdc4"}, {file = "winrt_windows_foundation-3.2.1-cp311-cp311-win32.whl", hash = "sha256:d1b5970241ccd61428f7330d099be75f4f52f25e510d82c84dbbdaadd625e437"}, {file = "winrt_windows_foundation-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:f3762be2f6e0f2aedf83a0742fd727290b397ffe3463d963d29211e4ebb53a7e"}, {file = "winrt_windows_foundation-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:806c77818217b3476e6c617293b3d5b0ff8a9901549dc3417586f6799938d671"}, {file = "winrt_windows_foundation-3.2.1-cp312-cp312-win32.whl", hash = "sha256:867642ccf629611733db482c4288e17b7919f743a5873450efb6d69ae09fdc2b"}, {file = "winrt_windows_foundation-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:45550c5b6c2125cde495c409633e6b1ea5aa1677724e3b95eb8140bfccbe30c9"}, {file = "winrt_windows_foundation-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:94f4661d71cb35ebc52be7af112f2eeabdfa02cb05e0243bf9d6bd2cafaa6f37"}, {file = "winrt_windows_foundation-3.2.1-cp313-cp313-win32.whl", hash = "sha256:3998dc58ed50ecbdbabace1cdef3a12920b725e32a5806d648ad3f4829d5ba46"}, {file = "winrt_windows_foundation-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:6e98617c1e46665c7a56ce3f5d28e252798416d1ebfee3201267a644a4e3c479"}, {file = "winrt_windows_foundation-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:2a8c1204db5c352f6a563130a5a41d25b887aff7897bb677d4ff0b660315aad4"}, {file = "winrt_windows_foundation-3.2.1-cp314-cp314-win32.whl", hash = "sha256:35e973ab3c77c2a943e139302256c040e017fd6ff1a75911c102964603bba1da"}, {file = "winrt_windows_foundation-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22a7ebcec0d262e60119cff728f32962a02df60471ded8b2735a655eccc0ef5"}, {file = "winrt_windows_foundation-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:3be7fbae829b98a6a946db4fbaf356b11db1fbcbb5d4f37e7a73ac6b25de8b87"}, {file = "winrt_windows_foundation-3.2.1-cp39-cp39-win32.whl", hash = "sha256:14d5191725301498e4feb744d91f5b46ce317bf3d28370efda407d5c87f4423b"}, {file = "winrt_windows_foundation-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:de5e4f61d253a91ba05019dbf4338c43f962bdad935721ced5e7997933994af5"}, {file = "winrt_windows_foundation-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:ebbf6e8168398c9ed0c72c8bdde95a406b9fbb9a23e3705d4f0fe28e5a209705"}, {file = "winrt_windows_foundation-3.2.1.tar.gz", hash = "sha256:ad2f1fcaa6c34672df45527d7c533731fdf65b67c4638c2b4aca949f6eec0656"}, ] [package.dependencies] winrt-runtime = ">=3.2.1.0,<3.3.0.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)"] [[package]] name = "winrt-windows-foundation-collections" version = "3.2.1" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Windows\"" files = [ {file = "winrt_windows_foundation_collections-3.2.1-cp310-cp310-win32.whl", hash = "sha256:46948484addfc4db981dab35688d4457533ceb54d4954922af41503fddaa8389"}, {file = "winrt_windows_foundation_collections-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:899eaa3a93c35bfb1857d649e8dd60c38b978dda7cedd9725fcdbcebba156fd6"}, {file = "winrt_windows_foundation_collections-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:c36eb49ad1eba1b32134df768bb47af13cabb9b59f974a3cea37843e2d80e0e6"}, {file = "winrt_windows_foundation_collections-3.2.1-cp311-cp311-win32.whl", hash = "sha256:9b272d9936e7db4840881c5dcf921eb26789ae4ef23fb6ec15e13e19a16254e7"}, {file = "winrt_windows_foundation_collections-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:c646a5d442dd6540ade50890081ca118b41f073356e19032d0a5d7d0d38fbc89"}, {file = "winrt_windows_foundation_collections-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:2c4630027c93cdd518b0cf4cc726b8fbdbc3388e36d02aa1de190a0fc18ca523"}, {file = "winrt_windows_foundation_collections-3.2.1-cp312-cp312-win32.whl", hash = "sha256:15704eef3125788f846f269cf54a3d89656fa09a1dc8428b70871f717d595ad6"}, {file = "winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:550dfb8c82fe74d9e0728a2a16a9175cc9e34ca2b8ef758d69b2a398894b698b"}, {file = "winrt_windows_foundation_collections-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:810ad4bd11ab4a74fdbcd3ed33b597ef7c0b03af73fc9d7986c22bcf3bd24f84"}, {file = "winrt_windows_foundation_collections-3.2.1-cp313-cp313-win32.whl", hash = "sha256:4267a711b63476d36d39227883aeb3fb19ac92b88a9fc9973e66fbce1fd4aed9"}, {file = "winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:5e12a6e75036ee90484c33e204b85fb6785fcc9e7c8066ad65097301f48cdd10"}, {file = "winrt_windows_foundation_collections-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:34b556255562f1b36d07fba933c2bcd9f0db167fa96727a6cbb4717b152ad7a2"}, {file = "winrt_windows_foundation_collections-3.2.1-cp314-cp314-win32.whl", hash = "sha256:33188ed2d63e844c8adfbb82d1d3d461d64aaf78d225ce9c5930421b413c45ab"}, {file = "winrt_windows_foundation_collections-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:d4cfece7e9c0ead2941e55a1da82f20d2b9c8003bb7a8853bb7f999b539f80a4"}, {file = "winrt_windows_foundation_collections-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:3884146fea13727510458f6a14040b7632d5d90127028b9bfd503c6c655d0c01"}, {file = "winrt_windows_foundation_collections-3.2.1-cp39-cp39-win32.whl", hash = "sha256:20610f098b84c87765018cbc71471092197881f3b92e5d06158fad3bfcea2563"}, {file = "winrt_windows_foundation_collections-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9739775320ac4c0238e1775d94a54e886d621f9995977e65d4feb8b3778c111"}, {file = "winrt_windows_foundation_collections-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:e4c6bddb1359d5014ceb45fe2ecd838d4afeb1184f2ea202c2d21037af0d08a3"}, {file = "winrt_windows_foundation_collections-3.2.1.tar.gz", hash = "sha256:0eff1ad0d8d763ad17e9e7bbd0c26a62b27215016393c05b09b046d6503ae6d5"}, ] [package.dependencies] winrt-runtime = ">=3.2.1.0,<3.3.0.0" [package.extras] all = ["winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)"] [[package]] name = "winrt-windows-storage-streams" version = "3.2.1" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false python-versions = ">=3.9" groups = ["main"] markers = "platform_system == \"Windows\"" files = [ {file = "winrt_windows_storage_streams-3.2.1-cp310-cp310-win32.whl", hash = "sha256:89bb2d667ebed6861af36ed2710757456e12921ee56347946540320dacf6c003"}, {file = "winrt_windows_storage_streams-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:48a78e5dc7d3488eb77e449c278bc6d6ac28abcdda7df298462c4112d7635d00"}, {file = "winrt_windows_storage_streams-3.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:da71231d4a554f9f15f1249b4990c6431176f6dfb0e3385c7caa7896f4ca24d6"}, {file = "winrt_windows_storage_streams-3.2.1-cp311-cp311-win32.whl", hash = "sha256:7dace2f9e364422255d0e2f335f741bfe7abb1f4d4f6003622b2450b87c91e69"}, {file = "winrt_windows_storage_streams-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:b02fa251a7eef6081eca1a5f64ecf349cfd1ac0ac0c5a5a30be52897d060bed5"}, {file = "winrt_windows_storage_streams-3.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:efdf250140340a75647e8e8ad002782d91308e9fdd1e19470a5b9cc969ae4780"}, {file = "winrt_windows_storage_streams-3.2.1-cp312-cp312-win32.whl", hash = "sha256:77c1f0e004b84347b5bd705e8f0fc63be8cd29a6093be13f1d0869d0d97b7d78"}, {file = "winrt_windows_storage_streams-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4508ee135af53e4fc142876abbf4bc7c2a95edfc7d19f52b291a8499cacd6dc"}, {file = "winrt_windows_storage_streams-3.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:040cb94e6fb26b0d00a00e8b88b06fadf29dfe18cf24ed6cb3e69709c3613307"}, {file = "winrt_windows_storage_streams-3.2.1-cp313-cp313-win32.whl", hash = "sha256:401bb44371720dc43bd1e78662615a2124372e7d5d9d65dfa8f77877bbcb8163"}, {file = "winrt_windows_storage_streams-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:202c5875606398b8bfaa2a290831458bb55f2196a39c1d4e5fa88a03d65ef915"}, {file = "winrt_windows_storage_streams-3.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ca3c5ec0aab60895006bf61053a1aca6418bc7f9a27a34791ba3443b789d230d"}, {file = "winrt_windows_storage_streams-3.2.1-cp314-cp314-win32.whl", hash = "sha256:5cd0dbad86fcc860366f6515fce97177b7eaa7069da261057be4813819ba37ee"}, {file = "winrt_windows_storage_streams-3.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3c5bf41d725369b9986e6d64bad7079372b95c329897d684f955d7028c7f27a0"}, {file = "winrt_windows_storage_streams-3.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:293e09825559d0929bbe5de01e1e115f7a6283d8996ab55652e5af365f032987"}, {file = "winrt_windows_storage_streams-3.2.1-cp39-cp39-win32.whl", hash = "sha256:1c630cfdece58fcf82e4ed86c826326123529836d6d4d855ae8e9ceeff67b627"}, {file = "winrt_windows_storage_streams-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7ff22434a4829d616a04b068a191ac79e008f6c27541bb178c1f6f1fe7a1657"}, {file = "winrt_windows_storage_streams-3.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:fa90244191108f85f6f7afb43a11d365aca4e0722fe8adc62fb4d2c678d0993d"}, {file = "winrt_windows_storage_streams-3.2.1.tar.gz", hash = "sha256:476f522722751eb0b571bc7802d85a82a3cae8b1cce66061e6e758f525e7b80f"}, ] [package.dependencies] winrt-runtime = ">=3.2.1.0,<3.3.0.0" [package.extras] all = ["winrt-Windows.Foundation.Collections[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Foundation[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.Storage[all] (>=3.2.1.0,<3.3.0.0)", "winrt-Windows.System[all] (>=3.2.1.0,<3.3.0.0)"] [extras] docs = ["Sphinx", "myst-parser", "sphinx-rtd-theme"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.15" content-hash = "901391e36f17a4c67f9bffef74104090cc67d54d7d6ccbbbf2323948e3f20477" Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/pyproject.toml000066400000000000000000000060011520473267500252020ustar00rootroot00000000000000[project] name = "bluetooth-auto-recovery" version = "1.6.4" description = "Recover bluetooth adapters that are in an stuck state" authors = [{ name = "J. Nick Koston", email = "nick@koston.org" }] license = "MIT" readme = "README.md" requires-python = ">=3.10" dynamic = ["classifiers", "dependencies"] [project.urls] "Repository" = "https://github.com/bluetooth-devices/bluetooth-auto-recovery" "Documentation" = "https://bluetooth-auto-recovery.readthedocs.io" "Bug Tracker" = "https://github.com/bluetooth-devices/bluetooth-auto-recovery/issues" "Changelog" = "https://github.com/bluetooth-devices/bluetooth-auto-recovery/blob/main/CHANGELOG.md" [tool.poetry] classifiers = [ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries", ] packages = [ { include = "bluetooth_auto_recovery", from = "src" }, ] [tool.poetry.dependencies] python = ">=3.10,<3.15" # Documentation Dependencies Sphinx = {version = ">=5,<8", optional = true} sphinx-rtd-theme = {version = ">=1,<4", optional = true} myst-parser = {version = ">=0.18,<3.1", optional = true} PyRIC = ">=0.1.6.3" btsocket = ">=0.2.0" async-timeout = {version = ">=3.0.0", python = "<3.11"} usb-devices = ">=0.4.1" bluetooth-adapters = ">=0.16.0" [tool.poetry.extras] docs = [ "myst-parser", "sphinx", "sphinx-rtd-theme", ] [tool.poetry.group.dev.dependencies] pytest = "^9.0" pytest-asyncio = ">=0.23.5" pytest-cov = "^7.1" [tool.semantic_release] branch = "main" version_toml = ["pyproject.toml:project.version"] version_variables = ["src/bluetooth_auto_recovery/__init__.py:__version__"] build_command = "pip install poetry && poetry build" [tool.pytest.ini_options] addopts = "-v -Wdefault --cov=bluetooth_auto_recovery --cov-report=term-missing:skip-covered" pythonpath = ["src"] [tool.coverage.run] branch = true [tool.coverage.report] exclude_lines = [ "pragma: no cover", "@overload", "if TYPE_CHECKING", "raise NotImplementedError", ] [tool.ruff] target-version = "py310" line-length = 88 [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "UP", # pyupgrade ] # E501 (line-too-long) is left to the formatter; unsplittable strings # (e.g. log messages) are intentionally allowed to exceed the line length. ignore = ["E501"] [tool.ruff.lint.isort] known-first-party = ["bluetooth_auto_recovery", "tests"] [tool.mypy] check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_defs = true mypy_path = "src/" no_implicit_optional = true show_error_codes = true warn_unreachable = true warn_unused_ignores = true exclude = [ 'docs/.*', 'setup.py', ] [[tool.mypy.overrides]] module = "tests.*" allow_untyped_defs = true [[tool.mypy.overrides]] module = "docs.*" ignore_errors = true [build-system] requires = ["poetry-core>=2.1.0"] build-backend = "poetry.core.masonry.api" Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/renovate.json000066400000000000000000000001011520473267500247770ustar00rootroot00000000000000{ "extends": ["github>browniebroke/renovate-configs:python"] } Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/src/000077500000000000000000000000001520473267500230605ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/src/bluetooth_auto_recovery/000077500000000000000000000000001520473267500300335ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/src/bluetooth_auto_recovery/__init__.py000066400000000000000000000020571520473267500321500ustar00rootroot00000000000000from __future__ import annotations __version__ = "1.6.4" import asyncio import importlib import sys from types import ModuleType _MODULE_CACHE: dict[str, ModuleType] = {} async def recover_adapter(hci: int, mac: str, gone_silent: bool = False) -> bool: """Recover the Bluetooth adapter with the given HCI and MAC address. This function is a wrapper that late imports the `bluetooth_auto_recovery.recover` module and calls its `recover_adapter` function. """ recover_module_name = f"{__package__}.recover" if not (recover_module := _MODULE_CACHE.get(recover_module_name)): loop = asyncio.get_running_loop() recover_module = await loop.run_in_executor( None, importlib.import_module, recover_module_name ) _MODULE_CACHE[recover_module_name] = recover_module this_module = sys.modules[__package__] this_module.recover_adapter = recover_module.recover_adapter # type: ignore return await recover_module.recover_adapter(hci, mac, gone_silent) __all__ = ["recover_adapter"] Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/src/bluetooth_auto_recovery/py.typed000066400000000000000000000000001520473267500315200ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/src/bluetooth_auto_recovery/recover.py000066400000000000000000001044041520473267500320550ustar00rootroot00000000000000"""Automatic recovery for bluetooth adapters.""" from __future__ import annotations import array import asyncio import errno import logging import socket import struct from contextlib import asynccontextmanager, suppress from dataclasses import dataclass from enum import Enum, auto from functools import cached_property try: from fcntl import ioctl import pyric.utils.rfkill as rfkill except ImportError: ioctl = None # type: ignore rfkill = None from collections.abc import AsyncIterator from typing import Any, cast import pyric.net.wireless.rfkill_h as rfkh from bluetooth_adapters import get_adapters_from_hci from btsocket import btmgmt_protocol, btmgmt_socket from btsocket.btmgmt_socket import AF_BLUETOOTH, BTPROTO_HCI from usb_devices import BluetoothDevice, NotAUSBDeviceError from .util import asyncio_timeout _LOGGER = logging.getLogger(__name__) POWER_OFF_TIME = 2 POWER_ON_TIME = 3 MAX_RFKILL_TIME = 3 DBUS_REGISTER_TIME = 3.5 # After an rfkill unblock the kernel clears the soft block asynchronously. # Poll for it instead of a single fixed wait, bounded by RFKILL_UNBLOCK_GRACE_TIME # of wall-clock time (>= the old DBUS_REGISTER_TIME grace) and re-checking every # RFKILL_UNBLOCK_POLL_INTERVAL seconds. RFKILL_UNBLOCK_GRACE_TIME = 4.5 RFKILL_UNBLOCK_POLL_INTERVAL = 1.5 # A USB reset disconnects the adapter and forces a re-enumeration, after which # it must also re-register with BlueZ. On slower systems (e.g. Raspberry Pi / # Home Assistant) this can take longer than a single DBUS_REGISTER_TIME wait, # so we poll for the adapter to reappear instead of giving up after one lookup. POST_RESET_LOOKUP_ATTEMPTS = 3 POST_RESET_LOOKUP_RETRY_TIME = 2 MGMT_PROTOCOL_TIMEOUT = 5 # https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/lib/hci.h HCIDEVUP = 0x400448C9 # 201 HCIDEVDOWN = 0x400448CA # 202 class USBResetOutcome(Enum): """Outcome of a USB reset attempt.""" SUCCEEDED = auto() # reset attempted and succeeded FAILED = auto() # reset attempted but failed NOT_APPLICABLE = auto() # adapter is not a USB device @dataclass class RFKillInfo: """RFKill info.""" soft_block: bool | None hard_block: bool | None idx: int | None def rfkill_unblock(adapter: MGMTBluetoothCtl, rfkill_idx: int) -> bool: """Try to remove an rfkill soft block.""" try: with open(rfkill.dpath, "wb") as fout: fout.write( rfkh.rfkill_event( rfkill_idx, rfkh.RFKILL_TYPE_ALL, rfkh.RFKILL_OP_CHANGE, 0, 0 ) ) except Exception: # pylint: disable=broad-except _LOGGER.exception( "RF kill switch unblock of %s (rfkill_idx:%s) failed", adapter.name, rfkill_idx, ) return False return True def rfkill_list_bluetooth(adapter: MGMTBluetoothCtl) -> RFKillInfo: """Execute the rfkill list bluetooth command.""" try: rfkill_dict = rfkill.rfkill_list() except FileNotFoundError as ex: _LOGGER.debug( "rfkill at /dev/rfkill is not accessible, cannot check bluetooth adapter %s: %s", adapter.name, ex, ) return RFKillInfo(None, None, None) except IndexError as ex: _LOGGER.debug( "rfkill at /dev/rfkill returned unexpected results, cannot check bluetooth adapter %s: %s", adapter.name, ex, ) return RFKillInfo(None, None, None) except PermissionError as ex: _LOGGER.debug( "Access to rfkill at /dev/rfkill is not permitted, cannot check bluetooth adapter %s: %s", adapter.name, ex, ) return RFKillInfo(None, None, None) except UnicodeDecodeError as ex: _LOGGER.debug( "RF kill switch check failed - data for %s is not UTF-8 encoded: %s", adapter.name, ex, ) return RFKillInfo(None, None, None) except Exception: # pylint: disable=broad-except _LOGGER.exception("RF kill switch check failed") return RFKillInfo(None, None, None) try: rfkill_hci_state = rfkill_dict[adapter.hci_name] except KeyError: _LOGGER.debug( "RF kill switch check failed - no data for %s. Available data: %s", adapter.name, rfkill_dict, ) return RFKillInfo(None, None, None) return RFKillInfo( rfkill_hci_state["soft"], rfkill_hci_state["hard"], rfkill_hci_state["idx"] ) class BluetoothMGMTProtocol(asyncio.Protocol): """Bluetooth MGMT protocol.""" def __init__( self, timeout: float, connection_mode_future: asyncio.Future[None], sock: socket.socket, ) -> None: """Initialize the protocol.""" self.future: asyncio.Future[btmgmt_protocol.Response] | None = None self.transport: asyncio.Transport | None = None self.timeout = timeout self.connection_mode_future = connection_mode_future self.loop = asyncio.get_running_loop() self.sock = sock def connection_made(self, transport: asyncio.BaseTransport) -> None: """Handle connection made.""" if not self.connection_mode_future.done(): self.connection_mode_future.set_result(None) self.transport = cast(asyncio.Transport, transport) def data_received(self, data: bytes) -> None: """Handle data received.""" try: if ( self.future and not self.future.done() and (response := btmgmt_protocol.reader(data)) and response.cmd_response_frame ): self.future.set_result(response) except ValueError as ex: # ValueError: 47 is not a valid Events may happen on newer kernels # and we need to ignore these events _LOGGER.debug("Error parsing response: %s", ex) async def send(self, *args: Any) -> btmgmt_protocol.Response: """Send command.""" pkt_objs = btmgmt_protocol.command(*args) self.future = self.loop.create_future() if self.transport is None: raise btmgmt_socket.BluetoothSocketError("Connection was closed") # Write directly to the socket to work around kernel ABI inconsistency # where sendto() may return 0 on certain systems (e.g., Odroid M1 with kernel 6.12.43-haos) # even though data was successfully sent. Using transport.write() can cause # infinite retries in asyncio's transport layer. # See: https://github.com/Bluetooth-Devices/habluetooth/pull/303 # See: https://github.com/home-assistant/core/issues/152204 data = b"".join(frame.octets for frame in pkt_objs if frame) self.sock.send(data) cancel_timeout = self.loop.call_later( self.timeout, self._timeout_future, self.future ) try: return await self.future finally: cancel_timeout.cancel() self.future = None def _timeout_future(self, future: asyncio.Future[btmgmt_protocol.Response]) -> None: if future and not future.done(): future.set_exception(asyncio.TimeoutError("Timeout waiting for response")) def connection_lost(self, exc: Exception | None) -> None: """Handle connection lost.""" if exc: _LOGGER.warning("Bluetooth management socket connection lost: %s", exc) self.transport = None class MGMTBluetoothCtl: """Class to control interfaces using the BlueZ management API""" def __init__(self, hci_name: str, mac: str, timeout: float) -> None: """Initialize the control class.""" # These get set when we enumerate the controllers self.idx: int | None = None self.hci_name: str | None = None self.mac: str | None = None # This is what we expect to find self._expected_bdaddr = mac.upper() self._expected_hci_name = hci_name # Internal state self.timeout = timeout self.protocol: BluetoothMGMTProtocol | None = None self.presented_list: dict[int, str] = {} self.sock: socket.socket | None = None @cached_property def name(self) -> str: """Return the name of the adapter.""" return f"{self.hci_name} [{self.mac}] ({self.idx})" async def close(self) -> None: """Close the management interface.""" if self.protocol and self.protocol.transport: self.protocol.transport.close() self.protocol = None btmgmt_socket.close(self.sock) async def setup(self) -> None: """Set up management interface.""" sock = btmgmt_socket.open() self.sock = sock loop = asyncio.get_running_loop() connection_made_future: asyncio.Future[None] = loop.create_future() try: async with asyncio_timeout(5): # _create_connection_transport accessed directly to avoid SOCK_STREAM check # see https://bugs.python.org/issue38285 _, protocol = await loop._create_connection_transport( # type: ignore[attr-defined] sock, lambda: BluetoothMGMTProtocol( self.timeout, connection_made_future, sock ), None, None, ) await connection_made_future except asyncio.TimeoutError: btmgmt_socket.close(sock) raise assert isinstance(protocol, BluetoothMGMTProtocol) # nosec self.protocol = protocol await self._find_controller() async def _find_controller(self) -> None: """Find the controller.""" assert self.protocol is not None # nosec loop = asyncio.get_running_loop() # Try to get the adapter index from the hci device first # since it can see downed adapters. if adapters_from_hci := await loop.run_in_executor(None, get_adapters_from_hci): _LOGGER.debug("Found adapters from hci: %s", adapters_from_hci) for adapter in adapters_from_hci.values(): if adapter["bdaddr"] == self._expected_bdaddr: self.idx = adapter["dev_id"] self.hci_name = adapter["name"] self.mac = adapter["bdaddr"] _LOGGER.debug( "Found adapter %s by mac in hci device as %s", self.mac, self.idx, ) return for adapter in adapters_from_hci.values(): if adapter["name"] == self._expected_hci_name: self.idx = adapter["dev_id"] self.hci_name = adapter["name"] self.mac = adapter["bdaddr"] _LOGGER.debug( "Found adapter %s by name as hci device %s as %s", self.mac, self._expected_hci_name, self.idx, ) return idxdata = await self.protocol.send("ReadControllerIndexList", None) if idxdata.event_frame.status.value != 0x00: # 0x00 - Success _LOGGER.error( "Unable to get hci controllers index list! Event frame status: %s", idxdata.event_frame.status, ) return if idxdata.cmd_response_frame.num_controllers == 0: _LOGGER.warning("There are no BT controllers present in the system!") return hci_idx_list = getattr(idxdata.cmd_response_frame, "controller_index[i]") _LOGGER.debug("hci_idx_list: %s", hci_idx_list) for idx in hci_idx_list: hci_info = await self.protocol.send("ReadControllerInformation", idx) _LOGGER.debug("controller idx %s: %s", idx, hci_info) response = hci_info.cmd_response_frame mac: str = response.address.upper() self.presented_list[idx] = mac if self._expected_bdaddr == mac: _LOGGER.debug( "Found adapter %s by mac by reading controller info %s", mac, idx ) self.idx = idx self.hci_name = f"hci{idx}" self.mac = mac return expected_hci = hci_name_to_number(self._expected_hci_name) if maybe_mac := self.presented_list.get(expected_hci): _LOGGER.warning( "The mac address %s was not found in the adapter list: %s, " "falling back to matching by %s", self._expected_bdaddr, self.presented_list, self._expected_hci_name, ) self.idx = expected_hci self.hci_name = self._expected_hci_name self.mac = maybe_mac async def get_powered(self) -> bool | None: """Powered state of the interface.""" assert self.protocol is not None # nosec if self.idx is not None: response = await self.protocol.send("ReadControllerInformation", self.idx) return response.cmd_response_frame.current_settings.get( btmgmt_protocol.SupportedSettings.Powered ) return None async def set_powered(self, new_state: bool) -> bool: """Set the powered state of the interface.""" assert self.protocol is not None # nosec response = await self.protocol.send( "SetPowered", self.idx, int(new_state is True) ) if response.event_frame.status.value == 0x00: # 0x00 - Success return True return False async def wait_for_power_state( self, new_state: bool, timeout: float ) -> bool | None: """Wait for the adapter to be powered on or off.""" assert self.protocol is not None # nosec current_state: bool | None = not new_state try: async with asyncio_timeout(timeout): while True: current_state = await self.get_powered() if current_state == new_state: return current_state await asyncio.sleep(0.1) except asyncio.TimeoutError: return current_state async def _check_rfkill(adapter: MGMTBluetoothCtl) -> RFKillInfo: """Check if rfkill is blocked.""" loop = asyncio.get_running_loop() try: async with asyncio_timeout(MAX_RFKILL_TIME): return await loop.run_in_executor(None, rfkill_list_bluetooth, adapter) except asyncio.TimeoutError: _LOGGER.warning( "Checking rfkill for %s timed out after %s seconds!", adapter.name, MAX_RFKILL_TIME, ) return RFKillInfo(None, None, None) async def _unblock_rfkill(adapter: MGMTBluetoothCtl, rfkill_idx: int) -> bool: """Try to unblock an adapter.""" loop = asyncio.get_running_loop() try: async with asyncio_timeout(MAX_RFKILL_TIME): return await loop.run_in_executor(None, rfkill_unblock, adapter, rfkill_idx) except asyncio.TimeoutError: _LOGGER.warning( "Unblocking rfkill for %s with idx:%s timed out after %s seconds!", adapter.name, rfkill_idx, MAX_RFKILL_TIME, ) return False async def _check_or_unblock_rfkill(adapter: MGMTBluetoothCtl) -> bool: """Check if rfkill is blocked, and try to unblock if possible. Returns False if the adapter is blocked or the state could not be determined. """ rfkill_info = await _check_rfkill(adapter) if rfkill_info.idx is None: _LOGGER.debug( "Could not determine rfkill_idx of %s: %s", adapter.name, rfkill_info ) return True _LOGGER.debug("rfkill_idx of %s is %s", adapter.name, rfkill_info.idx) if rfkill_info.hard_block: _LOGGER.warning( "Bluetooth adapter %s is hard blocked by rfkill; hardware reboot required: %s", adapter.name, rfkill_info, ) return False if not rfkill_info.soft_block: _LOGGER.debug( "Bluetooth adapter %s is NOT soft blocked by rfkill: %s", adapter.name, rfkill_info, ) return True _LOGGER.debug( "Bluetooth adapter %s is soft blocked by rfkill; trying to unblock", adapter.name, ) await _unblock_rfkill(adapter, rfkill_info.idx) # The kernel does not clear the rfkill block synchronously. A single fixed # wait is both wasteful when the block clears quickly and too short on slow # systems (Pi/HA), where the adapter is still reported blocked after the # wait and is then falsely declared "could not be unblocked". Poll instead: # return as soon as the block clears, re-checking every # RFKILL_UNBLOCK_POLL_INTERVAL seconds, with the whole poll bounded by # RFKILL_UNBLOCK_GRACE_TIME of wall-clock time so the worst case stays # capped regardless of how long each re-check takes. # # This is NOT a busy wait: each iteration sleeps for # RFKILL_UNBLOCK_POLL_INTERVAL seconds with `await asyncio.sleep(...)`, and # `_check_rfkill` runs in an executor under its own timeout, so the event # loop is yielded for the entire duration of the poll. with suppress(asyncio.TimeoutError): async with asyncio_timeout(RFKILL_UNBLOCK_GRACE_TIME): while True: rfkill_info = await _check_rfkill(adapter) # Require an explicit unblocked reading. A timed-out check # returns RFKillInfo(None, None, None); `not None` is truthy, so # treating None as "unblocked" would falsely report success on an # unknown state. Keep polling until both blocks are explicitly # False (or the grace expires and we report failure below). if rfkill_info.soft_block is False and rfkill_info.hard_block is False: _LOGGER.debug( "Bluetooth adapter %s was successfully unblocked", adapter.name, ) return True _LOGGER.debug( "Waiting %ss for kernel to catch up after rfkill unblock of %s", RFKILL_UNBLOCK_POLL_INTERVAL, adapter.name, ) await asyncio.sleep(RFKILL_UNBLOCK_POLL_INTERVAL) _LOGGER.warning( "Bluetooth adapter %s is blocked by rfkill and could not be unblocked", adapter.name, ) return False async def recover_adapter(hci: int, mac: str, gone_silent: bool = False) -> bool: """Reset the bluetooth adapter.""" mac = mac.upper() hci_name = f"hci{hci}" _LOGGER.debug( "Attempting to recover bluetooth adapter %s with mac address %s (gone_silent=%s)", hci_name, mac, gone_silent, ) async with _get_adapter(hci_name, mac) as adapter: if ( not adapter or adapter.idx is None or adapter.hci_name is None or adapter.mac is None ): _LOGGER.warning( "Could not find adapter with mac address %s or %s", mac, hci_name ) return False if adapter.hci_name != hci_name: hci_name = adapter.hci_name _LOGGER.warning( "Adapter with mac address %s has moved to %s", mac, hci_name ) if adapter.mac != mac: mac = adapter.mac _LOGGER.warning( "Adapter with name %s mac address resolved to %s", hci_name, mac ) if not await _check_or_unblock_rfkill(adapter): _LOGGER.warning( "rfkill has blocked %s, and could not be unblocked", adapter.name ) power_cycle_ok = await _power_cycle_adapter(adapter) # If the adapter has not gone silent, a successful power cycle is enough. if power_cycle_ok and not gone_silent: # Give Dbus some time to catch up _LOGGER.debug( "Waiting %ss for kernel and Dbus to catch up after successful power cycle", DBUS_REGISTER_TIME, ) await asyncio.sleep(DBUS_REGISTER_TIME) return True # The adapter has gone silent (or the power cycle failed), so escalate to # a USB reset. This may also move the adapter to a new hci number. usb_reset = await _usb_reset_adapter(adapter) if usb_reset is USBResetOutcome.NOT_APPLICABLE: # A USB reset is not applicable because the adapter is not a USB # device. A non-USB adapter (e.g. a built-in UART controller) can # only be recovered by the power cycle, so fall back to its result # rather than reporting a spurious failure. if not power_cycle_ok: return False _LOGGER.debug( "Adapter %s is not a USB device; relying on the successful " "power cycle for recovery", adapter.name, ) await asyncio.sleep(DBUS_REGISTER_TIME) return True if usb_reset is USBResetOutcome.FAILED: return False # Give Dbus some time to catch up in case # the adapter is going to move to a new hci number. _LOGGER.debug( "Waiting %ss for kernel and Dbus to catch up after successful USB reset", DBUS_REGISTER_TIME, ) await asyncio.sleep(DBUS_REGISTER_TIME) # We just did a USB reset which causes the adapter to disconnect and # re-enumerate (and possibly move to a different hci number). On slower # systems the re-enumeration plus BlueZ re-registration can take longer # than the single DBUS_REGISTER_TIME wait above, so poll for the adapter # to reappear instead of reporting failure after a single lookup. for attempt in range(1, POST_RESET_LOOKUP_ATTEMPTS + 1): async with _get_adapter(hci_name, mac) as adapter: if adapter and adapter.idx is not None and adapter.hci_name is not None: if adapter.hci_name != hci_name: hci_name = adapter.hci_name _LOGGER.warning( "Adapter with mac address %s has moved to %s", mac, hci_name ) # After the reset, rfkill may be blocked so we need # to check and unblock it. if not await _check_or_unblock_rfkill(adapter): _LOGGER.warning( "rfkill has blocked %s, and could not be unblocked", adapter.name, ) return False return True if attempt < POST_RESET_LOOKUP_ATTEMPTS: _LOGGER.debug( "Adapter with mac address %s (%s) has not reappeared after the " "USB reset yet (attempt %s/%s); waiting %ss before retrying", mac, hci_name, attempt, POST_RESET_LOOKUP_ATTEMPTS, POST_RESET_LOOKUP_RETRY_TIME, ) await asyncio.sleep(POST_RESET_LOOKUP_RETRY_TIME) _LOGGER.warning( "Could not find adapter with mac address %s or %s after USB reset", mac, hci_name, ) return False @asynccontextmanager async def _get_adapter( hci_name: str, mac: str ) -> AsyncIterator[MGMTBluetoothCtl | None]: """Get the adapter.""" name = f"{hci_name} [{mac}]" _LOGGER.debug("Attempting to power cycle bluetooth adapter %s", name) adapter = None try: adapter = MGMTBluetoothCtl(hci_name, mac, MGMT_PROTOCOL_TIMEOUT) await adapter.setup() _LOGGER.debug( "_get_adapter: %s (hci_name=%s) (mac=%s) (idx=%s)", name, adapter.hci_name, adapter.mac, adapter.idx, ) if adapter.idx is not None: yield adapter else: yield None except btmgmt_socket.BluetoothSocketError as ex: _LOGGER.warning( "Getting Bluetooth adapter failed %s " "because the system cannot create a bluetooth socket: %s", name, ex, ) yield None except asyncio.TimeoutError: # On Python 3.11+ asyncio.TimeoutError is an alias of the builtin # TimeoutError, which subclasses OSError, so this must precede the # OSError handler or the timeout-specific message is never emitted. _LOGGER.warning("Getting Bluetooth adapter %s failed due to timeout", name) yield None except OSError as ex: _LOGGER.warning("Getting Bluetooth adapter %s failed: %s", name, ex) yield None finally: if adapter: try: await adapter.close() except Exception as ex: # pylint: disable=broad-except _LOGGER.warning("Closing Bluetooth adapter %s failed: %s", name, ex) async def _power_cycle_adapter(adapter: MGMTBluetoothCtl) -> bool: _LOGGER.debug("Attempting to power cycle bluetooth adapter %s", adapter.name) try: return await _execute_reset(adapter) except btmgmt_socket.BluetoothSocketError as ex: _LOGGER.warning( "Bluetooth adapter %s could not be reset " "because the system cannot create a bluetooth socket: %s", adapter.name, ex, ) return False except asyncio.TimeoutError: # On Python 3.11+ asyncio.TimeoutError is an alias of the builtin # TimeoutError, which subclasses OSError, so this must precede the # OSError handler or the timeout-specific message is never emitted. _LOGGER.warning( "Bluetooth adapter %s could not be reset due to timeout after %s seconds", adapter.name, adapter.timeout, ) return False except OSError as ex: _LOGGER.warning("Bluetooth adapter %s could not be reset: %s", adapter.name, ex) return False def hci_name_to_number(hci_name: str) -> int: """Convert hci name to number.""" return int(hci_name.removeprefix("hci")) async def _usb_reset_adapter(adapter: MGMTBluetoothCtl) -> USBResetOutcome: """Reset the bluetooth adapter via USB. Returns an outcome describing whether a USB reset was applicable (the adapter is a USB device) and, if so, whether it succeeded. A non-USB adapter (e.g. a built-in UART controller) yields a not-applicable outcome. """ assert adapter.hci_name is not None # nosec hci = hci_name_to_number(adapter.hci_name) _LOGGER.debug("Executing USB reset for Bluetooth adapter hci%i", hci) dev = BluetoothDevice(hci) try: return ( USBResetOutcome.SUCCEEDED if await dev.async_reset() else USBResetOutcome.FAILED ) except NotAUSBDeviceError as ex: _LOGGER.debug( "hci%s is not a USB device while attempting USB reset: %s", hci, ex ) return USBResetOutcome.NOT_APPLICABLE except FileNotFoundError as ex: _LOGGER.debug("hci%s not found while attempting USB reset: %s", hci, ex) return USBResetOutcome.NOT_APPLICABLE except PermissionError as ex: _LOGGER.info( "hci%s permission denied to %s while attempting USB reset: %s", hci, ex.filename, ex, ) return USBResetOutcome.FAILED except Exception as ex: # pylint: disable=broad-except _LOGGER.exception( "Unexpected error while attempting USB reset of hci%s: %s", hci, ex ) return USBResetOutcome.FAILED async def _set_adapter_up_down( adapter: MGMTBluetoothCtl, sock: socket.socket, loop: asyncio.AbstractEventLoop, code: int, state: str, ) -> None: """Set the adapter up or down.""" req_str = struct.pack("H", adapter.idx) request = array.array("b", req_str) _LOGGER.debug("Setting hci%i %s", adapter.idx, state) await loop.run_in_executor(None, ioctl, sock.fileno(), code, request[0]) async def _bounce_adapter_interface( adapter: MGMTBluetoothCtl, *, up: bool, down: bool ) -> None: """Bounce the adapter ex. hciconfig down/up.""" loop = asyncio.get_running_loop() assert adapter.idx is not None, "Adapter must have an idx" # nosec sock = await loop.run_in_executor(None, raw_open, adapter.idx) try: _LOGGER.debug("Bouncing Bluetooth adapter hci%i", adapter.idx) if down: await _set_adapter_up_down(adapter, sock, loop, HCIDEVDOWN, "down") await asyncio.sleep(0.5) if up: await _set_adapter_up_down(adapter, sock, loop, HCIDEVUP, "up") await asyncio.sleep(0.5) _LOGGER.debug("Finished bouncing hci%i", adapter.idx) finally: await loop.run_in_executor(None, raw_close, sock) async def _execute_reset(adapter: MGMTBluetoothCtl) -> bool: """Execute the reset.""" timed_out_getting_powered: bool = False power_state_before_reset: bool | None = None try: power_state_before_reset = await adapter.get_powered() except AttributeError as ex: _LOGGER.warning( "Could not determine the power state of the Bluetooth adapter %s: %s", adapter.name, ex, ) except asyncio.TimeoutError: _LOGGER.warning( "Could not determine the power state of the Bluetooth adapter %s due to timeout after %s seconds", adapter.name, adapter.timeout, ) timed_out_getting_powered = True except Exception: # pylint: disable=broad-except # _LOGGER.exception already records the traceback, so no extra %s is needed. _LOGGER.exception( "Could not determine the power state of the Bluetooth adapter %s", adapter.name, ) # Do not attempt to power off if it timed out getting the power state # as it likely means the adapter interface is frozen and will not respond to # power off commands so we need to proceed to bounce the interface if not timed_out_getting_powered: try: await _execute_power_off(adapter, power_state_before_reset) except asyncio.TimeoutError: _LOGGER.warning( "Could not reset the power state of the Bluetooth adapter %s due to timeout after %s seconds", adapter.name, adapter.timeout, ) except Exception: _LOGGER.exception( "Could not reset the power state of the Bluetooth adapter %s", adapter.name, ) try: await _bounce_adapter_interface(adapter, down=True, up=True) except Exception as ex: # pylint: disable=broad-except _LOGGER.warning( "Could not cycle the Bluetooth adapter %s: %s", adapter.name, ex ) try: power_on_ok = await _execute_power_on(adapter, power_state_before_reset) except asyncio.TimeoutError: _LOGGER.warning( "Could not reset the power state of the Bluetooth adapter %s due to timeout after %s seconds", adapter.name, adapter.timeout, ) return False except Exception: _LOGGER.exception( "Could not reset the power state of the Bluetooth adapter %s", adapter.name ) return False if not power_on_ok: return False try: await _bounce_adapter_interface(adapter, down=False, up=True) except OSError as ex: if ex.errno == errno.EALREADY: _LOGGER.debug("Adapter %s is already up", adapter.name) return True _LOGGER.warning( "Could not bring up the Bluetooth adapter %s: %s", adapter.name, ex ) return False except Exception as ex: # pylint: disable=broad-except _LOGGER.warning( "Could not bring up the Bluetooth adapter %s: %s", adapter.name, ex ) return False return True async def _execute_power_on( adapter: MGMTBluetoothCtl, power_state_before_reset: bool | None ) -> bool: """Execute the power off.""" try: await adapter.set_powered(True) except AttributeError as ex: _LOGGER.warning( "Could not re-enable power after cycle of the Bluetooth adapter %s: %s", adapter.name, ex, ) return False pstate_after = await adapter.wait_for_power_state(True, POWER_ON_TIME) # Check the state after the reset if pstate_after is True: if power_state_before_reset is False: _LOGGER.debug( "Bluetooth adapter %s successfully turned back ON", adapter.name ) else: _LOGGER.debug( "Power state of bluetooth adapter %s is ON after power cycle", adapter.name, ) return True if pstate_after is False: _LOGGER.warning( "Power state of bluetooth adapter %s is OFF after power cycle", adapter.name ) return False _LOGGER.debug( "Power state of bluetooth adapter %s could not be determined after power cycle", adapter.name, ) return False async def _execute_power_off( adapter: MGMTBluetoothCtl, power_state_before_reset: bool | None ) -> bool: """Execute the power off.""" if power_state_before_reset is True: _LOGGER.debug("Current power state of bluetooth adapter is ON.") try: await adapter.set_powered(False) except AttributeError as ex: _LOGGER.warning( "Could not power cycle the Bluetooth adapter %s: %s", adapter.name, ex ) return False await adapter.wait_for_power_state(False, POWER_OFF_TIME) elif power_state_before_reset is False: _LOGGER.debug( "Current power state of bluetooth adapter %s is OFF, trying to turn it back ON", adapter.name, ) else: _LOGGER.debug("Power state of bluetooth adapter could not be determined") return False return True def raw_open(adapter_idx: int) -> socket.socket: """Create a bluetooth socket for a specific adapter.""" sock = socket.socket(AF_BLUETOOTH, socket.SOCK_RAW, BTPROTO_HCI) sock.bind((adapter_idx,)) return sock def raw_close(bt_socket: socket.socket) -> None: """Close the bluetooth socket.""" fd = bt_socket.detach() socket.close(fd) Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/src/bluetooth_auto_recovery/util.py000066400000000000000000000003411520473267500313600ustar00rootroot00000000000000from __future__ import annotations import sys if sys.version_info[:2] < (3, 11): from async_timeout import timeout as asyncio_timeout # noqa: F401 else: from asyncio import timeout as asyncio_timeout # noqa: F401 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/tests/000077500000000000000000000000001520473267500234335ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/tests/__init__.py000066400000000000000000000000001520473267500255320ustar00rootroot00000000000000Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/tests/conftest.py000066400000000000000000000017121520473267500256330ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncIterator from contextlib import asynccontextmanager from unittest.mock import AsyncMock, MagicMock import pytest from bluetooth_auto_recovery.recover import MGMTBluetoothCtl @pytest.fixture def adapter() -> MGMTBluetoothCtl: """A fully-resolved adapter with an AsyncMock protocol.""" ctl = MGMTBluetoothCtl("hci0", "AA:BB:CC:DD:EE:FF", 5) ctl.idx = 0 ctl.hci_name = "hci0" ctl.mac = "AA:BB:CC:DD:EE:FF" ctl.protocol = AsyncMock() return ctl def make_send_response(*, status: int = 0x00) -> MagicMock: """Build a fake btmgmt protocol response.""" response = MagicMock() response.event_frame.status.value = status return response @asynccontextmanager async def adapter_cm( value: MGMTBluetoothCtl | None, ) -> AsyncIterator[MGMTBluetoothCtl | None]: """Yield a value as an async context manager (stand-in for _get_adapter).""" yield value Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/tests/test_init.py000066400000000000000000000013371520473267500260130ustar00rootroot00000000000000from unittest.mock import patch import pytest import bluetooth_auto_recovery from bluetooth_auto_recovery import recover def test_init(): """Test the init function.""" assert bluetooth_auto_recovery @pytest.mark.asyncio async def test_recover_adapter(): """Test the recover_adapter function.""" assert bluetooth_auto_recovery.recover_adapter is not recover.recover_adapter with patch("bluetooth_auto_recovery.recover.recover_adapter") as recover_adapter: await bluetooth_auto_recovery.recover_adapter(0, "00:00:00:00:00:00") assert recover_adapter.called assert bluetooth_auto_recovery.recover_adapter is recover_adapter bluetooth_auto_recovery.recover_adapter = recover.recover_adapter Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/tests/test_recover.py000066400000000000000000001434151520473267500265210ustar00rootroot00000000000000"""Tests for the recover.py state machine.""" from __future__ import annotations import asyncio import errno import logging from typing import cast from unittest.mock import AsyncMock, MagicMock, call, patch import pytest from bluetooth_auto_recovery import recover from bluetooth_auto_recovery.recover import ( BluetoothMGMTProtocol, MGMTBluetoothCtl, RFKillInfo, hci_name_to_number, raw_close, raw_open, rfkill_list_bluetooth, rfkill_unblock, ) from .conftest import adapter_cm, make_send_response # --------------------------------------------------------------------------- # Pure helpers # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("name", "expected"), [("hci0", 0), ("hci1", 1), ("hci15", 15), ("hci255", 255)], ) def test_hci_name_to_number(name: str, expected: int) -> None: assert hci_name_to_number(name) == expected def test_rfkill_info_dataclass() -> None: info = RFKillInfo(soft_block=True, hard_block=False, idx=3) assert info.soft_block is True assert info.hard_block is False assert info.idx == 3 # --------------------------------------------------------------------------- # rfkill_list_bluetooth # --------------------------------------------------------------------------- def test_rfkill_list_bluetooth_success(adapter: MGMTBluetoothCtl) -> None: rfkill_mod = MagicMock() rfkill_mod.rfkill_list.return_value = { "hci0": {"soft": True, "hard": False, "idx": 7} } with patch.object(recover, "rfkill", rfkill_mod): info = rfkill_list_bluetooth(adapter) assert info == RFKillInfo(soft_block=True, hard_block=False, idx=7) @pytest.mark.parametrize( "exc", [ FileNotFoundError(), IndexError(), PermissionError(), UnicodeDecodeError("utf-8", b"", 0, 1, "bad"), RuntimeError("boom"), ], ) def test_rfkill_list_bluetooth_handles_errors( adapter: MGMTBluetoothCtl, exc: Exception ) -> None: rfkill_mod = MagicMock() rfkill_mod.rfkill_list.side_effect = exc with patch.object(recover, "rfkill", rfkill_mod): info = rfkill_list_bluetooth(adapter) assert info == RFKillInfo(None, None, None) def test_rfkill_list_bluetooth_adapter_not_in_results( adapter: MGMTBluetoothCtl, ) -> None: rfkill_mod = MagicMock() rfkill_mod.rfkill_list.return_value = { "hci9": {"soft": False, "hard": False, "idx": 1} } with patch.object(recover, "rfkill", rfkill_mod): info = rfkill_list_bluetooth(adapter) assert info == RFKillInfo(None, None, None) # --------------------------------------------------------------------------- # rfkill_unblock # --------------------------------------------------------------------------- def test_rfkill_unblock_success(adapter: MGMTBluetoothCtl) -> None: rfkill_mod = MagicMock() rfkill_mod.dpath = "/dev/rfkill" rfkh_mod = MagicMock() rfkh_mod.rfkill_event.return_value = b"event" with ( patch.object(recover, "rfkill", rfkill_mod), patch.object(recover, "rfkh", rfkh_mod), patch("builtins.open", MagicMock()), ): assert rfkill_unblock(adapter, 7) is True def test_rfkill_unblock_failure(adapter: MGMTBluetoothCtl) -> None: rfkill_mod = MagicMock() rfkill_mod.dpath = "/dev/rfkill" with ( patch.object(recover, "rfkill", rfkill_mod), patch("builtins.open", side_effect=OSError("nope")), ): assert rfkill_unblock(adapter, 7) is False # --------------------------------------------------------------------------- # MGMTBluetoothCtl basics # --------------------------------------------------------------------------- def test_name_property(adapter: MGMTBluetoothCtl) -> None: assert adapter.name == "hci0 [AA:BB:CC:DD:EE:FF] (0)" @pytest.mark.asyncio async def test_close_closes_transport_and_socket() -> None: ctl = MGMTBluetoothCtl("hci0", "AA:BB:CC:DD:EE:FF", 5) transport = MagicMock() protocol = MagicMock() protocol.transport = transport ctl.protocol = cast("BluetoothMGMTProtocol | None", protocol) ctl.sock = MagicMock() with patch.object(recover.btmgmt_socket, "close") as mock_close: await ctl.close() transport.close.assert_called_once() assert ctl.protocol is None mock_close.assert_called_once_with(ctl.sock) @pytest.mark.asyncio async def test_close_no_protocol() -> None: ctl = MGMTBluetoothCtl("hci0", "AA:BB:CC:DD:EE:FF", 5) ctl.protocol = None ctl.sock = MagicMock() with patch.object(recover.btmgmt_socket, "close") as mock_close: await ctl.close() mock_close.assert_called_once_with(ctl.sock) @pytest.mark.asyncio async def test_get_powered(adapter: MGMTBluetoothCtl) -> None: response = MagicMock() response.cmd_response_frame.current_settings.get.return_value = True cast(AsyncMock, adapter.protocol).send.return_value = response assert await adapter.get_powered() is True @pytest.mark.asyncio async def test_get_powered_no_idx(adapter: MGMTBluetoothCtl) -> None: adapter.idx = None assert await adapter.get_powered() is None @pytest.mark.asyncio async def test_set_powered_success(adapter: MGMTBluetoothCtl) -> None: cast(AsyncMock, adapter.protocol).send.return_value = make_send_response( status=0x00 ) assert await adapter.set_powered(True) is True @pytest.mark.asyncio async def test_set_powered_failure(adapter: MGMTBluetoothCtl) -> None: cast(AsyncMock, adapter.protocol).send.return_value = make_send_response( status=0x01 ) assert await adapter.set_powered(True) is False @pytest.mark.asyncio async def test_wait_for_power_state_reaches_target(adapter: MGMTBluetoothCtl) -> None: with patch.object(adapter, "get_powered", AsyncMock(return_value=True)): assert await adapter.wait_for_power_state(True, 1) is True @pytest.mark.asyncio async def test_wait_for_power_state_times_out(adapter: MGMTBluetoothCtl) -> None: with patch.object(adapter, "get_powered", AsyncMock(return_value=False)): # Never reaches True -> returns last observed state on timeout. assert await adapter.wait_for_power_state(True, 0.05) is False # --------------------------------------------------------------------------- # MGMTBluetoothCtl._find_controller # --------------------------------------------------------------------------- def _ctl() -> MGMTBluetoothCtl: ctl = MGMTBluetoothCtl("hci0", "AA:BB:CC:DD:EE:FF", 5) ctl.protocol = AsyncMock() return ctl @pytest.mark.asyncio async def test_find_controller_match_by_mac_from_hci() -> None: ctl = _ctl() adapters = { "hci0": {"dev_id": 0, "name": "hci0", "bdaddr": "AA:BB:CC:DD:EE:FF"}, } with patch.object(recover, "get_adapters_from_hci", return_value=adapters): await ctl._find_controller() assert ctl.idx == 0 assert ctl.hci_name == "hci0" assert ctl.mac == "AA:BB:CC:DD:EE:FF" cast(AsyncMock, ctl.protocol).send.assert_not_called() @pytest.mark.asyncio async def test_find_controller_match_by_name_from_hci() -> None: ctl = _ctl() adapters = { "hci0": {"dev_id": 3, "name": "hci0", "bdaddr": "11:22:33:44:55:66"}, } with patch.object(recover, "get_adapters_from_hci", return_value=adapters): await ctl._find_controller() # MAC did not match, but the hci name did. assert ctl.idx == 3 assert ctl.hci_name == "hci0" assert ctl.mac == "11:22:33:44:55:66" @pytest.mark.asyncio async def test_find_controller_match_by_mac_via_controller_info() -> None: ctl = _ctl() idx_response = MagicMock() idx_response.event_frame.status.value = 0x00 idx_response.cmd_response_frame.num_controllers = 1 setattr(idx_response.cmd_response_frame, "controller_index[i]", [5]) info_response = MagicMock() info_response.cmd_response_frame.address = "aa:bb:cc:dd:ee:ff" cast(AsyncMock, ctl.protocol).send = AsyncMock( side_effect=[idx_response, info_response] ) with patch.object(recover, "get_adapters_from_hci", return_value={}): await ctl._find_controller() assert ctl.idx == 5 assert ctl.hci_name == "hci5" assert ctl.mac == "AA:BB:CC:DD:EE:FF" @pytest.mark.asyncio async def test_find_controller_fallback_by_hci_number() -> None: ctl = _ctl() idx_response = MagicMock() idx_response.event_frame.status.value = 0x00 idx_response.cmd_response_frame.num_controllers = 1 setattr(idx_response.cmd_response_frame, "controller_index[i]", [0]) info_response = MagicMock() # MAC differs from expected, so it falls back to matching by hci number 0. info_response.cmd_response_frame.address = "99:99:99:99:99:99" cast(AsyncMock, ctl.protocol).send = AsyncMock( side_effect=[idx_response, info_response] ) with patch.object(recover, "get_adapters_from_hci", return_value={}): await ctl._find_controller() assert ctl.idx == 0 assert ctl.hci_name == "hci0" assert ctl.mac == "99:99:99:99:99:99" @pytest.mark.asyncio async def test_find_controller_index_list_error_status() -> None: ctl = _ctl() idx_response = MagicMock() idx_response.event_frame.status.value = 0x01 # non-success cast(AsyncMock, ctl.protocol).send = AsyncMock(return_value=idx_response) with patch.object(recover, "get_adapters_from_hci", return_value={}): await ctl._find_controller() assert ctl.idx is None @pytest.mark.asyncio async def test_find_controller_no_controllers() -> None: ctl = _ctl() idx_response = MagicMock() idx_response.event_frame.status.value = 0x00 idx_response.cmd_response_frame.num_controllers = 0 cast(AsyncMock, ctl.protocol).send = AsyncMock(return_value=idx_response) with patch.object(recover, "get_adapters_from_hci", return_value={}): await ctl._find_controller() assert ctl.idx is None # --------------------------------------------------------------------------- # _check_rfkill / _unblock_rfkill # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_check_rfkill_success(adapter: MGMTBluetoothCtl) -> None: info = RFKillInfo(soft_block=False, hard_block=False, idx=2) with patch.object(recover, "rfkill_list_bluetooth", return_value=info): assert await recover._check_rfkill(adapter) == info @pytest.mark.asyncio async def test_check_rfkill_timeout(adapter: MGMTBluetoothCtl) -> None: with patch.object( recover, "rfkill_list_bluetooth", side_effect=lambda a: _block_forever() ): # asyncio_timeout wraps the executor call; force it to expire fast. with patch.object(recover, "MAX_RFKILL_TIME", 0.01): result = await recover._check_rfkill(adapter) assert result == RFKillInfo(None, None, None) def _block_forever() -> RFKillInfo: import time time.sleep(0.2) return RFKillInfo(None, None, None) @pytest.mark.asyncio async def test_unblock_rfkill_success(adapter: MGMTBluetoothCtl) -> None: with patch.object(recover, "rfkill_unblock", return_value=True): assert await recover._unblock_rfkill(adapter, 3) is True @pytest.mark.asyncio async def test_unblock_rfkill_timeout(adapter: MGMTBluetoothCtl) -> None: with patch.object( recover, "rfkill_unblock", side_effect=lambda a, idx: _block_forever() ): # asyncio_timeout wraps the executor call; force it to expire fast. with patch.object(recover, "MAX_RFKILL_TIME", 0.01): assert await recover._unblock_rfkill(adapter, 3) is False # --------------------------------------------------------------------------- # _check_or_unblock_rfkill # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_check_or_unblock_idx_none(adapter: MGMTBluetoothCtl) -> None: with patch.object( recover, "_check_rfkill", AsyncMock(return_value=RFKillInfo(None, None, None)) ): assert await recover._check_or_unblock_rfkill(adapter) is True @pytest.mark.asyncio async def test_check_or_unblock_hard_block(adapter: MGMTBluetoothCtl) -> None: info = RFKillInfo(soft_block=False, hard_block=True, idx=1) with patch.object(recover, "_check_rfkill", AsyncMock(return_value=info)): assert await recover._check_or_unblock_rfkill(adapter) is False @pytest.mark.asyncio async def test_check_or_unblock_not_soft_blocked(adapter: MGMTBluetoothCtl) -> None: info = RFKillInfo(soft_block=False, hard_block=False, idx=1) with patch.object(recover, "_check_rfkill", AsyncMock(return_value=info)): assert await recover._check_or_unblock_rfkill(adapter) is True @pytest.mark.asyncio async def test_check_or_unblock_soft_block_then_clear( adapter: MGMTBluetoothCtl, ) -> None: blocked = RFKillInfo(soft_block=True, hard_block=False, idx=1) cleared = RFKillInfo(soft_block=False, hard_block=False, idx=1) with ( patch.object( recover, "_check_rfkill", AsyncMock(side_effect=[blocked, cleared]) ), patch.object(recover, "_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover.asyncio, "sleep", AsyncMock()), ): assert await recover._check_or_unblock_rfkill(adapter) is True @pytest.mark.asyncio async def test_check_or_unblock_soft_block_still_blocked( adapter: MGMTBluetoothCtl, ) -> None: blocked = RFKillInfo(soft_block=True, hard_block=False, idx=1) # Block never clears -> the poll runs until the wall-clock grace expires and # then reports failure. Shrink the grace so the real timeout fires fast. with ( patch.object(recover, "_check_rfkill", AsyncMock(return_value=blocked)), patch.object(recover, "_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "RFKILL_UNBLOCK_GRACE_TIME", 0.05), ): assert await recover._check_or_unblock_rfkill(adapter) is False @pytest.mark.asyncio async def test_check_or_unblock_soft_block_clears_on_later_attempt( adapter: MGMTBluetoothCtl, ) -> None: """A block that clears on the second poll re-check still succeeds. The side-effect sequence is initial check (blocked) -> first poll re-check (still blocked) -> second poll re-check (cleared), so the block clears on the second re-check. The old single fixed-wait implementation would have reported failure here; polling tolerates the late unblock. """ blocked = RFKillInfo(soft_block=True, hard_block=False, idx=1) cleared = RFKillInfo(soft_block=False, hard_block=False, idx=1) check = AsyncMock(side_effect=[blocked, blocked, cleared]) with ( patch.object(recover, "_check_rfkill", check), patch.object(recover, "_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover.asyncio, "sleep", AsyncMock()), ): assert await recover._check_or_unblock_rfkill(adapter) is True # initial check + 2 poll re-checks (clears on the second) assert check.await_count == 3 # --------------------------------------------------------------------------- # _power_cycle_adapter # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_power_cycle_success(adapter: MGMTBluetoothCtl) -> None: with patch.object(recover, "_execute_reset", AsyncMock(return_value=True)): assert await recover._power_cycle_adapter(adapter) is True @pytest.mark.asyncio @pytest.mark.parametrize( "exc", [ recover.btmgmt_socket.BluetoothSocketError("no socket"), OSError("io"), asyncio.TimeoutError(), ], ) async def test_power_cycle_handles_errors( adapter: MGMTBluetoothCtl, exc: Exception ) -> None: with patch.object(recover, "_execute_reset", AsyncMock(side_effect=exc)): assert await recover._power_cycle_adapter(adapter) is False @pytest.mark.asyncio async def test_power_cycle_timeout_logs_timeout_message( adapter: MGMTBluetoothCtl, caplog: pytest.LogCaptureFixture ) -> None: with ( patch.object( recover, "_execute_reset", AsyncMock(side_effect=asyncio.TimeoutError()) ), caplog.at_level(logging.WARNING), ): assert await recover._power_cycle_adapter(adapter) is False assert "due to timeout" in caplog.text # --------------------------------------------------------------------------- # _usb_reset_adapter # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_usb_reset_success(adapter: MGMTBluetoothCtl) -> None: dev = MagicMock() dev.async_reset = AsyncMock(return_value=True) with patch.object(recover, "BluetoothDevice", return_value=dev): outcome = await recover._usb_reset_adapter(adapter) assert outcome is recover.USBResetOutcome.SUCCEEDED @pytest.mark.asyncio @pytest.mark.parametrize( "exc", [ PermissionError(2, "denied", "/dev/foo"), RuntimeError("unexpected"), ], ) async def test_usb_reset_handles_errors( adapter: MGMTBluetoothCtl, exc: Exception ) -> None: dev = MagicMock() dev.async_reset = AsyncMock(side_effect=exc) with patch.object(recover, "BluetoothDevice", return_value=dev): outcome = await recover._usb_reset_adapter(adapter) # A USB reset was attempted but failed. assert outcome is recover.USBResetOutcome.FAILED @pytest.mark.asyncio @pytest.mark.parametrize( "exc", [ recover.NotAUSBDeviceError(), FileNotFoundError(), ], ) async def test_usb_reset_not_applicable( adapter: MGMTBluetoothCtl, exc: Exception ) -> None: # A non-USB adapter (no USB device behind the hci) is "not applicable" — # distinct from an attempted-but-failed USB reset. dev = MagicMock() dev.async_reset = AsyncMock(side_effect=exc) with patch.object(recover, "BluetoothDevice", return_value=dev): outcome = await recover._usb_reset_adapter(adapter) assert outcome is recover.USBResetOutcome.NOT_APPLICABLE # --------------------------------------------------------------------------- # _execute_power_on # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_execute_power_on_success(adapter: MGMTBluetoothCtl) -> None: with ( patch.object(adapter, "set_powered", AsyncMock(return_value=True)), patch.object(adapter, "wait_for_power_state", AsyncMock(return_value=True)), ): assert ( await recover._execute_power_on(adapter, power_state_before_reset=False) is True ) @pytest.mark.asyncio async def test_execute_power_on_was_already_on(adapter: MGMTBluetoothCtl) -> None: # power_state_before_reset is True: takes the "is ON after power cycle" branch. with ( patch.object(adapter, "set_powered", AsyncMock(return_value=True)), patch.object(adapter, "wait_for_power_state", AsyncMock(return_value=True)), ): assert ( await recover._execute_power_on(adapter, power_state_before_reset=True) is True ) @pytest.mark.asyncio async def test_execute_power_on_state_false(adapter: MGMTBluetoothCtl) -> None: with ( patch.object(adapter, "set_powered", AsyncMock(return_value=True)), patch.object(adapter, "wait_for_power_state", AsyncMock(return_value=False)), ): assert ( await recover._execute_power_on(adapter, power_state_before_reset=True) is False ) @pytest.mark.asyncio async def test_execute_power_on_state_unknown(adapter: MGMTBluetoothCtl) -> None: with ( patch.object(adapter, "set_powered", AsyncMock(return_value=True)), patch.object(adapter, "wait_for_power_state", AsyncMock(return_value=None)), ): assert ( await recover._execute_power_on(adapter, power_state_before_reset=True) is False ) @pytest.mark.asyncio async def test_execute_power_on_set_powered_attribute_error( adapter: MGMTBluetoothCtl, ) -> None: with patch.object( adapter, "set_powered", AsyncMock(side_effect=AttributeError("gone")) ): assert ( await recover._execute_power_on(adapter, power_state_before_reset=True) is False ) # --------------------------------------------------------------------------- # _execute_power_off # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_execute_power_off_when_on(adapter: MGMTBluetoothCtl) -> None: set_powered = AsyncMock(return_value=True) with ( patch.object(adapter, "set_powered", set_powered), patch.object(adapter, "wait_for_power_state", AsyncMock(return_value=False)), ): assert ( await recover._execute_power_off(adapter, power_state_before_reset=True) is True ) set_powered.assert_awaited_once_with(False) @pytest.mark.asyncio async def test_execute_power_off_when_off(adapter: MGMTBluetoothCtl) -> None: set_powered = AsyncMock() with patch.object(adapter, "set_powered", set_powered): assert ( await recover._execute_power_off(adapter, power_state_before_reset=False) is True ) set_powered.assert_not_called() @pytest.mark.asyncio async def test_execute_power_off_unknown_state(adapter: MGMTBluetoothCtl) -> None: set_powered = AsyncMock() with patch.object(adapter, "set_powered", set_powered): assert ( await recover._execute_power_off(adapter, power_state_before_reset=None) is False ) @pytest.mark.asyncio async def test_execute_power_off_attribute_error(adapter: MGMTBluetoothCtl) -> None: with patch.object( adapter, "set_powered", AsyncMock(side_effect=AttributeError("gone")) ): assert ( await recover._execute_power_off(adapter, power_state_before_reset=True) is False ) # --------------------------------------------------------------------------- # _set_adapter_up_down / _bounce_adapter_interface # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_set_adapter_up_down_passes_low_byte(adapter: MGMTBluetoothCtl) -> None: adapter.idx = 0 sock = MagicMock() sock.fileno.return_value = 9 loop = asyncio.get_running_loop() with patch.object(recover, "ioctl") as mock_ioctl: await recover._set_adapter_up_down(adapter, sock, loop, recover.HCIDEVUP, "up") mock_ioctl.assert_called_once_with(9, recover.HCIDEVUP, 0) @pytest.mark.asyncio async def test_bounce_adapter_interface_down_then_up(adapter: MGMTBluetoothCtl) -> None: sock = MagicMock() calls: list[str] = [] async def fake_set(_adapter, _sock, _loop, code, state): # noqa: ANN001 calls.append(state) with ( patch.object(recover, "raw_open", return_value=sock), patch.object(recover, "raw_close") as mock_close, patch.object(recover, "_set_adapter_up_down", side_effect=fake_set), patch.object(recover.asyncio, "sleep", AsyncMock()), ): await recover._bounce_adapter_interface(adapter, up=True, down=True) assert calls == ["down", "up"] mock_close.assert_called_once_with(sock) @pytest.mark.asyncio async def test_bounce_adapter_interface_up_only(adapter: MGMTBluetoothCtl) -> None: sock = MagicMock() calls: list[str] = [] async def fake_set(_adapter, _sock, _loop, code, state): # noqa: ANN001 calls.append(state) with ( patch.object(recover, "raw_open", return_value=sock), patch.object(recover, "raw_close"), patch.object(recover, "_set_adapter_up_down", side_effect=fake_set), patch.object(recover.asyncio, "sleep", AsyncMock()), ): await recover._bounce_adapter_interface(adapter, up=True, down=False) assert calls == ["up"] # --------------------------------------------------------------------------- # _execute_reset # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_execute_reset_happy_path(adapter: MGMTBluetoothCtl) -> None: with ( patch.object(adapter, "get_powered", AsyncMock(return_value=True)), patch.object(recover, "_execute_power_off", AsyncMock(return_value=True)), patch.object(recover, "_bounce_adapter_interface", AsyncMock()), patch.object(recover, "_execute_power_on", AsyncMock(return_value=True)), ): assert await recover._execute_reset(adapter) is True @pytest.mark.asyncio async def test_execute_reset_power_on_fails(adapter: MGMTBluetoothCtl) -> None: with ( patch.object(adapter, "get_powered", AsyncMock(return_value=True)), patch.object(recover, "_execute_power_off", AsyncMock(return_value=True)), patch.object(recover, "_bounce_adapter_interface", AsyncMock()), patch.object(recover, "_execute_power_on", AsyncMock(return_value=False)), ): assert await recover._execute_reset(adapter) is False @pytest.mark.asyncio async def test_execute_reset_skips_power_off_on_timeout( adapter: MGMTBluetoothCtl, ) -> None: power_off = AsyncMock(return_value=True) with ( patch.object( adapter, "get_powered", AsyncMock(side_effect=asyncio.TimeoutError()) ), patch.object(recover, "_execute_power_off", power_off), patch.object(recover, "_bounce_adapter_interface", AsyncMock()), patch.object(recover, "_execute_power_on", AsyncMock(return_value=True)), ): assert await recover._execute_reset(adapter) is True # Frozen adapter: power off must be skipped. power_off.assert_not_called() @pytest.mark.asyncio async def test_execute_reset_final_bounce_already_up(adapter: MGMTBluetoothCtl) -> None: async def bounce(_adapter, *, down, up): # noqa: ANN001 if down is False and up is True: raise OSError(errno.EALREADY, "already up") with ( patch.object(adapter, "get_powered", AsyncMock(return_value=True)), patch.object(recover, "_execute_power_off", AsyncMock(return_value=True)), patch.object(recover, "_bounce_adapter_interface", side_effect=bounce), patch.object(recover, "_execute_power_on", AsyncMock(return_value=True)), ): assert await recover._execute_reset(adapter) is True @pytest.mark.asyncio async def test_execute_reset_final_bounce_oserror(adapter: MGMTBluetoothCtl) -> None: async def bounce(_adapter, *, down, up): # noqa: ANN001 if down is False and up is True: raise OSError(errno.EIO, "io error") with ( patch.object(adapter, "get_powered", AsyncMock(return_value=True)), patch.object(recover, "_execute_power_off", AsyncMock(return_value=True)), patch.object(recover, "_bounce_adapter_interface", side_effect=bounce), patch.object(recover, "_execute_power_on", AsyncMock(return_value=True)), ): assert await recover._execute_reset(adapter) is False @pytest.mark.asyncio async def test_execute_reset_final_bounce_unexpected_error( adapter: MGMTBluetoothCtl, ) -> None: # A non-OSError raised by the final bounce is swallowed and fails the reset. async def bounce(_adapter, *, down, up): # noqa: ANN001 if down is False and up is True: raise RuntimeError("boom") with ( patch.object(adapter, "get_powered", AsyncMock(return_value=True)), patch.object(recover, "_execute_power_off", AsyncMock(return_value=True)), patch.object(recover, "_bounce_adapter_interface", side_effect=bounce), patch.object(recover, "_execute_power_on", AsyncMock(return_value=True)), ): assert await recover._execute_reset(adapter) is False @pytest.mark.asyncio @pytest.mark.parametrize("exc", [AttributeError("gone"), RuntimeError("unexpected")]) async def test_execute_reset_get_powered_error_continues( adapter: MGMTBluetoothCtl, exc: Exception ) -> None: # If reading the initial power state fails (but does not time out), the reset # still proceeds: power-off is attempted, then bounce + power-on decide. power_off = AsyncMock(return_value=True) with ( patch.object(adapter, "get_powered", AsyncMock(side_effect=exc)), patch.object(recover, "_execute_power_off", power_off), patch.object(recover, "_bounce_adapter_interface", AsyncMock()), patch.object(recover, "_execute_power_on", AsyncMock(return_value=True)), ): assert await recover._execute_reset(adapter) is True power_off.assert_awaited_once() @pytest.mark.asyncio @pytest.mark.parametrize("exc", [asyncio.TimeoutError(), RuntimeError("boom")]) async def test_execute_reset_power_off_error_is_swallowed( adapter: MGMTBluetoothCtl, exc: Exception ) -> None: # Failures while powering off are swallowed; the reset proceeds to bounce/power-on. with ( patch.object(adapter, "get_powered", AsyncMock(return_value=True)), patch.object(recover, "_execute_power_off", AsyncMock(side_effect=exc)), patch.object(recover, "_bounce_adapter_interface", AsyncMock()), patch.object(recover, "_execute_power_on", AsyncMock(return_value=True)), ): assert await recover._execute_reset(adapter) is True @pytest.mark.asyncio async def test_execute_reset_first_bounce_error_is_swallowed( adapter: MGMTBluetoothCtl, ) -> None: # The down/up bounce before power-on is best-effort: a failure does not abort. async def bounce(_adapter, *, down, up): # noqa: ANN001 if down is True: raise OSError(errno.EIO, "io error") with ( patch.object(adapter, "get_powered", AsyncMock(return_value=True)), patch.object(recover, "_execute_power_off", AsyncMock(return_value=True)), patch.object(recover, "_bounce_adapter_interface", side_effect=bounce), patch.object(recover, "_execute_power_on", AsyncMock(return_value=True)), ): assert await recover._execute_reset(adapter) is True @pytest.mark.asyncio @pytest.mark.parametrize("exc", [asyncio.TimeoutError(), RuntimeError("boom")]) async def test_execute_reset_power_on_error_fails( adapter: MGMTBluetoothCtl, exc: Exception ) -> None: # A timeout or unexpected error while powering back on fails the reset. with ( patch.object(adapter, "get_powered", AsyncMock(return_value=True)), patch.object(recover, "_execute_power_off", AsyncMock(return_value=True)), patch.object(recover, "_bounce_adapter_interface", AsyncMock()), patch.object(recover, "_execute_power_on", AsyncMock(side_effect=exc)), ): assert await recover._execute_reset(adapter) is False # --------------------------------------------------------------------------- # raw_open / raw_close # --------------------------------------------------------------------------- def test_raw_open_binds_adapter() -> None: sock = MagicMock() with patch.object(recover.socket, "socket", return_value=sock) as mock_socket: result = raw_open(2) mock_socket.assert_called_once() sock.bind.assert_called_once_with((2,)) assert result is sock def test_raw_close_detaches_and_closes() -> None: sock = MagicMock() sock.detach.return_value = 11 with patch.object(recover.socket, "close") as mock_close: raw_close(sock) sock.detach.assert_called_once() mock_close.assert_called_once_with(11) # --------------------------------------------------------------------------- # _get_adapter context manager # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_get_adapter_yields_resolved_adapter() -> None: ctl = MagicMock() ctl.idx = 0 ctl.hci_name = "hci0" ctl.mac = "AA:BB:CC:DD:EE:FF" ctl.setup = AsyncMock() ctl.close = AsyncMock() with patch.object(recover, "MGMTBluetoothCtl", return_value=ctl): async with recover._get_adapter("hci0", "AA:BB:CC:DD:EE:FF") as got: assert got is ctl ctl.close.assert_awaited_once() @pytest.mark.asyncio async def test_get_adapter_yields_none_when_idx_missing() -> None: ctl = MagicMock() ctl.idx = None ctl.setup = AsyncMock() ctl.close = AsyncMock() with patch.object(recover, "MGMTBluetoothCtl", return_value=ctl): async with recover._get_adapter("hci0", "AA:BB:CC:DD:EE:FF") as got: assert got is None ctl.close.assert_awaited_once() @pytest.mark.asyncio @pytest.mark.parametrize( "exc", [ recover.btmgmt_socket.BluetoothSocketError("no socket"), OSError("io"), asyncio.TimeoutError(), ], ) async def test_get_adapter_yields_none_on_setup_error(exc: Exception) -> None: ctl = MagicMock() ctl.setup = AsyncMock(side_effect=exc) ctl.close = AsyncMock() with patch.object(recover, "MGMTBluetoothCtl", return_value=ctl): async with recover._get_adapter("hci0", "AA:BB:CC:DD:EE:FF") as got: assert got is None @pytest.mark.asyncio async def test_get_adapter_timeout_logs_timeout_message( caplog: pytest.LogCaptureFixture, ) -> None: ctl = MagicMock() ctl.setup = AsyncMock(side_effect=asyncio.TimeoutError()) ctl.close = AsyncMock() with ( patch.object(recover, "MGMTBluetoothCtl", return_value=ctl), caplog.at_level(logging.WARNING), ): async with recover._get_adapter("hci0", "AA:BB:CC:DD:EE:FF") as got: assert got is None assert "due to timeout" in caplog.text @pytest.mark.asyncio async def test_get_adapter_close_failure_is_swallowed() -> None: # A failure closing the adapter in the finally block must not propagate. ctl = MagicMock() ctl.idx = 0 ctl.hci_name = "hci0" ctl.mac = "AA:BB:CC:DD:EE:FF" ctl.setup = AsyncMock() ctl.close = AsyncMock(side_effect=OSError("close failed")) with patch.object(recover, "MGMTBluetoothCtl", return_value=ctl): async with recover._get_adapter("hci0", "AA:BB:CC:DD:EE:FF") as got: assert got is ctl ctl.close.assert_awaited_once() # --------------------------------------------------------------------------- # BluetoothMGMTProtocol # --------------------------------------------------------------------------- def _make_protocol() -> BluetoothMGMTProtocol: loop = asyncio.get_running_loop() return BluetoothMGMTProtocol(5, loop.create_future(), MagicMock()) @pytest.mark.asyncio async def test_protocol_connection_made_resolves_future() -> None: proto = _make_protocol() transport = MagicMock() proto.connection_made(transport) assert proto.transport is transport assert proto.connection_mode_future.done() @pytest.mark.asyncio async def test_protocol_data_received_resolves_future() -> None: proto = _make_protocol() loop = asyncio.get_running_loop() proto.future = loop.create_future() response = MagicMock() response.cmd_response_frame = MagicMock() with patch.object(recover.btmgmt_protocol, "reader", return_value=response): proto.data_received(b"payload") assert proto.future.result() is response @pytest.mark.asyncio async def test_protocol_data_received_ignores_value_error() -> None: proto = _make_protocol() loop = asyncio.get_running_loop() proto.future = loop.create_future() with patch.object( recover.btmgmt_protocol, "reader", side_effect=ValueError("bad event") ): proto.data_received(b"payload") # Malformed event must not crash or resolve the pending future. assert not proto.future.done() @pytest.mark.asyncio async def test_protocol_send_without_transport_raises() -> None: proto = _make_protocol() proto.transport = None with patch.object(recover.btmgmt_protocol, "command", return_value=[]): with pytest.raises(recover.btmgmt_socket.BluetoothSocketError): await proto.send("ReadControllerIndexList", None) @pytest.mark.asyncio async def test_protocol_send_writes_to_socket_directly() -> None: proto = _make_protocol() proto.transport = MagicMock() frame = MagicMock() frame.octets = b"data" with patch.object(recover.btmgmt_protocol, "command", return_value=[frame]): task = asyncio.ensure_future(proto.send("SetPowered", 0, 1)) await asyncio.sleep(0) assert proto.future is not None proto.future.set_result("RESPONSE") result = await task # The kernel-ABI workaround writes to the raw socket, not the transport. cast(MagicMock, proto.sock).send.assert_called_once_with(b"data") cast(MagicMock, proto.transport).write.assert_not_called() assert result == "RESPONSE" @pytest.mark.asyncio async def test_protocol_send_times_out() -> None: proto = _make_protocol() proto.timeout = 0.01 proto.transport = MagicMock() frame = MagicMock() frame.octets = b"data" with patch.object(recover.btmgmt_protocol, "command", return_value=[frame]): with pytest.raises(asyncio.TimeoutError): await proto.send("ReadControllerInformation", 0) @pytest.mark.asyncio async def test_protocol_timeout_future_sets_exception() -> None: proto = _make_protocol() loop = asyncio.get_running_loop() fut = loop.create_future() proto._timeout_future(fut) with pytest.raises(asyncio.TimeoutError): fut.result() @pytest.mark.asyncio async def test_protocol_connection_lost_clears_transport() -> None: proto = _make_protocol() proto.transport = MagicMock() proto.connection_lost(OSError("dropped")) assert proto.transport is None # --------------------------------------------------------------------------- # MGMTBluetoothCtl.setup # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_setup_success() -> None: ctl = MGMTBluetoothCtl("hci0", "AA:BB:CC:DD:EE:FF", 5) sock = MagicMock() loop = asyncio.get_running_loop() async def fake_create(sock_arg, factory, *args, **kwargs): # noqa: ANN001 proto = factory() proto.connection_made(MagicMock()) return (MagicMock(), proto) with ( patch.object(recover.btmgmt_socket, "open", return_value=sock), patch.object(loop, "_create_connection_transport", fake_create), patch.object(recover.MGMTBluetoothCtl, "_find_controller", AsyncMock()), ): await ctl.setup() assert ctl.sock is sock assert isinstance(ctl.protocol, BluetoothMGMTProtocol) @pytest.mark.asyncio async def test_setup_timeout_closes_socket() -> None: ctl = MGMTBluetoothCtl("hci0", "AA:BB:CC:DD:EE:FF", 5) sock = MagicMock() loop = asyncio.get_running_loop() async def hang(*args, **kwargs): # noqa: ANN001 await asyncio.sleep(10) real_timeout = recover.asyncio_timeout with ( patch.object(recover.btmgmt_socket, "open", return_value=sock), patch.object(loop, "_create_connection_transport", hang), patch.object(recover, "asyncio_timeout", lambda _t: real_timeout(0.01)), patch.object(recover.btmgmt_socket, "close") as mock_close, ): with pytest.raises(asyncio.TimeoutError): await ctl.setup() mock_close.assert_called_once_with(sock) # --------------------------------------------------------------------------- # recover_adapter — top-level state machine # --------------------------------------------------------------------------- def _resolved_adapter() -> MagicMock: ctl = MagicMock() ctl.idx = 0 ctl.hci_name = "hci0" ctl.mac = "AA:BB:CC:DD:EE:FF" ctl.name = "hci0 [AA:BB:CC:DD:EE:FF] (0)" return ctl @pytest.mark.asyncio async def test_recover_adapter_not_found() -> None: with patch.object(recover, "_get_adapter", return_value=adapter_cm(None)): assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is False @pytest.mark.asyncio async def test_recover_adapter_power_cycle_success() -> None: ctl = _resolved_adapter() with ( patch.object(recover, "_get_adapter", return_value=adapter_cm(ctl)), patch.object(recover, "_check_or_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=True)), patch.object(recover.asyncio, "sleep", AsyncMock()), ): # Power cycle succeeds and the adapter has not gone silent: short-circuits True. assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is True @pytest.mark.asyncio async def test_recover_adapter_usb_reset_path() -> None: first = _resolved_adapter() second = _resolved_adapter() with ( patch.object( recover, "_get_adapter", side_effect=[adapter_cm(first), adapter_cm(second)], ), patch.object(recover, "_check_or_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=False)), patch.object( recover, "_usb_reset_adapter", AsyncMock(return_value=recover.USBResetOutcome.SUCCEEDED), ), patch.object(recover.asyncio, "sleep", AsyncMock()), ): assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is True @pytest.mark.asyncio async def test_recover_adapter_usb_reset_fails() -> None: ctl = _resolved_adapter() with ( patch.object(recover, "_get_adapter", return_value=adapter_cm(ctl)), patch.object(recover, "_check_or_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=False)), patch.object( recover, "_usb_reset_adapter", AsyncMock(return_value=recover.USBResetOutcome.FAILED), ), patch.object(recover.asyncio, "sleep", AsyncMock()), ): assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is False @pytest.mark.asyncio async def test_recover_adapter_gone_silent_forces_usb_reset() -> None: first = _resolved_adapter() second = _resolved_adapter() power_cycle = AsyncMock(return_value=True) usb_reset = AsyncMock(return_value=recover.USBResetOutcome.SUCCEEDED) with ( patch.object( recover, "_get_adapter", side_effect=[adapter_cm(first), adapter_cm(second)], ), patch.object(recover, "_check_or_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "_power_cycle_adapter", power_cycle), patch.object(recover, "_usb_reset_adapter", usb_reset), patch.object(recover.asyncio, "sleep", AsyncMock()), ): # gone_silent=True: even a successful power cycle still triggers a USB reset. assert ( await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF", gone_silent=True) is True ) usb_reset.assert_awaited_once() @pytest.mark.asyncio async def test_recover_adapter_gone_silent_non_usb_power_cycle_ok() -> None: # gone_silent forces a USB reset, but the adapter is not a USB device # (USB reset is not applicable). The power cycle succeeded, so a non-USB # adapter is still recovered and recover_adapter must report success. ctl = _resolved_adapter() with ( patch.object(recover, "_get_adapter", return_value=adapter_cm(ctl)), patch.object(recover, "_check_or_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=True)), patch.object( recover, "_usb_reset_adapter", AsyncMock(return_value=recover.USBResetOutcome.NOT_APPLICABLE), ), patch.object(recover.asyncio, "sleep", AsyncMock()), ): assert ( await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF", gone_silent=True) is True ) @pytest.mark.asyncio async def test_recover_adapter_non_usb_power_cycle_failed() -> None: # USB reset not applicable AND the power cycle failed: nothing recovered # the adapter, so recover_adapter must report failure. ctl = _resolved_adapter() with ( patch.object(recover, "_get_adapter", return_value=adapter_cm(ctl)), patch.object(recover, "_check_or_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=False)), patch.object( recover, "_usb_reset_adapter", AsyncMock(return_value=recover.USBResetOutcome.NOT_APPLICABLE), ), patch.object(recover.asyncio, "sleep", AsyncMock()), ): assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is False @pytest.mark.asyncio async def test_recover_adapter_second_lookup_fails() -> None: first = _resolved_adapter() calls = {"n": 0} def get_adapter(*_args: object, **_kwargs: object) -> object: calls["n"] += 1 # First lookup (pre-reset) resolves; every post-reset lookup misses. return adapter_cm(first if calls["n"] == 1 else None) with ( patch.object(recover, "_get_adapter", side_effect=get_adapter), patch.object(recover, "_check_or_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=False)), patch.object( recover, "_usb_reset_adapter", AsyncMock(return_value=recover.USBResetOutcome.SUCCEEDED), ), patch.object(recover.asyncio, "sleep", AsyncMock()), ): # USB reset succeeded but the adapter never reappears: every retry # misses, so recovery is reported as failed only after exhausting them. assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is False # Pre-reset lookup + one lookup per post-reset attempt. assert calls["n"] == 1 + recover.POST_RESET_LOOKUP_ATTEMPTS @pytest.mark.asyncio async def test_recover_adapter_second_lookup_succeeds_after_retry() -> None: # The adapter re-enumerates slowly after the USB reset: the first two # post-reset lookups miss, then it reappears and recovery succeeds. first = _resolved_adapter() second = _resolved_adapter() sleep = AsyncMock() with ( patch.object( recover, "_get_adapter", side_effect=[ adapter_cm(first), adapter_cm(None), adapter_cm(None), adapter_cm(second), ], ), patch.object(recover, "_check_or_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=False)), patch.object(recover, "_usb_reset_adapter", AsyncMock(return_value=True)), patch.object(recover.asyncio, "sleep", sleep), ): assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is True # Exact sleep sequence: the post-USB-reset DBUS_REGISTER_TIME wait, then one # POST_RESET_LOOKUP_RETRY_TIME wait per missed lookup (two misses here) # before the adapter is found on the third attempt. assert sleep.await_args_list == [ call(recover.DBUS_REGISTER_TIME), call(recover.POST_RESET_LOOKUP_RETRY_TIME), call(recover.POST_RESET_LOOKUP_RETRY_TIME), ] @pytest.mark.asyncio async def test_recover_adapter_handles_moved_hci_and_resolved_mac() -> None: # Adapter reports a different hci number and MAC than requested. ctl = MagicMock() ctl.idx = 1 ctl.hci_name = "hci1" ctl.mac = "11:22:33:44:55:66" ctl.name = "hci1 [11:22:33:44:55:66] (1)" with ( patch.object(recover, "_get_adapter", return_value=adapter_cm(ctl)), patch.object(recover, "_check_or_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=True)), patch.object(recover.asyncio, "sleep", AsyncMock()), ): assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is True @pytest.mark.asyncio async def test_recover_adapter_first_rfkill_block_is_non_fatal() -> None: # A failed rfkill unblock before the power cycle only warns; it does not abort. ctl = _resolved_adapter() with ( patch.object(recover, "_get_adapter", return_value=adapter_cm(ctl)), patch.object( recover, "_check_or_unblock_rfkill", AsyncMock(return_value=False) ), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=True)), patch.object(recover.asyncio, "sleep", AsyncMock()), ): assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is True @pytest.mark.asyncio async def test_recover_adapter_post_reset_rfkill_blocked() -> None: first = _resolved_adapter() second = _resolved_adapter() with ( patch.object( recover, "_get_adapter", side_effect=[adapter_cm(first), adapter_cm(second)], ), # Passes the first rfkill check, fails the post-reset one. patch.object( recover, "_check_or_unblock_rfkill", AsyncMock(side_effect=[True, False]), ), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=False)), patch.object( recover, "_usb_reset_adapter", AsyncMock(return_value=recover.USBResetOutcome.SUCCEEDED), ), patch.object(recover.asyncio, "sleep", AsyncMock()), ): assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is False @pytest.mark.asyncio async def test_recover_adapter_post_reset_moved_hci() -> None: # The USB reset moves the adapter to a new hci number: the post-reset # lookup resolves it under hci1, and recovery still succeeds. first = _resolved_adapter() moved = MagicMock() moved.idx = 1 moved.hci_name = "hci1" moved.mac = "AA:BB:CC:DD:EE:FF" moved.name = "hci1 [AA:BB:CC:DD:EE:FF] (1)" with ( patch.object( recover, "_get_adapter", side_effect=[adapter_cm(first), adapter_cm(moved)], ), patch.object(recover, "_check_or_unblock_rfkill", AsyncMock(return_value=True)), patch.object(recover, "_power_cycle_adapter", AsyncMock(return_value=False)), patch.object(recover, "_usb_reset_adapter", AsyncMock(return_value=True)), patch.object(recover.asyncio, "sleep", AsyncMock()), ): assert await recover.recover_adapter(0, "AA:BB:CC:DD:EE:FF") is True