pax_global_header 0000666 0000000 0000000 00000000064 15204732675 0014525 g ustar 00root root 0000000 0000000 52 comment=1cc791eddb460469f237ce3e9119da2b0ef199dc
Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/ 0000775 0000000 0000000 00000000000 15204732675 0022271 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.all-contributorsrc 0000664 0000000 0000000 00000000466 15204732675 0026130 0 ustar 00root root 0000000 0000000 {
"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/.editorconfig 0000664 0000000 0000000 00000000444 15204732675 0024750 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 15204732675 0023631 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15204732675 0026014 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/ISSUE_TEMPLATE/1-bug_report.md 0000664 0000000 0000000 00000000422 15204732675 0030642 0 ustar 00root root 0000000 0000000 ---
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.md 0000664 0000000 0000000 00000000672 15204732675 0031623 0 ustar 00root root 0000000 0000000 ---
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.yml 0000664 0000000 0000000 00000001351 15204732675 0026461 0 ustar 00root root 0000000 0000000 # 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.toml 0000664 0000000 0000000 00000003515 15204732675 0025774 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 15204732675 0025666 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.github/workflows/ci.yml 0000664 0000000 0000000 00000005036 15204732675 0027010 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000534 15204732675 0031237 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000001340 15204732675 0031147 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000774 15204732675 0027663 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000004066 15204732675 0024267 0 ustar 00root root 0000000 0000000 # 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.yml 0000664 0000000 0000000 00000000306 15204732675 0024357 0 ustar 00root root 0000000 0000000 tasks:
- 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/ 0000775 0000000 0000000 00000000000 15204732675 0023251 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.idea/bluetooth-auto-recovery.iml 0000664 0000000 0000000 00000000515 15204732675 0030564 0 ustar 00root root 0000000 0000000
Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.idea/watcherTasks.xml 0000664 0000000 0000000 00000005253 15204732675 0026443 0 ustar 00root root 0000000 0000000
Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.idea/workspace.xml 0000664 0000000 0000000 00000002743 15204732675 0025777 0 ustar 00root root 0000000 0000000
Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/.pre-commit-config.yaml 0000664 0000000 0000000 00000002754 15204732675 0026562 0 ustar 00root root 0000000 0000000 # 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.yml 0000664 0000000 0000000 00000001005 15204732675 0025353 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000076053 15204732675 0024115 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000007512 15204732675 0024527 0 ustar 00root root 0000000 0000000 # 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/LICENSE 0000664 0000000 0000000 00000002060 15204732675 0023274 0 ustar 00root root 0000000 0000000
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.md 0000664 0000000 0000000 00000007467 15204732675 0023566 0 ustar 00root root 0000000 0000000 # Bluetooth Auto Recovery
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.mjs 0000664 0000000 0000000 00000000362 15204732675 0026430 0 ustar 00root root 0000000 0000000 export 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/ 0000775 0000000 0000000 00000000000 15204732675 0023221 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/Makefile 0000664 0000000 0000000 00000001175 15204732675 0024665 0 ustar 00root root 0000000 0000000 # 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.bat 0000664 0000000 0000000 00000001374 15204732675 0024633 0 ustar 00root root 0000000 0000000 @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/ 0000775 0000000 0000000 00000000000 15204732675 0024521 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/_static/ 0000775 0000000 0000000 00000000000 15204732675 0026147 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/_static/.gitkeep 0000664 0000000 0000000 00000000000 15204732675 0027566 0 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/changelog.md 0000664 0000000 0000000 00000000045 15204732675 0026771 0 ustar 00root root 0000000 0000000 ```{include} ../../CHANGELOG.md
```
Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/conf.py 0000664 0000000 0000000 00000003661 15204732675 0026026 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000000050 15204732675 0027545 0 ustar 00root root 0000000 0000000 ```{include} ../../CONTRIBUTING.md
```
Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/docs/source/index.md 0000664 0000000 0000000 00000000367 15204732675 0026160 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000000302 15204732675 0027537 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000000155 15204732675 0026150 0 ustar 00root root 0000000 0000000 # Usage
To use this package, import it:
```python
import bluetooth_auto_recovery
```
TODO: Document usage
Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/examples/ 0000775 0000000 0000000 00000000000 15204732675 0024107 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/examples/reset_hci0.py 0000664 0000000 0000000 00000000443 15204732675 0026507 0 ustar 00root root 0000000 0000000 import 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.lock 0000664 0000000 0000000 00000371400 15204732675 0024472 0 ustar 00root root 0000000 0000000 # 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.toml 0000664 0000000 0000000 00000006001 15204732675 0025202 0 ustar 00root root 0000000 0000000 [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.json 0000664 0000000 0000000 00000000101 15204732675 0024777 0 ustar 00root root 0000000 0000000 {
"extends": ["github>browniebroke/renovate-configs:python"]
}
Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/src/ 0000775 0000000 0000000 00000000000 15204732675 0023060 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/src/bluetooth_auto_recovery/ 0000775 0000000 0000000 00000000000 15204732675 0030033 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/src/bluetooth_auto_recovery/__init__.py 0000664 0000000 0000000 00000002057 15204732675 0032150 0 ustar 00root root 0000000 0000000 from __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.typed 0000664 0000000 0000000 00000000000 15204732675 0031520 0 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/src/bluetooth_auto_recovery/recover.py 0000664 0000000 0000000 00000104404 15204732675 0032055 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000000341 15204732675 0031360 0 ustar 00root root 0000000 0000000 from __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/ 0000775 0000000 0000000 00000000000 15204732675 0023433 5 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/tests/__init__.py 0000664 0000000 0000000 00000000000 15204732675 0025532 0 ustar 00root root 0000000 0000000 Bluetooth-Devices-bluetooth-auto-recovery-0e9db41/tests/conftest.py 0000664 0000000 0000000 00000001712 15204732675 0025633 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000001337 15204732675 0026013 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000143415 15204732675 0026521 0 ustar 00root root 0000000 0000000 """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