diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 92ebdab1..ed5500ae 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,7 +12,7 @@ // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": - "python -m pip install '.[build,test,development,documentation]'", + "python -m pip install '.[all]' --group all", // Configure tool-specific properties. "customizations": { diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 9f24fcdf..f742c95f 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -35,7 +35,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install ".[build,test,development]" + python -m pip install ".[all]" --group all - name: Check run: | invoke project.pre-commit diff --git a/.github/workflows/python-avatar.yml b/.github/workflows/python-avatar.yml index 60a6aeda..46b5bc3a 100644 --- a/.github/workflows/python-avatar.yml +++ b/.github/workflows/python-avatar.yml @@ -34,7 +34,7 @@ jobs: - name: Install run: | python -m pip install --upgrade pip - python -m pip install .[avatar] + python -m pip install .[all,avatar] - name: Rootcanal run: nohup python -m rootcanal > rootcanal.log & - name: Test diff --git a/.github/workflows/python-build-test.yml b/.github/workflows/python-build-test.yml index eb770450..7c07bac1 100644 --- a/.github/workflows/python-build-test.yml +++ b/.github/workflows/python-build-test.yml @@ -35,7 +35,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install ".[build,test,development,documentation]" + python -m pip install ".[all]" --group all - name: Test run: | invoke test @@ -62,7 +62,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - python -m pip install ".[build,test,development,documentation]" + python -m pip install ".[all]" --group all - name: Install Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: diff --git a/README.md b/README.md index 43e3c642..cbd4ebf4 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ To install package dependencies needed to run the bumble examples, execute the f ``` python -m pip install --upgrade pip -python -m pip install ".[test,development,documentation]" +python -m pip install ".[all]" --group dev ``` ### Examples diff --git a/apps/auracast.py b/apps/auracast.py index 59cedbbd..a886e342 100644 --- a/apps/auracast.py +++ b/apps/auracast.py @@ -40,9 +40,7 @@ try: import lc3 # type: ignore # pylint: disable=E0401 except ImportError as e: - raise ImportError( - "Try `python -m pip install \"git+https://github.com/google/liblc3.git\"`." - ) from e + raise ImportError("Try `python -m pip install '.[auracast]'`.") from e import bumble.device import bumble.logging diff --git a/apps/lea_unicast/app.py b/apps/lea_unicast/app.py index 4e31c0eb..d821c9a7 100644 --- a/apps/lea_unicast/app.py +++ b/apps/lea_unicast/app.py @@ -30,7 +30,7 @@ try: import lc3 # type: ignore # pylint: disable=E0401 except ImportError as e: - raise ImportError("Try `python -m pip install \".[lc3]\"`.") from e + raise ImportError("Try `python -m pip install \".[auracast]\"`.") from e import aiohttp.web import click diff --git a/bumble/keys.py b/bumble/keys.py index 3dc739d8..2e05aaab 100644 --- a/bumble/keys.py +++ b/bumble/keys.py @@ -27,6 +27,7 @@ import json import logging import os +import pathlib from typing import TYPE_CHECKING, Any from typing_extensions import Self @@ -248,29 +249,26 @@ class without a namespace. With the default namespace, reading from a file will DEFAULT_NAMESPACE = '__DEFAULT__' DEFAULT_BASE_NAME = "keys" - def __init__(self, namespace, filename=None): - self.namespace = namespace if namespace is not None else self.DEFAULT_NAMESPACE + def __init__( + self, namespace: str | None = None, filename: str | None = None + ) -> None: + self.namespace = namespace or self.DEFAULT_NAMESPACE - if filename is None: - # Use a default for the current user + if filename: + self.filename = pathlib.Path(filename).resolve() + self.directory_name = self.filename.parent + else: + import platformdirs # Deferred import - # Import here because this may not exist on all platforms - # pylint: disable=import-outside-toplevel - import appdirs + base_dir = platformdirs.user_data_path(self.APP_NAME, self.APP_AUTHOR) + self.directory_name = base_dir / self.KEYS_DIR - self.directory_name = os.path.join( - appdirs.user_data_dir(self.APP_NAME, self.APP_AUTHOR), self.KEYS_DIR - ) - base_name = self.DEFAULT_BASE_NAME if namespace is None else self.namespace - json_filename = ( - f'{base_name}.json'.lower().replace(':', '-').replace('/p', '-p') - ) - self.filename = os.path.join(self.directory_name, json_filename) - else: - self.filename = filename - self.directory_name = os.path.dirname(os.path.abspath(self.filename)) + base_name = self.namespace if namespace else self.DEFAULT_BASE_NAME + safe_name = base_name.lower().replace(':', '-').replace('/', '-') + + self.filename = self.directory_name / f"{safe_name}.json" - logger.debug(f'JSON keystore: {self.filename}') + logger.debug('JSON keystore: %s', self.filename) @classmethod def from_device( @@ -293,7 +291,9 @@ def from_device( return cls(namespace, filename) - async def load(self): + async def load( + self, + ) -> tuple[dict[str, dict[str, dict[str, Any]]], dict[str, dict[str, Any]]]: # Try to open the file, without failing. If the file does not exist, it # will be created upon saving. try: @@ -312,17 +312,17 @@ async def load(self): return next(iter(db.items())) # Finally, just create an empty key map for the namespace - key_map = {} + key_map: dict[str, dict[str, Any]] = {} db[self.namespace] = key_map return (db, key_map) - async def save(self, db): + async def save(self, db: dict[str, dict[str, dict[str, Any]]]) -> None: # Create the directory if it doesn't exist if not os.path.exists(self.directory_name): os.makedirs(self.directory_name, exist_ok=True) # Save to a temporary file - temp_filename = self.filename + '.tmp' + temp_filename = self.filename.with_name(self.filename.name + ".tmp") with open(temp_filename, 'w', encoding='utf-8') as output: json.dump(db, output, sort_keys=True, indent=4) @@ -334,16 +334,16 @@ async def delete(self, name: str) -> None: del key_map[name] await self.save(db) - async def update(self, name, keys): + async def update(self, name: str, keys: PairingKeys) -> None: db, key_map = await self.load() key_map.setdefault(name, {}).update(keys.to_dict()) await self.save(db) - async def get_all(self): + async def get_all(self) -> list[tuple[str, PairingKeys]]: _, key_map = await self.load() return [(name, PairingKeys.from_dict(keys)) for (name, keys) in key_map.items()] - async def delete_all(self): + async def delete_all(self) -> None: db, key_map = await self.load() key_map.clear() await self.save(db) diff --git a/pyproject.toml b/pyproject.toml index 601c3367..b497063c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,9 +12,6 @@ license-files = ["LICENSE"] authors = [{ name = "Google", email = "bumble-dev@google.com" }] requires-python = ">=3.10" dependencies = [ - "aiohttp ~= 3.8; platform_system!='Emscripten'", - "appdirs >= 1.4; platform_system!='Emscripten'", - "click >= 8.1.3; platform_system!='Emscripten'", "cryptography >= 44.0.3; platform_system!='Emscripten' and platform_system!='Android'", # Pyodide bundles a version of cryptography that is built for wasm, which may not match the # versions available on PyPI. Relax the version requirement since it's better than being @@ -25,31 +22,52 @@ dependencies = [ # updated. Relax the version requirement since it's better than being completely unable # to import the package in case of version mismatch. "cryptography >= 42.0.8; platform_system=='Android'", - "grpcio >= 1.62.1; platform_system!='Emscripten'", - "humanize >= 4.6.0; platform_system!='Emscripten'", - "libusb1 >= 2.0.1; platform_system!='Emscripten'", - "libusb-package == 1.0.26.1; platform_system!='Emscripten' and platform_system!='Android'", - "platformdirs >= 3.10.0; platform_system!='Emscripten'", - "prompt_toolkit >= 3.0.16; platform_system!='Emscripten'", - "prettytable >= 3.6.0; platform_system!='Emscripten'", - "protobuf >= 3.12.4; platform_system!='Emscripten'", "pyee >= 13.0.0", - "pyserial-asyncio >= 0.5; platform_system!='Emscripten'", - "pyserial >= 3.5; platform_system!='Emscripten'", - "pyusb >= 1.2; platform_system!='Emscripten'", - "tomli ~= 2.2.1; platform_system!='Emscripten' and python_version<'3.11'", - "websockets >= 15.0.1; platform_system!='Emscripten'", + "platformdirs >= 3.10.0; platform_system!='Emscripten'", ] [project.optional-dependencies] -build = ["build >= 0.7"] -test = [ +avatar = [ + "pandora-avatar == 0.0.10", + "rootcanal == 1.11.1 ; python_version>='3.10'", +] +pandora = ["bt-test-interfaces >= 0.0.6"] +auracast = [ + "lc3py >= 1.1.3; python_version>='3.10' and ((platform_system=='Linux' and platform_machine=='x86_64') or (platform_system=='Darwin' and platform_machine=='arm64'))", + "sounddevice >= 0.5.1", +] +app = [ + "aiohttp ~= 3.8", + "click >= 8.1.3", + "humanize >= 4.6.0", + "prompt_toolkit >= 3.0.16", + "prettytable >= 3.6.0", + "tomli ~= 2.2.1; python_version<'3.11'", +] +transport = [ + "grpcio >= 1.62.1", + "libusb1 >= 2.0.1", + "libusb-package == 1.0.26.1; platform_system!='Android'", + "protobuf >= 3.12.4", + "pyserial-asyncio >= 0.5", + "pyserial >= 3.5", + "pyusb >= 1.2", + "websockets >= 15.0.1", +] +all = [ + "bumble[auracast]", + "bumble[app]", + "bumble[transport]", +] + + +[dependency-groups] +dev = [ + "build >= 0.7", "pytest >= 8.2", "pytest-asyncio >= 0.23.5", "pytest-html >= 3.2.0", "coverage >= 6.4", -] -development = [ "black ~= 25.1", "bt-test-interfaces >= 0.0.6", "grpcio-tools >= 1.62.1", @@ -64,21 +82,23 @@ development = [ "types-invoke >= 1.7.3", "types-protobuf >= 4.21.0", ] -avatar = [ - "pandora-avatar == 0.0.10", - "rootcanal == 1.11.1 ; python_version>='3.10'", -] -pandora = ["bt-test-interfaces >= 0.0.6"] documentation = [ "mkdocs >= 1.6.0", "mkdocs-material >= 9.6", "mkdocstrings[python] >= 0.27.0", ] -auracast = [ - "lc3py >= 1.1.3; python_version>='3.10' and ((platform_system=='Linux' and platform_machine=='x86_64') or (platform_system=='Darwin' and platform_machine=='arm64'))", - "sounddevice >= 0.5.1", +all = [ + {include-group = "dev"}, + {include-group = "documentation"}, ] +[tool.uv] +default-groups = [ + "dev", + "documentation", +] + + [project.scripts] bumble-auracast = "bumble.apps.auracast:main" bumble-ble-rpa-tool = "bumble.apps.ble_rpa_tool:main"