diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 28918d22..33e3fb6e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ Before you submit a pull request, check that it meets these guidelines: 1. If the pull request adds functionality, the docs should be updated. 2. Modify the `CHANGELOG.rst`, describing your changes as is specified by the guidelines in that document. -3. The pull request should work for Python 3.7+ on the following platforms: +3. The pull request should work for Python 3.8+ on the following platforms: - Windows 10, version 16299 (Fall Creators Update) and greater - Linux distributions with BlueZ >= 5.43 - OS X / macOS >= 10.11 diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index ea19753b..e7266056 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index 9a2dbf83..2b57fada 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ examples/notcommit/ -.vscode/ .idea/ .vs/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..c45bcc66 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "ms-python.python", + "ms-python.black-formatter", + "ms-python.isort", + "ms-python.flake8" + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..d0bf3eaf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, + }, + "black-formatter.importStrategy": "fromEnvironment", + "isort.importStrategy": "fromEnvironment", + "isort.args":["--profile", "black"], + "flake8.importStrategy": "fromEnvironment" +} diff --git a/AUTHORS.rst b/AUTHORS.rst index 503a5757..d4e73d8e 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -20,6 +20,7 @@ Contributors * Jonathan Soto * Kyle J. Williams * Edward Betts +* Robbe Gaeremynck Sponsors -------- diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a75363ad..723d8ee5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,14 +10,52 @@ and this project adheres to `Semantic Versioning = 5.43 - OS X / macOS >= 10.11 diff --git a/README.rst b/README.rst index 4644d4c2..ea720c45 100644 --- a/README.rst +++ b/README.rst @@ -79,7 +79,7 @@ Connect to a Bluetooth device and read its model number: from bleak import BleakClient address = "24:71:89:cc:09:05" - MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb" + MODEL_NBR_UUID = "2A24" async def main(address): async with BleakClient(address) as client: diff --git a/bleak/__init__.py b/bleak/__init__.py index aef9e976..28b5e3bc 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -16,6 +16,7 @@ import uuid from typing import ( TYPE_CHECKING, + AsyncGenerator, Awaitable, Callable, Dict, @@ -25,20 +26,25 @@ Set, Tuple, Type, + TypedDict, Union, overload, ) from warnings import warn +from typing import Literal + +if sys.version_info < (3, 12): + from typing_extensions import Buffer +else: + from collections.abc import Buffer if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout + from typing_extensions import Unpack else: from asyncio import timeout as async_timeout + from typing import Unpack -if sys.version_info[:2] < (3, 8): - from typing_extensions import Literal -else: - from typing import Literal from .backends.characteristic import BleakGATTCharacteristic from .backends.client import BaseBleakClient, get_platform_client_backend_type @@ -175,7 +181,17 @@ def register_detection_callback( FutureWarning, stacklevel=2, ) - self._backend.register_detection_callback(callback) + + try: + unregister = getattr(self, "_unregister_") + except AttributeError: + pass + else: + unregister() + + if callback is not None: + unregister = self._backend.register_detection_callback(callback) + setattr(self, "_unregister_", unregister) async def start(self): """Start scanning for devices""" @@ -204,6 +220,63 @@ def set_scanning_filter(self, **kwargs): ) self._backend.set_scanning_filter(**kwargs) + async def advertisement_data( + self, + ) -> AsyncGenerator[Tuple[BLEDevice, AdvertisementData], None]: + """ + Yields devices and associated advertising data packets as they are discovered. + + .. note:: + Ensure that scanning is started before calling this method. + + Returns: + An async iterator that yields tuples (:class:`BLEDevice`, :class:`AdvertisementData`). + + .. versionadded:: 0.21 + """ + devices = asyncio.Queue() + + unregister_callback = self._backend.register_detection_callback( + lambda bd, ad: devices.put_nowait((bd, ad)) + ) + try: + while True: + yield await devices.get() + finally: + unregister_callback() + + class ExtraArgs(TypedDict): + """ + Keyword args from :class:`~bleak.BleakScanner` that can be passed to + other convenience methods. + """ + + service_uuids: List[str] + """ + Optional list of service UUIDs to filter on. Only advertisements + containing this advertising data will be received. Required on + macOS >= 12.0, < 12.3 (unless you create an app with ``py2app``). + """ + scanning_mode: Literal["active", "passive"] + """ + Set to ``"passive"`` to avoid the ``"active"`` scanning mode. + Passive scanning is not supported on macOS! Will raise + :class:`BleakError` if set to ``"passive"`` on macOS. + """ + bluez: BlueZScannerArgs + """ + Dictionary of arguments specific to the BlueZ backend. + """ + cb: CBScannerArgs + """ + Dictionary of arguments specific to the CoreBluetooth backend. + """ + backend: Type[BaseBleakScanner] + """ + Used to override the automatically selected backend (i.e. for a + custom backend). + """ + @overload @classmethod async def discover( @@ -214,12 +287,14 @@ async def discover( @overload @classmethod async def discover( - cls, timeout: float = 5.0, *, return_adv: Literal[True] = True, **kwargs + cls, timeout: float = 5.0, *, return_adv: Literal[True], **kwargs ) -> Dict[str, Tuple[BLEDevice, AdvertisementData]]: ... @classmethod - async def discover(cls, timeout=5.0, *, return_adv=False, **kwargs): + async def discover( + cls, timeout=5.0, *, return_adv=False, **kwargs: Unpack[ExtraArgs] + ): """ Scan continuously for ``timeout`` seconds and return discovered devices. @@ -293,7 +368,7 @@ async def get_discovered_devices(self) -> List[BLEDevice]: @classmethod async def find_device_by_address( - cls, device_identifier: str, timeout: float = 10.0, **kwargs + cls, device_identifier: str, timeout: float = 10.0, **kwargs: Unpack[ExtraArgs] ) -> Optional[BLEDevice]: """Obtain a ``BLEDevice`` for a BLE server specified by Bluetooth address or (macOS) UUID address. @@ -315,7 +390,7 @@ async def find_device_by_address( @classmethod async def find_device_by_name( - cls, name: str, timeout: float = 10.0, **kwargs + cls, name: str, timeout: float = 10.0, **kwargs: Unpack[ExtraArgs] ) -> Optional[BLEDevice]: """Obtain a ``BLEDevice`` for a BLE server specified by the local name in the advertising data. @@ -337,7 +412,10 @@ async def find_device_by_name( @classmethod async def find_device_by_filter( - cls, filterfunc: AdvertisementDataFilter, timeout: float = 10.0, **kwargs + cls, + filterfunc: AdvertisementDataFilter, + timeout: float = 10.0, + **kwargs: Unpack[ExtraArgs], ) -> Optional[BLEDevice]: """Obtain a ``BLEDevice`` for a BLE server that matches a given filter function. @@ -360,16 +438,12 @@ async def find_device_by_filter( the timeout. """ - found_device_queue: asyncio.Queue[BLEDevice] = asyncio.Queue() - - def apply_filter(d: BLEDevice, ad: AdvertisementData): - if filterfunc(d, ad): - found_device_queue.put_nowait(d) - - async with cls(detection_callback=apply_filter, **kwargs): + async with cls(**kwargs) as scanner: try: async with async_timeout(timeout): - return await found_device_queue.get() + async for bd, ad in scanner.advertisement_data(): + if filterfunc(bd, ad): + return bd except asyncio.TimeoutError: return None @@ -639,24 +713,67 @@ async def read_gatt_char( async def write_gatt_char( self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - data: Union[bytes, bytearray, memoryview], - response: bool = False, + data: Buffer, + response: bool = None, ) -> None: """ Perform a write operation on the specified GATT characteristic. + There are two possible kinds of writes. *Write with response* (sometimes + called a *Request*) will write the data then wait for a response from + the remote device. *Write without response* (sometimes called *Command*) + will queue data to be written and return immediately. + + Each characteristic may support one kind or the other or both or neither. + Consult the device's documentation or inspect the properties of the + characteristic to find out which kind of writes are supported. + + .. tip:: Explicit is better than implicit. Best practice is to always + include an explicit ``response=True`` or ``response=False`` + when calling this method. + Args: char_specifier: The characteristic to write to, specified by either integer - handle, UUID or directly by the BleakGATTCharacteristic object - representing it. + handle, UUID or directly by the :class:`~bleak.backends.characteristic.BleakGATTCharacteristic` + object representing it. If a device has more than one characteristic + with the same UUID, then attempting to use the UUID wil fail and + a characteristic object must be used instead. data: - The data to send. + The data to send. When a write-with-response operation is used, + the length of the data is limited to 512 bytes. When a + write-without-response operation is used, the length of the + data is limited to :attr:`~bleak.backends.characteristic.BleakGATTCharacteristic.max_write_without_response_size`. + Any type that supports the buffer protocol can be passed. response: - If write-with-response operation should be done. Defaults to ``False``. + If ``True``, a write-with-response operation will be used. If + ``False``, a write-without-response operation will be used. + If omitted or ``None``, the "best" operation will be used + based on the reported properties of the characteristic. + + .. versionchanged:: 0.21 + The default behavior when ``response=`` is omitted was changed. + Example:: + + MY_CHAR_UUID = "1234" + ... + await client.write_gatt_char(MY_CHAR_UUID, b"\x00\x01\x02\x03", response=True) """ - await self._backend.write_gatt_char(char_specifier, data, response) + if isinstance(char_specifier, BleakGATTCharacteristic): + characteristic = char_specifier + else: + characteristic = self.services.get_characteristic(char_specifier) + + if not characteristic: + raise BleakError("Characteristic {char_specifier} was not found!") + + if response is None: + # if not specified, prefer write-with-response over write-without- + # response if it is available since it is the more reliable write. + response = "write" in characteristic.properties + + await self._backend.write_gatt_char(characteristic, data, response) async def start_notify( self, @@ -747,9 +864,7 @@ async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: """ return await self._backend.read_gatt_descriptor(handle, **kwargs) - async def write_gatt_descriptor( - self, handle: int, data: Union[bytes, bytearray, memoryview] - ) -> None: + async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: """ Perform a write operation on the specified GATT descriptor. diff --git a/bleak/backends/bluezdbus/advertisement_monitor.py b/bleak/backends/bluezdbus/advertisement_monitor.py index 9bdee086..83de82f1 100644 --- a/bleak/backends/bluezdbus/advertisement_monitor.py +++ b/bleak/backends/bluezdbus/advertisement_monitor.py @@ -75,12 +75,14 @@ def Activate(self): @method() @no_type_check def DeviceFound(self, device: "o"): # noqa: F821 - logger.debug("DeviceFound %s", device) + if logger.isEnabledFor(logging.DEBUG): + logger.debug("DeviceFound %s", device) @method() @no_type_check def DeviceLost(self, device: "o"): # noqa: F821 - logger.debug("DeviceLost %s", device) + if logger.isEnabledFor(logging.DEBUG): + logger.debug("DeviceLost %s", device) @dbus_property(PropertyAccess.READ) @no_type_check diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 127a7f94..d1ee0733 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -10,6 +10,11 @@ from typing import Callable, Dict, Optional, Set, Union, cast from uuid import UUID +if sys.version_info < (3, 12): + from typing_extensions import Buffer +else: + from collections.abc import Buffer + if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout else: @@ -150,7 +155,7 @@ async def connect(self, dangerous_use_bleak_cache: bool = False, **kwargs) -> bo def on_connected_changed(connected: bool) -> None: if not connected: - logger.debug(f"Device disconnected ({self._device_path})") + logger.debug("Device disconnected (%s)", self._device_path) self._is_connected = False @@ -340,14 +345,14 @@ def _cleanup_all(self) -> None: Free all the allocated resource in DBus. Use this method to eventually cleanup all otherwise leaked resources. """ - logger.debug(f"_cleanup_all({self._device_path})") + logger.debug("_cleanup_all(%s)", self._device_path) if self._remove_device_watcher: self._remove_device_watcher() self._remove_device_watcher = None if not self._bus: - logger.debug(f"already disconnected ({self._device_path})") + logger.debug("already disconnected (%s)", self._device_path) return # Try to disconnect the System Bus. @@ -355,7 +360,9 @@ def _cleanup_all(self) -> None: self._bus.disconnect() except Exception as e: logger.error( - f"Attempt to disconnect system bus failed ({self._device_path}): {e}" + "Attempt to disconnect system bus failed (%s): %s", + self._device_path, + e, ) else: # Critical to remove the `self._bus` object here to since it was @@ -376,18 +383,18 @@ async def disconnect(self) -> bool: BleakDBusError: If there was a D-Bus error asyncio.TimeoutError if the device was not disconnected within 10 seconds """ - logger.debug(f"Disconnecting ({self._device_path})") + logger.debug("Disconnecting ({%s})", self._device_path) if self._bus is None: # No connection exists. Either one hasn't been created or # we have already called disconnect and closed the D-Bus # connection. - logger.debug(f"already disconnected ({self._device_path})") + logger.debug("already disconnected ({%s})", self._device_path) return True if self._disconnecting_event: # another call to disconnect() is already in progress - logger.debug(f"already in progress ({self._device_path})") + logger.debug("already in progress ({%s})", self._device_path) async with async_timeout(10): await self._disconnecting_event.wait() elif self.is_connected: @@ -804,73 +811,22 @@ async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: value = bytearray(reply.body[0]) - logger.debug( - "Read Descriptor {0} | {1}: {2}".format(handle, descriptor.path, value) - ) + logger.debug("Read Descriptor %s | %s: %s", handle, descriptor.path, value) return value async def write_gatt_char( self, - char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID], - data: Union[bytes, bytearray, memoryview], - response: bool = False, + characteristic: BleakGATTCharacteristic, + data: Buffer, + response: bool, ) -> None: - """Perform a write operation on the specified GATT characteristic. - - .. note:: - - The version check below is for the "type" option to the - "Characteristic.WriteValue" method that was added to `Bluez in 5.51 - `_ - Before that commit, ``Characteristic.WriteValue`` was only "Write with - response". ``Characteristic.AcquireWrite`` was `added in Bluez 5.46 - `_ - which can be used to "Write without response", but for older versions - of Bluez, it is not possible to "Write without response". - - Args: - char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to write - to, specified by either integer handle, UUID or directly by the - BleakGATTCharacteristicBlueZDBus object representing it. - data (bytes or bytearray): The data to send. - response (bool): If write-with-response operation should be done. Defaults to `False`. - - """ if not self.is_connected: raise BleakError("Not connected") - if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - - if not characteristic: - raise BleakError("Characteristic {0} was not found!".format(char_specifier)) - if ( - "write" not in characteristic.properties - and "write-without-response" not in characteristic.properties - ): - raise BleakError( - "Characteristic %s does not support write operations!" - % str(characteristic.uuid) - ) - if not response and "write-without-response" not in characteristic.properties: - response = True - # Force response here, since the device only supports that. - if ( - response - and "write" not in characteristic.properties - and "write-without-response" in characteristic.properties - ): - response = False - logger.warning( - "Characteristic %s does not support Write with response. Trying without..." - % str(characteristic.uuid) - ) - # See docstring for details about this handling. if not response and not BlueZFeatures.can_write_without_response: raise BleakError("Write without response requires at least BlueZ 5.46") + if response or not BlueZFeatures.write_without_response_workaround_needed: while True: assert self._bus @@ -926,19 +882,18 @@ async def write_gatt_char( os.close(fd) logger.debug( - "Write Characteristic {0} | {1}: {2}".format( - characteristic.uuid, characteristic.path, data - ) + "Write Characteristic %s | %s: %s", + characteristic.uuid, + characteristic.path, + data, ) - async def write_gatt_descriptor( - self, handle: int, data: Union[bytes, bytearray, memoryview] - ) -> None: + async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: """Perform a write operation on the specified GATT descriptor. Args: - handle (int): The handle of the descriptor to read from. - data (bytes or bytearray): The data to send. + handle: The handle of the descriptor to read from. + data: The data to send (any bytes-like object). """ if not self.is_connected: diff --git a/bleak/backends/bluezdbus/defs.py b/bleak/backends/bluezdbus/defs.py index 8ec19af2..50054540 100644 --- a/bleak/backends/bluezdbus/defs.py +++ b/bleak/backends/bluezdbus/defs.py @@ -1,12 +1,6 @@ # -*- coding: utf-8 -*- -import sys -from typing import Dict, List, Tuple - -if sys.version_info[:2] < (3, 8): - from typing_extensions import Literal, TypedDict -else: - from typing import Literal, TypedDict +from typing import Dict, List, Literal, Tuple, TypedDict # DBus Interfaces OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager" diff --git a/bleak/backends/bluezdbus/manager.py b/bleak/backends/bluezdbus/manager.py index dc49f6d7..325cd43a 100644 --- a/bleak/backends/bluezdbus/manager.py +++ b/bleak/backends/bluezdbus/manager.py @@ -7,6 +7,7 @@ """ import asyncio +import contextlib import logging import os from typing import ( @@ -36,7 +37,11 @@ from .descriptor import BleakGATTDescriptorBlueZDBus from .service import BleakGATTServiceBlueZDBus from .signals import MatchRules, add_match -from .utils import assert_reply, get_dbus_authenticator +from .utils import ( + assert_reply, + get_dbus_authenticator, + device_path_from_characteristic_path, +) logger = logging.getLogger(__name__) @@ -66,6 +71,31 @@ class CallbackAndState(NamedTuple): """ +DevicePropertiesChangedCallback = Callable[[Optional[Any]], None] +""" +A callback that is called when the properties of a device change in BlueZ. + +Args: + arg0: The new property value. +""" + + +class DeviceConditionCallback(NamedTuple): + """ + Encapsulates a :data:`DevicePropertiesChangedCallback` and the property name being watched. + """ + + callback: DevicePropertiesChangedCallback + """ + The callback. + """ + + property_name: str + """ + The name of the property to watch. + """ + + DeviceRemovedCallback = Callable[[str], None] """ A callback that is called when a device is removed from BlueZ. @@ -110,7 +140,6 @@ class DeviceRemovedCallbackAndState(NamedTuple): class DeviceWatcher(NamedTuple): - device_path: str """ The D-Bus object path of the device. @@ -167,10 +196,26 @@ def __init__(self): self._advertisement_callbacks: List[CallbackAndState] = [] self._device_removed_callbacks: List[DeviceRemovedCallbackAndState] = [] - self._device_watchers: Set[DeviceWatcher] = set() - self._condition_callbacks: Set[Callable] = set() + self._device_watchers: Dict[str, Set[DeviceWatcher]] = {} + self._condition_callbacks: Dict[str, Set[DeviceConditionCallback]] = {} self._services_cache: Dict[str, BleakGATTServiceCollection] = {} + def _check_adapter(self, adapter_path: str) -> None: + """ + Raises: + BleakError: if adapter is not present in BlueZ + """ + if adapter_path not in self._properties: + raise BleakError(f"adapter '{adapter_path.split('/')[-1]}' not found") + + def _check_device(self, device_path: str) -> None: + """ + Raises: + BleakError: if device is not present in BlueZ + """ + if device_path not in self._properties: + raise BleakError(f"device '{device_path.split('/')[-1]}' not found") + async def async_init(self): """ Connects to the D-Bus message bus and begins monitoring signals. @@ -274,7 +319,8 @@ async def async_init(self): desc_props["Characteristic"], set() ).add(path) - logger.debug(f"initial properties: {self._properties}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug("initial properties: %s", self._properties) except BaseException: # if setup failed, disconnect @@ -326,13 +372,15 @@ async def active_scan( Returns: An async function that is used to stop scanning and remove the filters. + + Raises: + BleakError: if the adapter is not present in BlueZ """ async with self._bus_lock: # If the adapter doesn't exist, then the message calls below would # fail with "method not found". This provides a more informative # error message. - if adapter_path not in self._properties: - raise BleakError(f"adapter '{adapter_path.split('/')[-1]}' not found") + self._check_adapter(adapter_path) callback_and_state = CallbackAndState(advertisement_callback, adapter_path) self._advertisement_callbacks.append(callback_and_state) @@ -432,13 +480,15 @@ async def passive_scan( Returns: An async function that is used to stop scanning and remove the filters. + + Raises: + BleakError: if the adapter is not present in BlueZ """ async with self._bus_lock: # If the adapter doesn't exist, then the message calls below would # fail with "method not found". This provides a more informative # error message. - if adapter_path not in self._properties: - raise BleakError(f"adapter '{adapter_path.split('/')[-1]}' not found") + self._check_adapter(adapter_path) callback_and_state = CallbackAndState(advertisement_callback, adapter_path) self._advertisement_callbacks.append(callback_and_state) @@ -534,12 +584,17 @@ def add_device_watcher( Returns: A device watcher object that acts a token to unregister the watcher. + + Raises: + BleakError: if the device is not present in BlueZ """ + self._check_device(device_path) + watcher = DeviceWatcher( device_path, on_connected_changed, on_characteristic_value_changed ) - self._device_watchers.add(watcher) + self._device_watchers.setdefault(device_path, set()).add(watcher) return watcher def remove_device_watcher(self, watcher: DeviceWatcher) -> None: @@ -550,7 +605,10 @@ def remove_device_watcher(self, watcher: DeviceWatcher) -> None: The device watcher token that was returned by :meth:`add_device_watcher`. """ - self._device_watchers.remove(watcher) + device_path = watcher.device_path + self._device_watchers[device_path].remove(watcher) + if not self._device_watchers[device_path]: + del self._device_watchers[device_path] async def get_services( self, device_path: str, use_cached: bool, requested_services: Optional[Set[str]] @@ -571,7 +629,12 @@ async def get_services( Returns: A new :class:`BleakGATTServiceCollection`. + + Raises: + BleakError: if the device is not present in BlueZ """ + self._check_device(device_path) + if use_cached: services = self._services_cache.get(device_path) if services is not None: @@ -644,7 +707,12 @@ def get_device_name(self, device_path: str) -> str: Returns: The current property value. + + Raises: + BleakError: if the device is not present in BlueZ """ + self._check_device(device_path) + return self._properties[device_path][defs.DEVICE_INTERFACE]["Name"] def is_connected(self, device_path: str) -> bool: @@ -655,7 +723,7 @@ def is_connected(self, device_path: str) -> bool: device_path: The D-Bus object path of the device. Returns: - The current property value. + The current property value or ``False`` if the device does not exist in BlueZ. """ try: return self._properties[device_path][defs.DEVICE_INTERFACE]["Connected"] @@ -667,23 +735,76 @@ async def _wait_for_services_discovery(self, device_path: str) -> None: Waits for the device services to be discovered. If a disconnect happens before the completion a BleakError exception is raised. + + Raises: + BleakError: if the device is not present in BlueZ """ - services_discovered_wait_task = asyncio.create_task( - self._wait_condition(device_path, "ServicesResolved", True) - ) - device_disconnected_wait_task = asyncio.create_task( - self._wait_condition(device_path, "Connected", False) - ) - done, pending = await asyncio.wait( - {services_discovered_wait_task, device_disconnected_wait_task}, - return_when=asyncio.FIRST_COMPLETED, - ) + self._check_device(device_path) - for p in pending: - p.cancel() + with contextlib.ExitStack() as stack: + services_discovered_wait_task = asyncio.create_task( + self._wait_condition(device_path, "ServicesResolved", True) + ) + stack.callback(services_discovered_wait_task.cancel) + + device_disconnected_wait_task = asyncio.create_task( + self._wait_condition(device_path, "Connected", False) + ) + stack.callback(device_disconnected_wait_task.cancel) + + # in some cases, we can get "InterfaceRemoved" without the + # "Connected" property changing, so we need to race against both + # conditions + device_removed_wait_task = asyncio.create_task( + self._wait_removed(device_path) + ) + stack.callback(device_removed_wait_task.cancel) + + done, _ = await asyncio.wait( + { + services_discovered_wait_task, + device_disconnected_wait_task, + device_removed_wait_task, + }, + return_when=asyncio.FIRST_COMPLETED, + ) - if device_disconnected_wait_task in done: - raise BleakError("failed to discover services, device disconnected") + # check for exceptions + for task in done: + task.result() + + if not done.isdisjoint( + {device_disconnected_wait_task, device_removed_wait_task} + ): + raise BleakError("failed to discover services, device disconnected") + + async def _wait_removed(self, device_path: str) -> None: + """ + Waits for the device interface to be removed. + + If the device is not present in BlueZ, this returns immediately. + + Args: + device_path: The D-Bus object path of a Bluetooth device. + """ + if device_path not in self._properties: + return + + event = asyncio.Event() + + def callback(_: str): + event.set() + + device_removed_callback_and_state = DeviceRemovedCallbackAndState( + callback, self._properties[device_path][defs.DEVICE_INTERFACE]["Adapter"] + ) + + with contextlib.ExitStack() as stack: + self._device_removed_callbacks.append(device_removed_callback_and_state) + stack.callback( + self._device_removed_callbacks.remove, device_removed_callback_and_state + ) + await event.wait() async def _wait_condition( self, device_path: str, property_name: str, property_value: Any @@ -695,7 +816,12 @@ async def _wait_condition( device_path: The D-Bus object path of a Bluetooth device. property_name: The name of the property to test. property_value: A value to compare the current property value to. + + Raises: + BleakError: if the device is not present in BlueZ """ + self._check_device(device_path) + if ( self._properties[device_path][defs.DEVICE_INTERFACE][property_name] == property_value @@ -704,20 +830,23 @@ async def _wait_condition( event = asyncio.Event() - def callback(): - if ( - self._properties[device_path][defs.DEVICE_INTERFACE][property_name] - == property_value - ): + def _wait_condition_callback(new_value: Optional[Any]) -> None: + """Callback for when a property changes.""" + if new_value == property_value: event.set() - self._condition_callbacks.add(callback) + condition_callbacks = self._condition_callbacks + device_callbacks = condition_callbacks.setdefault(device_path, set()) + callback = DeviceConditionCallback(_wait_condition_callback, property_name) + device_callbacks.add(callback) try: # can be canceled await event.wait() finally: - self._condition_callbacks.remove(callback) + device_callbacks.remove(callback) + if not device_callbacks: + del condition_callbacks[device_path] def _parse_msg(self, message: Message): """ @@ -727,13 +856,14 @@ def _parse_msg(self, message: Message): if message.message_type != MessageType.SIGNAL: return - logger.debug( - "received D-Bus signal: %s.%s (%s): %s", - message.interface, - message.member, - message.path, - message.body, - ) + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + "received D-Bus signal: %s.%s (%s): %s", + message.interface, + message.member, + message.path, + message.body, + ) # type hints obj_path: str @@ -813,9 +943,9 @@ def _parse_msg(self, message: Message): except KeyError: pass elif message.member == "PropertiesChanged": - assert message.path is not None - interface, changed, invalidated = message.body + message_path = message.path + assert message_path is not None try: self_interface = self._properties[message.path][interface] @@ -844,34 +974,40 @@ def _parse_msg(self, message: Message): if interface == defs.DEVICE_INTERFACE: # handle advertisement watchers + device_path = message_path self._run_advertisement_callbacks( - message.path, cast(Device1, self_interface), changed.keys() + device_path, cast(Device1, self_interface), changed.keys() ) # handle device condition watchers - for condition_callback in self._condition_callbacks: - condition_callback() + callbacks = self._condition_callbacks.get(device_path) + if callbacks: + for callback in callbacks: + name = callback.property_name + if name in changed: + callback.callback(self_interface.get(name)) # handle device connection change watchers - if "Connected" in changed: - for ( - device_path, - on_connected_changed, - _, - ) in self._device_watchers.copy(): - # callbacks may remove the watcher, hence the copy() above - if message.path == device_path: - on_connected_changed(self_interface["Connected"]) + new_connected = self_interface["Connected"] + watchers = self._device_watchers.get(device_path) + if watchers: + # callbacks may remove the watcher, hence the copy + for watcher in watchers.copy(): + watcher.on_connected_changed(new_connected) elif interface == defs.GATT_CHARACTERISTIC_INTERFACE: # handle characteristic value change watchers - if "Value" in changed: - for device_path, _, on_value_changed in self._device_watchers: - if message.path.startswith(device_path): - on_value_changed(message.path, self_interface["Value"]) + new_value = self_interface["Value"] + device_path = device_path_from_characteristic_path(message_path) + watchers = self._device_watchers.get(device_path) + if watchers: + for watcher in watchers: + watcher.on_characteristic_value_changed( + message_path, new_value + ) def _run_advertisement_callbacks( self, device_path: str, device: Device1, changed: Iterable[str] @@ -884,7 +1020,7 @@ def _run_advertisement_callbacks( device: The current D-Bus properties of the device. changed: A list of properties that have changed since the last call. """ - for (callback, adapter_path) in self._advertisement_callbacks: + for callback, adapter_path in self._advertisement_callbacks: # filter messages from other adapters if adapter_path != device["Adapter"]: continue diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 6500fcdd..24591398 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -1,15 +1,9 @@ import logging -import sys -from typing import Callable, Coroutine, Dict, List, Optional +from typing import Callable, Coroutine, Dict, List, Literal, Optional, TypedDict from warnings import warn from dbus_fast import Variant -if sys.version_info[:2] < (3, 8): - from typing_extensions import Literal, TypedDict -else: - from typing import Literal, TypedDict - from ...exc import BleakError from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner from .advertisement_monitor import OrPatternLike @@ -273,10 +267,7 @@ def _handle_advertising_data(self, path: str, props: Device1) -> None: advertisement_data, ) - if self._callback is None: - return - - self._callback(device, advertisement_data) + self.call_detection_callbacks(device, advertisement_data) def _handle_device_removed(self, device_path: str) -> None: """ diff --git a/bleak/backends/bluezdbus/utils.py b/bleak/backends/bluezdbus/utils.py index 5b0eeeb8..c8547dd8 100644 --- a/bleak/backends/bluezdbus/utils.py +++ b/bleak/backends/bluezdbus/utils.py @@ -47,6 +47,20 @@ def bdaddr_from_device_path(device_path: str) -> str: return ":".join(device_path[-17:].split("_")) +def device_path_from_characteristic_path(characteristic_path: str) -> str: + """ + Scrape the device path from a D-Bus characteristic path. + + Args: + characteristic_path: The D-Bus object path of the characteristic. + + Returns: + A D-Bus object path of the device. + """ + # /org/bluez/hci1/dev_FA_23_9D_AA_45_46/service000c/char000d + return characteristic_path[:37] + + def get_dbus_authenticator(): uid = None try: diff --git a/bleak/backends/client.py b/bleak/backends/client.py index 5b7041c1..307753e5 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -9,10 +9,16 @@ import asyncio import os import platform +import sys import uuid from typing import Callable, Optional, Type, Union from warnings import warn +if sys.version_info < (3, 12): + from typing_extensions import Buffer +else: + from collections.abc import Buffer + from ..exc import BleakError from .service import BleakGATTServiceCollection from .characteristic import BleakGATTCharacteristic @@ -183,31 +189,27 @@ async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: @abc.abstractmethod async def write_gatt_char( self, - char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - data: Union[bytes, bytearray, memoryview], - response: bool = False, + characteristic: BleakGATTCharacteristic, + data: Buffer, + response: bool, ) -> None: - """Perform a write operation on the specified GATT characteristic. + """ + Perform a write operation on the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write - to, specified by either integer handle, UUID or directly by the - BleakGATTCharacteristic object representing it. - data (bytes or bytearray): The data to send. - response (bool): If write-with-response operation should be done. Defaults to `False`. - + characteristic: The characteristic to write to. + data: The data to send. + response: If write-with-response operation should be done. """ raise NotImplementedError() @abc.abstractmethod - async def write_gatt_descriptor( - self, handle: int, data: Union[bytes, bytearray, memoryview] - ) -> None: + async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: """Perform a write operation on the specified GATT descriptor. Args: - handle (int): The handle of the descriptor to read from. - data (bytes or bytearray): The data to send. + handle: The handle of the descriptor to read from. + data: The data to send (any bytes-like object). """ raise NotImplementedError() diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 45781a04..c27aeabf 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -5,9 +5,15 @@ """ import asyncio import logging +import sys import uuid from typing import Optional, Set, Union +if sys.version_info < (3, 12): + from typing_extensions import Buffer +else: + from collections.abc import Buffer + from CoreBluetooth import ( CBUUID, CBCharacteristicWriteWithoutResponse, @@ -307,27 +313,10 @@ async def read_gatt_descriptor( async def write_gatt_char( self, - char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - data: Union[bytes, bytearray, memoryview], - response: bool = False, + characteristic: BleakGATTCharacteristic, + data: Buffer, + response: bool, ) -> None: - """Perform a write operation of the specified GATT characteristic. - - Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write - to, specified by either integer handle, UUID or directly by the - BleakGATTCharacteristic object representing it. - data (bytes or bytearray): The data to send. - response (bool): If write-with-response operation should be done. Defaults to `False`. - - """ - if not isinstance(char_specifier, BleakGATTCharacteristic): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - if not characteristic: - raise BleakError("Characteristic {} was not found!".format(char_specifier)) - value = NSData.alloc().initWithBytes_length_(data, len(data)) await self._delegate.write_characteristic( characteristic.obj, @@ -338,14 +327,12 @@ async def write_gatt_char( ) logger.debug(f"Write Characteristic {characteristic.uuid} : {data}") - async def write_gatt_descriptor( - self, handle: int, data: Union[bytes, bytearray, memoryview] - ) -> None: + async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: """Perform a write operation on the specified GATT descriptor. Args: - handle (int): The handle of the descriptor to read from. - data (bytes or bytearray): The data to send. + handle: The handle of the descriptor to read from. + data: The data to send (any bytes-like object). """ descriptor = self.services.get_descriptor(handle) diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index b23803e8..e6d20c2d 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -1,11 +1,5 @@ import logging -import sys -from typing import Any, Dict, List, Optional - -if sys.version_info[:2] < (3, 8): - from typing_extensions import Literal, TypedDict -else: - from typing import Literal, TypedDict +from typing import Any, Dict, List, Literal, Optional, TypedDict import objc from CoreBluetooth import CBPeripheral @@ -29,7 +23,8 @@ class CBScannerArgs(TypedDict, total=False): If true, use Bluetooth address instead of UUID. .. warning:: This uses an undocumented IOBluetooth API to get the Bluetooth - address and may break in the future macOS releases. + address and may break in the future macOS releases. `It is known to not + work on macOS 10.15 `_. """ @@ -146,10 +141,7 @@ def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None: advertisement_data, ) - if not self._callback: - return - - self._callback(device, advertisement_data) + self.call_detection_callbacks(device, advertisement_data) self._manager.callbacks[id(self)] = callback await self._manager.start_scan(self._service_uuids) diff --git a/bleak/backends/descriptor.py b/bleak/backends/descriptor.py index cf584937..828ead56 100644 --- a/bleak/backends/descriptor.py +++ b/bleak/backends/descriptor.py @@ -8,94 +8,94 @@ import abc from typing import Any -from ..uuids import normalize_uuid_str +from ..uuids import normalize_uuid_16 _descriptor_descriptions = { - normalize_uuid_str("2905"): [ + normalize_uuid_16(0x2905): [ "Characteristic Aggregate Format", "org.bluetooth.descriptor.gatt.characteristic_aggregate_format", "0x2905", "GSS", ], - normalize_uuid_str("2900"): [ + normalize_uuid_16(0x2900): [ "Characteristic Extended Properties", "org.bluetooth.descriptor.gatt.characteristic_extended_properties", "0x2900", "GSS", ], - normalize_uuid_str("2904"): [ + normalize_uuid_16(0x2904): [ "Characteristic Presentation Format", "org.bluetooth.descriptor.gatt.characteristic_presentation_format", "0x2904", "GSS", ], - normalize_uuid_str("2901"): [ + normalize_uuid_16(0x2901): [ "Characteristic User Description", "org.bluetooth.descriptor.gatt.characteristic_user_description", "0x2901", "GSS", ], - normalize_uuid_str("2902"): [ + normalize_uuid_16(0x2902): [ "Client Characteristic Configuration", "org.bluetooth.descriptor.gatt.client_characteristic_configuration", "0x2902", "GSS", ], - normalize_uuid_str("290B"): [ + normalize_uuid_16(0x290B): [ "Environmental Sensing Configuration", "org.bluetooth.descriptor.es_configuration", "0x290B", "GSS", ], - normalize_uuid_str("290C"): [ + normalize_uuid_16(0x290C): [ "Environmental Sensing Measurement", "org.bluetooth.descriptor.es_measurement", "0x290C", "GSS", ], - normalize_uuid_str("290d"): [ + normalize_uuid_16(0x290D): [ "Environmental Sensing Trigger Setting", "org.bluetooth.descriptor.es_trigger_setting", "0x290D", "GSS", ], - normalize_uuid_str("2907"): [ + normalize_uuid_16(0x2907): [ "External Report Reference", "org.bluetooth.descriptor.external_report_reference", "0x2907", "GSS", ], - normalize_uuid_str("2909"): [ + normalize_uuid_16(0x2909): [ "Number of Digitals", "org.bluetooth.descriptor.number_of_digitals", "0x2909", "GSS", ], - normalize_uuid_str("2908"): [ + normalize_uuid_16(0x2908): [ "Report Reference", "org.bluetooth.descriptor.report_reference", "0x2908", "GSS", ], - normalize_uuid_str("2903"): [ + normalize_uuid_16(0x2903): [ "Server Characteristic Configuration", "org.bluetooth.descriptor.gatt.server_characteristic_configuration", "0x2903", "GSS", ], - normalize_uuid_str("290E"): [ + normalize_uuid_16(0x290E): [ "Time Trigger Setting", "org.bluetooth.descriptor.time_trigger_setting", "0x290E", "GSS", ], - normalize_uuid_str("2906"): [ + normalize_uuid_16(0x2906): [ "Valid Range", "org.bluetooth.descriptor.valid_range", "0x2906", "GSS", ], - normalize_uuid_str("290A"): [ + normalize_uuid_16(0x290A): [ "Value Trigger Setting", "org.bluetooth.descriptor.value_trigger_setting", "0x290A", diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index 6eceec28..4bab6913 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -370,49 +370,10 @@ async def read_gatt_descriptor( async def write_gatt_char( self, - char_specifier: Union[BleakGATTCharacteristicP4Android, int, str, uuid.UUID], + characteristic: BleakGATTCharacteristic, data: bytearray, - response: bool = False, + response: bool, ) -> None: - """Perform a write operation on the specified GATT characteristic. - - Args: - char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to write - to, specified by either integer handle, UUID or directly by the - BleakGATTCharacteristicP4Android object representing it. - data (bytes or bytearray): The data to send. - response (bool): If write-with-response operation should be done. Defaults to `False`. - - """ - if not isinstance(char_specifier, BleakGATTCharacteristicP4Android): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - - if not characteristic: - raise BleakError(f"Characteristic {char_specifier} was not found!") - - if ( - "write" not in characteristic.properties - and "write-without-response" not in characteristic.properties - ): - raise BleakError( - f"Characteristic {str(characteristic.uuid)} does not support write operations!" - ) - if not response and "write-without-response" not in characteristic.properties: - response = True - # Force response here, since the device only supports that. - if ( - response - and "write" not in characteristic.properties - and "write-without-response" in characteristic.properties - ): - response = False - logger.warning( - "Characteristic %s does not support Write with response. Trying without..." - % str(characteristic.uuid) - ) - if response: characteristic.obj.setWriteType( defs.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT diff --git a/bleak/backends/p4android/defs.py b/bleak/backends/p4android/defs.py index c097553b..0fbee94d 100644 --- a/bleak/backends/p4android/defs.py +++ b/bleak/backends/p4android/defs.py @@ -86,4 +86,4 @@ class ScanFailed(enum.IntEnum): BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE: "write-without-response", } -CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = "00002902-0000-1000-8000-00805f9b34fb" +CLIENT_CHARACTERISTIC_CONFIGURATION_UUID = "2902" diff --git a/bleak/backends/p4android/recipes/bleak/__init__.py b/bleak/backends/p4android/recipes/bleak/__init__.py index c8e2b30c..1e9c17d3 100644 --- a/bleak/backends/p4android/recipes/bleak/__init__.py +++ b/bleak/backends/p4android/recipes/bleak/__init__.py @@ -1,3 +1,5 @@ +import os + from pythonforandroid.recipe import PythonRecipe from pythonforandroid.toolchain import shprint, info import sh @@ -5,23 +7,35 @@ class BleakRecipe(PythonRecipe): - version = None - url = None + version = None # Must be none for p4a to correctly clone repo + fix_setup_py_version = "bleak develop branch" + url = "git+https://github.com/hbldh/bleak.git" name = "bleak" - src_filename = join("..", "..", "..", "..", "..") - depends = ["pyjnius"] call_hostpython_via_targetpython = False + fix_setup_filename = "fix_setup.py" + def prepare_build_dir(self, arch): - shprint(sh.rm, "-rf", self.get_build_dir(arch)) - shprint( - sh.ln, - "-s", - join(self.get_recipe_dir(), self.src_filename), - self.get_build_dir(arch), - ) + super().prepare_build_dir(arch) # Unpack the url file to the get_build_dir + build_dir = self.get_build_dir(arch) + + setup_py_path = join(build_dir, "setup.py") + if not os.path.exists(setup_py_path): + # Perform the p4a temporary fix + # At the moment, p4a recipe installing requires setup.py to be present + # So, we create a setup.py file only for android + + fix_setup_py_path = join(self.get_recipe_dir(), self.fix_setup_filename) + with open(fix_setup_py_path, "r") as f: + contents = f.read() + + # Write to the correct location and fill in the version number + with open(setup_py_path, "w") as f: + f.write(contents.replace("[VERSION]", self.fix_setup_py_version)) + else: + info("setup.py found in bleak directory, are you installing older version?") def get_recipe_env(self, arch=None, with_flags_in_cc=True): env = super().get_recipe_env(arch, with_flags_in_cc) @@ -33,13 +47,12 @@ def postbuild_arch(self, arch): super().postbuild_arch(arch) info("Copying java files") - - destdir = self.ctx.javaclass_dir + dest_dir = self.ctx.javaclass_dir path = join( self.get_build_dir(arch.arch), "bleak", "backends", "p4android", "java", "." ) - shprint(sh.cp, "-a", path, destdir) + shprint(sh.cp, "-a", path, dest_dir) recipe = BleakRecipe() diff --git a/bleak/backends/p4android/recipes/bleak/fix_setup.py b/bleak/backends/p4android/recipes/bleak/fix_setup.py new file mode 100644 index 00000000..b43d2c13 --- /dev/null +++ b/bleak/backends/p4android/recipes/bleak/fix_setup.py @@ -0,0 +1,10 @@ +from setuptools import find_packages, setup + +VERSION = "[VERSION]" # Version will be filled in by the bleak recipe +NAME = "bleak" + +setup( + name=NAME, + version=VERSION, + packages=find_packages(exclude=("tests", "examples", "docs")), +) diff --git a/bleak/backends/p4android/scanner.py b/bleak/backends/p4android/scanner.py index 2a69a7b2..e53662f6 100644 --- a/bleak/backends/p4android/scanner.py +++ b/bleak/backends/p4android/scanner.py @@ -4,18 +4,13 @@ import logging import sys import warnings -from typing import List, Optional +from typing import List, Literal, Optional if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout else: from asyncio import timeout as async_timeout -if sys.version_info[:2] < (3, 8): - from typing_extensions import Literal -else: - from typing import Literal - from android.broadcast import BroadcastReceiver from android.permissions import Permission, request_permissions from jnius import cast, java_method @@ -271,10 +266,7 @@ def _handle_scan_result(self, result) -> None: advertisement, ) - if not self._callback: - return - - self._callback(device, advertisement) + self.call_detection_callbacks(device, advertisement) class _PythonScanCallback(utils.AsyncJavaCallbacks): diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 0ae33641..60396fa7 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -5,9 +5,10 @@ import platform from typing import ( Any, - Awaitable, Callable, + Coroutine, Dict, + Hashable, List, NamedTuple, Optional, @@ -91,7 +92,7 @@ def __repr__(self) -> str: AdvertisementDataCallback = Callable[ [BLEDevice, AdvertisementData], - Optional[Awaitable[None]], + Optional[Coroutine[Any, Any, None]], ] """ Type alias for callback called when advertisement data is received. @@ -134,8 +135,17 @@ def __init__( service_uuids: Optional[List[str]], ): super(BaseBleakScanner, self).__init__() - self._callback: Optional[AdvertisementDataCallback] = None - self.register_detection_callback(detection_callback) + + self._ad_callbacks: Dict[ + Hashable, Callable[[BLEDevice, AdvertisementData], None] + ] = {} + """ + List of callbacks to call when an advertisement is received. + """ + + if detection_callback is not None: + self.register_detection_callback(detection_callback) + self._service_uuids: Optional[List[str]] = ( [u.lower() for u in service_uuids] if service_uuids is not None else None ) @@ -144,11 +154,10 @@ def __init__( def register_detection_callback( self, callback: Optional[AdvertisementDataCallback] - ) -> None: - """Register a callback that is called when a device is discovered or has a property changed. - - If another callback has already been registered, it will be replaced with ``callback``. - ``None`` can be used to remove the current callback. + ) -> Callable[[], None]: + """ + Register a callback that is called when an advertisement event from the + OS is received. The ``callback`` is a function or coroutine that takes two arguments: :class:`BLEDevice` and :class:`AdvertisementData`. @@ -156,15 +165,18 @@ def register_detection_callback( Args: callback: A function, coroutine or ``None``. + Returns: + A method that can be called to unregister the callback. """ - if callback is not None: - error_text = "callback must be callable with 2 parameters" - if not callable(callback): - raise TypeError(error_text) + error_text = "callback must be callable with 2 parameters" + + if not callable(callback): + raise TypeError(error_text) - handler_signature = inspect.signature(callback) - if len(handler_signature.parameters) != 2: - raise TypeError(error_text) + handler_signature = inspect.signature(callback) + + if len(handler_signature.parameters) != 2: + raise TypeError(error_text) if inspect.iscoroutinefunction(callback): @@ -176,7 +188,26 @@ def detection_callback(s, d): else: detection_callback = callback - self._callback = detection_callback + token = object() + + self._ad_callbacks[token] = detection_callback + + def remove(): + self._ad_callbacks.pop(token, None) + + return remove + + def call_detection_callbacks( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """ + Calls all registered detection callbacks. + + Backend implementations should call this method when an advertisement + event is received from the OS. + """ + for callback in self._ad_callbacks.values(): + callback(device, advertisement_data) def create_or_update_device( self, address: str, name: str, details: Any, adv: AdvertisementData diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py index e526f0fa..f72e9c67 100644 --- a/bleak/backends/winrt/characteristic.py +++ b/bleak/backends/winrt/characteristic.py @@ -1,11 +1,18 @@ # -*- coding: utf-8 -*- +import sys from typing import List, Union from uuid import UUID -from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattCharacteristic, - GattCharacteristicProperties, -) +if sys.version_info >= (3, 12): + from winrt.windows.devices.bluetooth.genericattributeprofile import ( + GattCharacteristic, + GattCharacteristicProperties, + ) +else: + from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( + GattCharacteristic, + GattCharacteristicProperties, + ) from ..characteristic import BleakGATTCharacteristic from ..descriptor import BleakGATTDescriptor diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index 3431d0bc..6a84e21e 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -10,50 +10,95 @@ import sys import uuid import warnings -from ctypes import pythonapi -from typing import Any, Dict, List, Optional, Sequence, Set, Union, cast +from ctypes import WinError +from typing import ( + Any, + Dict, + List, + Literal, + Optional, + Protocol, + Sequence, + Set, + TypedDict, + Union, + cast, +) + +if sys.version_info < (3, 12): + from typing_extensions import Buffer +else: + from collections.abc import Buffer if sys.version_info < (3, 11): from async_timeout import timeout as async_timeout else: from asyncio import timeout as async_timeout -if sys.version_info[:2] < (3, 8): - from typing_extensions import Literal, TypedDict +if sys.version_info >= (3, 12): + from winrt.windows.devices.bluetooth import ( + BluetoothAddressType, + BluetoothCacheMode, + BluetoothError, + BluetoothLEDevice, + ) + from winrt.windows.devices.bluetooth.genericattributeprofile import ( + GattCharacteristic, + GattCharacteristicProperties, + GattClientCharacteristicConfigurationDescriptorValue, + GattCommunicationStatus, + GattDescriptor, + GattDeviceService, + GattSession, + GattSessionStatus, + GattSessionStatusChangedEventArgs, + GattValueChangedEventArgs, + GattWriteOption, + ) + from winrt.windows.devices.enumeration import ( + DeviceInformation, + DevicePairingKinds, + DevicePairingResultStatus, + DeviceUnpairingResultStatus, + ) + from winrt.windows.foundation import ( + AsyncStatus, + EventRegistrationToken, + IAsyncOperation, + ) + from winrt.windows.storage.streams import Buffer as WinBuffer else: - from typing import Literal, TypedDict - -from bleak_winrt.windows.devices.bluetooth import ( - BluetoothAddressType, - BluetoothCacheMode, - BluetoothError, - BluetoothLEDevice, -) -from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattCharacteristic, - GattCharacteristicProperties, - GattClientCharacteristicConfigurationDescriptorValue, - GattCommunicationStatus, - GattDescriptor, - GattDeviceService, - GattSession, - GattSessionStatus, - GattSessionStatusChangedEventArgs, - GattValueChangedEventArgs, - GattWriteOption, -) -from bleak_winrt.windows.devices.enumeration import ( - DeviceInformation, - DevicePairingKinds, - DevicePairingResultStatus, - DeviceUnpairingResultStatus, -) -from bleak_winrt.windows.foundation import ( - AsyncStatus, - EventRegistrationToken, - IAsyncOperation, -) -from bleak_winrt.windows.storage.streams import Buffer + from bleak_winrt.windows.devices.bluetooth import ( + BluetoothAddressType, + BluetoothCacheMode, + BluetoothError, + BluetoothLEDevice, + ) + from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( + GattCharacteristic, + GattCharacteristicProperties, + GattClientCharacteristicConfigurationDescriptorValue, + GattCommunicationStatus, + GattDescriptor, + GattDeviceService, + GattSession, + GattSessionStatus, + GattSessionStatusChangedEventArgs, + GattValueChangedEventArgs, + GattWriteOption, + ) + from bleak_winrt.windows.devices.enumeration import ( + DeviceInformation, + DevicePairingKinds, + DevicePairingResultStatus, + DeviceUnpairingResultStatus, + ) + from bleak_winrt.windows.foundation import ( + AsyncStatus, + EventRegistrationToken, + IAsyncOperation, + ) + from bleak_winrt.windows.storage.streams import Buffer as WinBuffer from ... import BleakScanner from ...exc import PROTOCOL_ERROR_CODES, BleakDeviceNotFoundError, BleakError @@ -68,15 +113,10 @@ logger = logging.getLogger(__name__) -_ACCESS_DENIED_SERVICES = list( - uuid.UUID(u) - for u in ("00001812-0000-1000-8000-00805f9b34fb",) # Human Interface Device Service -) -# TODO: we can use this when minimum Python is 3.8 -# class _Result(typing.Protocol): -# status: GattCommunicationStatus -# protocol_error: typing.Optional[int] +class _Result(Protocol): + status: GattCommunicationStatus + protocol_error: int def _address_to_int(address: str) -> int: @@ -95,7 +135,7 @@ def _address_to_int(address: str) -> int: return int(address, base=16) -def _ensure_success(result: Any, attr: Optional[str], fail_msg: str) -> Any: +def _ensure_success(result: _Result, attr: Optional[str], fail_msg: str) -> Any: """ Ensures that *status* is ``GattCommunicationStatus.SUCCESS``, otherwise raises ``BleakError``. @@ -307,7 +347,7 @@ def handle_session_status_changed( args: GattSessionStatusChangedEventArgs, ): if args.error != BluetoothError.SUCCESS: - logger.error(f"Unhandled GATT error {args.error}") + logger.error("Unhandled GATT error %r", args.error) if args.status == GattSessionStatus.ACTIVE: for e in self._session_active_events: @@ -329,7 +369,7 @@ def session_status_changed_event_handler( sender: GattSession, args: GattSessionStatusChangedEventArgs ): logger.debug( - "session_status_changed_event_handler: id: %s, error: %s, status: %s", + "session_status_changed_event_handler: id: %s, error: %r, status: %r", sender.device_id.id, args.error, args.status, @@ -474,6 +514,11 @@ async def disconnect(self) -> bool: # Dispose all service components that we have requested and created. if self.services: + # HACK: sometimes GattDeviceService.Close() hangs forever, so we + # add a delay to give the Windows Bluetooth stack some time to + # "settle" before closing the services + await asyncio.sleep(0.1) + for service in self.services: service.obj.close() self.services = None @@ -543,7 +588,6 @@ async def pair(self, protection_level: int = None, **kwargs) -> bool: device_information.pairing.can_pair and not device_information.pairing.is_paired ): - # Currently only supporting Just Works solutions... ceremony = DevicePairingKinds.CONFIRM_ONLY custom_pairing = device_information.pairing.custom @@ -572,7 +616,7 @@ def handler(sender, args): raise BleakError(f"Could not pair with device: {pairing_result.status}") else: logger.info( - "Paired to device with protection level %d.", + "Paired to device with protection level %r.", pairing_result.protection_level_used, ) return True @@ -653,8 +697,6 @@ async def get_services( if cache_mode is not None: args.append(cache_mode) - logger.debug("calling get_gatt_services_async") - def dispose_on_cancel(future): if future._cancel_requested and future._result is not None: logger.debug("disposing services object because of cancel") @@ -689,44 +731,38 @@ def dispose_on_cancel(future): ) ) - logger.debug("returned from get_gatt_services_async") - try: for service in services: - # Windows returns an ACCESS_DENIED error when trying to enumerate - # characteristics of services used by the OS, like the HID service - # so we have to exclude those services. - if service.uuid in _ACCESS_DENIED_SERVICES: - continue - - new_services.add_service(BleakGATTServiceWinRT(service)) + result = await FutureLike(service.get_characteristics_async(*args)) - logger.debug("calling get_characteristics_async") + if result.status == GattCommunicationStatus.ACCESS_DENIED: + # Windows does not allow access to services "owned" by the + # OS. This includes services like HID and Bond Manager. + logger.debug( + "skipping service %s due to access denied", service.uuid + ) + continue characteristics: Sequence[GattCharacteristic] = _ensure_success( - await FutureLike(service.get_characteristics_async(*args)), + result, "characteristics", - f"Could not get GATT characteristics for {service}", + f"Could not get GATT characteristics for service {service.uuid} ({service.attribute_handle})", ) - logger.debug("returned from get_characteristics_async") + new_services.add_service(BleakGATTServiceWinRT(service)) for characteristic in characteristics: - new_services.add_characteristic( - BleakGATTCharacteristicWinRT( - characteristic, self._session.max_pdu_size - 3 - ) - ) - - logger.debug("calling get_descriptors_async") - descriptors: Sequence[GattDescriptor] = _ensure_success( await FutureLike(characteristic.get_descriptors_async(*args)), "descriptors", - f"Could not get GATT descriptors for {service}", + f"Could not get GATT descriptors for characteristic {characteristic.uuid} ({characteristic.attribute_handle})", ) - logger.debug("returned from get_descriptors_async") + new_services.add_characteristic( + BleakGATTCharacteristicWinRT( + characteristic, self._session.max_pdu_size - 3 + ) + ) for descriptor in descriptors: new_services.add_descriptor( @@ -742,6 +778,12 @@ def dispose_on_cancel(future): # Don't leak services. WinRT is quite particular about services # being closed. logger.debug("disposing service objects") + + # HACK: sometimes GattDeviceService.Close() hangs forever, so we + # add a delay to give the Windows Bluetooth stack some time to + # "settle" before closing the services + await asyncio.sleep(0.1) + for service in services: service.close() raise @@ -837,36 +879,19 @@ async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: async def write_gatt_char( self, - char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], - data: Union[bytes, bytearray, memoryview], - response: bool = False, + characteristic: BleakGATTCharacteristic, + data: Buffer, + response: bool, ) -> None: - """Perform a write operation of the specified GATT characteristic. - - Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write - to, specified by either integer handle, UUID or directly by the - BleakGATTCharacteristic object representing it. - data (bytes or bytearray): The data to send. - response (bool): If write-with-response operation should be done. Defaults to `False`. - - """ if not self.is_connected: raise BleakError("Not connected") - if not isinstance(char_specifier, BleakGATTCharacteristic): - characteristic = self.services.get_characteristic(char_specifier) - else: - characteristic = char_specifier - if not characteristic: - raise BleakError(f"Characteristic {char_specifier} was not found!") - response = ( GattWriteOption.WRITE_WITH_RESPONSE if response else GattWriteOption.WRITE_WITHOUT_RESPONSE ) - buf = Buffer(len(data)) + buf = WinBuffer(len(data)) buf.length = buf.capacity with memoryview(buf) as mv: mv[:] = data @@ -876,14 +901,12 @@ async def write_gatt_char( f"Could not write value {data} to characteristic {characteristic.handle:04X}", ) - async def write_gatt_descriptor( - self, handle: int, data: Union[bytes, bytearray, memoryview] - ) -> None: + async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: """Perform a write operation on the specified GATT descriptor. Args: - handle (int): The handle of the descriptor to read from. - data (bytes or bytearray): The data to send. + handle: The handle of the descriptor to read from. + data: The data to send (any bytes-like object). """ if not self.is_connected: @@ -893,14 +916,14 @@ async def write_gatt_descriptor( if not descriptor: raise BleakError(f"Descriptor with handle {handle} was not found!") - buf = Buffer(len(data)) + buf = WinBuffer(len(data)) buf.length = buf.capacity with memoryview(buf) as mv: mv[:] = data _ensure_success( await descriptor.obj.write_value_with_result_async(buf), None, - f"Could not write value {data} to descriptor {handle:04X}", + f"Could not write value {data!r} to descriptor {handle:04X}", ) logger.debug("Write Descriptor %04X : %s", handle, data) @@ -1046,7 +1069,7 @@ def result(self) -> Any: raise asyncio.CancelledError error_code = self._op.error_code.value - pythonapi.PyErr_SetFromWindowsErr(error_code) + raise WinError(error_code) def done(self) -> bool: return self._op.status != AsyncStatus.STARTED @@ -1088,10 +1111,7 @@ def exception(self) -> Optional[Exception]: error_code = self._op.error_code.value - try: - pythonapi.PyErr_SetFromWindowsErr(error_code) - except OSError as e: - return e + return WinError(error_code) def get_loop(self) -> asyncio.AbstractEventLoop: return self._loop diff --git a/bleak/backends/winrt/descriptor.py b/bleak/backends/winrt/descriptor.py index 34a90fba..1203b754 100644 --- a/bleak/backends/winrt/descriptor.py +++ b/bleak/backends/winrt/descriptor.py @@ -1,6 +1,12 @@ # -*- coding: utf-8 -*- - -from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import GattDescriptor +import sys + +if sys.version_info >= (3, 12): + from winrt.windows.devices.bluetooth.genericattributeprofile import GattDescriptor +else: + from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( + GattDescriptor, + ) from ..descriptor import BleakGATTDescriptor diff --git a/bleak/backends/winrt/scanner.py b/bleak/backends/winrt/scanner.py index 9ee745b0..bfd46c71 100644 --- a/bleak/backends/winrt/scanner.py +++ b/bleak/backends/winrt/scanner.py @@ -1,30 +1,35 @@ import asyncio import logging import sys -from typing import Dict, List, NamedTuple, Optional +from typing import Dict, List, Literal, NamedTuple, Optional from uuid import UUID -from bleak_winrt.windows.devices.bluetooth.advertisement import ( - BluetoothLEAdvertisementReceivedEventArgs, - BluetoothLEAdvertisementType, - BluetoothLEAdvertisementWatcher, - BluetoothLEAdvertisementWatcherStatus, - BluetoothLEScanningMode, -) - -if sys.version_info[:2] < (3, 8): - from typing_extensions import Literal +if sys.version_info >= (3, 12): + from winrt.windows.devices.bluetooth.advertisement import ( + BluetoothLEAdvertisementReceivedEventArgs, + BluetoothLEAdvertisementType, + BluetoothLEAdvertisementWatcher, + BluetoothLEAdvertisementWatcherStatus, + BluetoothLEScanningMode, + ) else: - from typing import Literal + from bleak_winrt.windows.devices.bluetooth.advertisement import ( + BluetoothLEAdvertisementReceivedEventArgs, + BluetoothLEAdvertisementType, + BluetoothLEAdvertisementWatcher, + BluetoothLEAdvertisementWatcherStatus, + BluetoothLEScanningMode, + ) from ...assigned_numbers import AdvertisementDataType +from ...uuids import normalize_uuid_str from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner logger = logging.getLogger(__name__) def _format_bdaddr(a: int) -> str: - return ":".join("{:02X}".format(x) for x in a.to_bytes(6, byteorder="big")) + return ":".join(f"{x:02X}" for x in a.to_bytes(6, byteorder="big")) def _format_event_args(e: BluetoothLEAdvertisementReceivedEventArgs) -> str: @@ -101,7 +106,7 @@ def _received_handler( ): """Callback for AdvertisementWatcher.Received""" # TODO: Cannot check for if sender == self.watcher in winrt? - logger.debug("Received {0}.".format(_format_event_args(event_args))) + logger.debug("Received %s.", _format_event_args(event_args)) # REVISIT: if scanning filters with BluetoothSignalStrengthFilter.OutOfRangeTimeout # are in place, an RSSI of -127 means that the device has gone out of range and should @@ -157,15 +162,17 @@ def _received_handler( AdvertisementDataType.SERVICE_DATA_UUID16 ): data = bytes(section.data) - service_data[ - f"0000{data[1]:02x}{data[0]:02x}-0000-1000-8000-00805f9b34fb" - ] = data[2:] + service_data[normalize_uuid_str(f"{data[1]:02x}{data[0]:02x}")] = data[ + 2: + ] for section in args.advertisement.get_sections_by_type( AdvertisementDataType.SERVICE_DATA_UUID32 ): data = bytes(section.data) service_data[ - f"{data[3]:02x}{data[2]:02x}{data[1]:02x}{data[0]:02x}-0000-1000-8000-00805f9b34fb" + normalize_uuid_str( + f"{data[3]:02x}{data[2]:02x}{data[1]:02x}{data[0]:02x}" + ) ] = data[4:] for section in args.advertisement.get_sections_by_type( AdvertisementDataType.SERVICE_DATA_UUID128 @@ -188,9 +195,6 @@ def _received_handler( bdaddr, local_name, raw_data, advertisement_data ) - if self._callback is None: - return - # On Windows, we have to fake service UUID filtering. If we were to pass # a BluetoothLEAdvertisementFilter to the BluetoothLEAdvertisementWatcher # with the service UUIDs appropriately set, we would no longer receive @@ -205,13 +209,13 @@ def _received_handler( # if there were no matching service uuids, the don't call the callback return - self._callback(device, advertisement_data) + self.call_detection_callbacks(device, advertisement_data) def _stopped_handler(self, sender, e): logger.debug( - "{0} devices found. Watcher status: {1}.".format( - len(self.seen_devices), self.watcher.status - ) + "%s devices found. Watcher status: %r.", + len(self.seen_devices), + sender.status, ) self._stopped_event.set() @@ -247,15 +251,15 @@ async def stop(self) -> None: await self._stopped_event.wait() else: logger.debug( - "skipping waiting for stop because status is %s", - self.watcher.status.name, + "skipping waiting for stop because status is %r", + self.watcher.status, ) try: self.watcher.remove_received(self._received_token) self.watcher.remove_stopped(self._stopped_token) except Exception as e: - logger.debug("Could not remove event handlers: {0}...".format(e)) + logger.debug("Could not remove event handlers: %s", e) self._stopped_token = None self._received_token = None diff --git a/bleak/backends/winrt/service.py b/bleak/backends/winrt/service.py index 66fa6253..dde2d4ea 100644 --- a/bleak/backends/winrt/service.py +++ b/bleak/backends/winrt/service.py @@ -1,8 +1,14 @@ +import sys from typing import List -from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( - GattDeviceService, -) +if sys.version_info >= (3, 12): + from winrt.windows.devices.bluetooth.genericattributeprofile import ( + GattDeviceService, + ) +else: + from bleak_winrt.windows.devices.bluetooth.genericattributeprofile import ( + GattDeviceService, + ) from ..service import BleakGATTService from ..winrt.characteristic import BleakGATTCharacteristicWinRT diff --git a/bleak/uuids.py b/bleak/uuids.py index 281975e8..386af91b 100644 --- a/bleak/uuids.py +++ b/bleak/uuids.py @@ -4,7 +4,7 @@ from uuid import UUID -uuid16_dict = { +uuid16_dict: Dict[int, str] = { 0x0001: "SDP", 0x0003: "RFCOMM", 0x0005: "TCS-BIN", @@ -971,7 +971,7 @@ 0xFFFE: "Alliance for Wireless Power (A4WP)", } -uuid128_dict = { +uuid128_dict: Dict[str, str] = { "a3c87500-8ed3-4bdf-8a39-a01bebede295": "Eddystone Configuration Service", "a3c87501-8ed3-4bdf-8a39-a01bebede295": "Capabilities", "a3c87502-8ed3-4bdf-8a39-a01bebede295": "Active Slot", @@ -1155,13 +1155,67 @@ def normalize_uuid_str(uuid: str) -> str: Normaizes a UUID to the format used by Bleak. - Converted to lower case. - - 16-bit UUIDs are expanded to 128-bit. + - 16-bit and 32-bit UUIDs are expanded to 128-bit. - .. versionadded:: 0.20.0 + Example:: + + # 16-bit + uuid1 = normalize_uuid_str("1234") + # uuid1 == "00001234-1000-8000-00805f9b34fb" + + # 32-bit + uuid2 = normalize_uuid_str("12345678") + # uuid2 == "12345678-1000-8000-00805f9b34fb" + + # 128-bit + uuid3 = normalize_uuid_str("12345678-1234-1234-1234567890ABC") + # uuid3 == "12345678-1234-1234-1234567890abc" + + .. versionadded:: 0.20 + .. versionchanged:: 0.21 + Added support for 32-bit UUIDs. """ + # See: BLUETOOTH CORE SPECIFICATION Version 5.4 | Vol 3, Part B - Section 2.5.1 if len(uuid) == 4: # Bluetooth SIG registered 16-bit UUIDs uuid = f"0000{uuid}-0000-1000-8000-00805f9b34fb" + elif len(uuid) == 8: + # Bluetooth SIG registered 32-bit UUIDs + uuid = f"{uuid}-0000-1000-8000-00805f9b34fb" # let UUID class do the validation and conversion to lower case return str(UUID(uuid)) + + +def normalize_uuid_16(uuid: int) -> str: + """ + Normaizes a 16-bit integer UUID to the format used by Bleak. + + Returns: + 128-bit UUID as string with the format ``"0000xxxx-1000-8000-00805f9b34fb"``. + + Example:: + + uuid = normalize_uuid_16(0x1234) + # uuid == "00001234-1000-8000-00805f9b34fb" + + .. versionadded:: 0.21 + """ + return normalize_uuid_str(f"{uuid:04X}") + + +def normalize_uuid_32(uuid: int) -> str: + """ + Normaizes a 32-bit integer UUID to the format used by Bleak. + + Returns: + 128-bit UUID as string with the format ``"xxxxxxxx-1000-8000-00805f9b34fb"``. + + Example:: + + uuid = normalize_uuid_32(0x12345678) + # uuid == "12345678-1000-8000-00805f9b34fb" + + .. versionadded:: 0.21 + """ + return normalize_uuid_str(f"{uuid:08X}") diff --git a/docs/api/scanner.rst b/docs/api/scanner.rst index f9a15787..1c232e71 100644 --- a/docs/api/scanner.rst +++ b/docs/api/scanner.rst @@ -19,6 +19,8 @@ multiple devices. .. automethod:: bleak.BleakScanner.find_device_by_name .. automethod:: bleak.BleakScanner.find_device_by_address .. automethod:: bleak.BleakScanner.find_device_by_filter +.. autoclass:: bleak.BleakScanner.ExtraArgs + :members: --------------------- @@ -62,13 +64,19 @@ following methods: Getting discovered devices and advertisement data ------------------------------------------------- -If you aren't using the "easy" class methods, there are two ways to get the +If you aren't using the "easy" class methods, there are three ways to get the discovered devices and advertisement data. For event-driven programming, you can provide a ``detection_callback`` callback to the :class:`BleakScanner` constructor. This will be called back each time and advertisement is received. +Alternatively, you can utilize the asynchronous iterator to iterate over +advertisements as they are received. The method below returns an async iterator +that yields the same tuples as otherwise provided to ``detection_callback``. + +.. automethod:: bleak.BleakScanner.advertisement_data + Otherwise, you can use one of the properties below after scanning has stopped. .. autoproperty:: bleak.BleakScanner.discovered_devices diff --git a/docs/conf.py b/docs/conf.py index cb885f63..9da6b546 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,9 +13,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. +import os import pathlib import sys -import os import tomli @@ -116,12 +116,15 @@ "android", "async_timeout", "bleak_winrt", + "winrt", "CoreBluetooth", "dbus_fast", "Foundation", "jnius", "libdispatch", "objc", + "ctypes", + "typing_extensions", ] # -- Options for HTML output ------------------------------------------- diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 0b139522..441dcade 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -103,9 +103,9 @@ Python:: from bleak.backends.device import BLEDevice - async def find_all_devices_services() - scanner = BleakScanner() - devices: Sequence[BLEDevice] = scanner.discover(timeout=5.0) + async def find_all_devices_services(): + devices: Sequence[BLEDevice] = await BleakScanner.discover(timeout=5.0) + for d in devices: async with BleakClient(d) as client: print(client.services) diff --git a/docs/usage.rst b/docs/usage.rst index 4871e0b3..57afbcea 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -20,7 +20,7 @@ via the asynchronous context manager like this: from bleak import BleakClient address = "24:71:89:cc:09:05" - MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb" + MODEL_NBR_UUID = "2A24" async def main(address): async with BleakClient(address) as client: @@ -37,7 +37,7 @@ or one can do it without the context manager like this: from bleak import BleakClient address = "24:71:89:cc:09:05" - MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb" + MODEL_NBR_UUID = "2A24" async def main(address): client = BleakClient(address) diff --git a/examples/kivy/README.md b/examples/kivy/README.md index d4854d98..f9c0c02c 100644 --- a/examples/kivy/README.md +++ b/examples/kivy/README.md @@ -1,4 +1,12 @@ -This is a kivy application that lists scanned devices in a desktop window. +## This is a kivy application that lists scanned devices in a desktop window + +- An iOS backend has not been implemented yet. + +- This kivy example can also be run on desktop. + +The default target architecture is arm64-v8a. +If you have an older device, change it in the buildozer.spec file (android.archs = arch1, arch2, ..). +Multiple targets are allowed (will significantly increase build time). It can be run on Android via: @@ -7,11 +15,23 @@ It can be run on Android via: # connect phone with USB and enable USB debugging buildozer android deploy run logcat +## To use with local version of bleak source: + +Local source path can be specified using the P4A_bleak_DIR environment variable: + + P4A_bleak_DIR="path to bleak source" buildozer android debug + + + Note: changes to `bleak/**` will not be automatically picked up when rebuilding. Instead the recipe build must be cleaned: buildozer android p4a -- clean_recipe_build --local-recipes $(pwd)/../../bleak/backends/p4android/recipes bleak -An iOS backend has not been implemented yet. +## To use bleak in your own app: + +- Copy the bleak folder under bleak/backends/p4android/recipes into the app recipes folder. +Make sure that 'local_recipes' in buildozer.spec points to the app recipes folder. +The latest version of bleak will be installed automatically. -This kivy example can also be run on desktop. +- Add 'bleak' and it's dependencies to the requirements in your buildozer.spec file. diff --git a/examples/kivy/buildozer.spec b/examples/kivy/buildozer.spec index d5796c32..d6bbd253 100644 --- a/examples/kivy/buildozer.spec +++ b/examples/kivy/buildozer.spec @@ -38,7 +38,12 @@ version = 0.1.0 # (list) Application requirements # comma separated e.g. requirements = sqlite3,kivy -requirements = python3,kivy,bleak,async_to_sync +requirements = + python3, + kivy, + bleak, + async_to_sync, + async-timeout # (str) Custom source folders for requirements # Sets custom source for any requirements with recipes @@ -90,7 +95,14 @@ fullscreen = 0 #android.presplash_lottie = "path/to/lottie/file.json" # (list) Permissions -android.permissions = BLUETOOTH,BLUETOOTH_ADMIN,ACCESS_FINE_LOCATION,ACCESS_COARSE_LOCATION,ACCESS_BACKGROUND_LOCATION +android.permissions = + BLUETOOTH, + BLUETOOTH_SCAN, + BLUETOOTH_CONNECT, + BLUETOOTH_ADMIN, + ACCESS_FINE_LOCATION, + ACCESS_COARSE_LOCATION, + ACCESS_BACKGROUND_LOCATION # (list) features (adds uses-feature -tags to manifest) #android.features = android.hardware.usb.host @@ -225,7 +237,7 @@ android.accept_sdk_license = True #android.copy_libs = 1 # (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 -android.arch = armeabi-v7a +android.archs = arm64-v8a # (int) overrides automatic versionCode computation (used in build.gradle) # this is not the same as app version and should only be edited if you know what you're doing @@ -243,7 +255,6 @@ android.allow_backup = True # (str) python-for-android fork to use, defaults to upstream (kivy) #p4a.fork = kivy -#p4a.fork = xloem # (str) python-for-android branch to use, defaults to master #p4a.branch = master diff --git a/examples/mtu_size.py b/examples/mtu_size.py index 031ba118..512f6dc5 100644 --- a/examples/mtu_size.py +++ b/examples/mtu_size.py @@ -38,7 +38,7 @@ def callback(device: BLEDevice, adv: AdvertisementData) -> None: for chunk in ( data[i : i + chunk_size] for i in range(0, len(data), chunk_size) ): - await client.write_gatt_char(CHAR_UUID, chunk) + await client.write_gatt_char(CHAR_UUID, chunk, response=False) if __name__ == "__main__": diff --git a/examples/philips_hue.py b/examples/philips_hue.py index 1e717fd4..9f7f76ef 100644 --- a/examples/philips_hue.py +++ b/examples/philips_hue.py @@ -53,25 +53,25 @@ async def main(address): print(f"Paired: {paired}") print("Turning Light off...") - await client.write_gatt_char(LIGHT_CHARACTERISTIC, b"\x00") + await client.write_gatt_char(LIGHT_CHARACTERISTIC, b"\x00", response=False) await asyncio.sleep(1.0) print("Turning Light on...") - await client.write_gatt_char(LIGHT_CHARACTERISTIC, b"\x01") + await client.write_gatt_char(LIGHT_CHARACTERISTIC, b"\x01", response=False) await asyncio.sleep(1.0) print("Setting color to RED...") color = convert_rgb([255, 0, 0]) - await client.write_gatt_char(COLOR_CHARACTERISTIC, color) + await client.write_gatt_char(COLOR_CHARACTERISTIC, color, response=False) await asyncio.sleep(1.0) print("Setting color to GREEN...") color = convert_rgb([0, 255, 0]) - await client.write_gatt_char(COLOR_CHARACTERISTIC, color) + await client.write_gatt_char(COLOR_CHARACTERISTIC, color, response=False) await asyncio.sleep(1.0) print("Setting color to BLUE...") color = convert_rgb([0, 0, 255]) - await client.write_gatt_char(COLOR_CHARACTERISTIC, color) + await client.write_gatt_char(COLOR_CHARACTERISTIC, color, response=False) await asyncio.sleep(1.0) for brightness in range(256): @@ -83,6 +83,7 @@ async def main(address): brightness, ] ), + response=False, ) await asyncio.sleep(0.2) @@ -94,6 +95,7 @@ async def main(address): 40, ] ), + response=False, ) diff --git a/examples/scan_iterator.py b/examples/scan_iterator.py new file mode 100644 index 00000000..5a407b38 --- /dev/null +++ b/examples/scan_iterator.py @@ -0,0 +1,37 @@ +""" +Scan/Discovery Async Iterator +-------------- + +Example showing how to scan for BLE devices using async iterator instead of callback function + +Created on 2023-07-07 by bojanpotocnik + +""" +import asyncio + +from bleak import BleakScanner + + +async def main(): + async with BleakScanner() as scanner: + print("Scanning...") + + n = 5 + print(f"\n{n} advertisement packets:") + async for bd, ad in scanner.advertisement_data(): + print(f" {n}. {bd!r} with {ad!r}") + n -= 1 + if n == 0: + break + + n = 10 + print(f"\nFind device with name longer than {n} characters...") + async for bd, ad in scanner.advertisement_data(): + found = len(bd.name or "") > n or len(ad.local_name or "") > n + print(f" Found{' it' if found else ''} {bd!r} with {ad!r}") + if found: + break + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/sensortag.py b/examples/sensortag.py index 40333ef9..bfae5d62 100644 --- a/examples/sensortag.py +++ b/examples/sensortag.py @@ -13,7 +13,7 @@ import sys from bleak import BleakClient -from bleak.uuids import uuid16_dict +from bleak.uuids import normalize_uuid_16, uuid16_dict ADDRESS = ( "24:71:89:cc:09:05" @@ -65,33 +65,17 @@ f000ffc4-0451-4000-b000-000000000000 """ -uuid16_dict = {v: k for k, v in uuid16_dict.items()} - -SYSTEM_ID_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format( - uuid16_dict.get("System ID") -) -MODEL_NBR_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format( - uuid16_dict.get("Model Number String") -) -DEVICE_NAME_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format( - uuid16_dict.get("Device Name") -) -FIRMWARE_REV_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format( - uuid16_dict.get("Firmware Revision String") -) -HARDWARE_REV_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format( - uuid16_dict.get("Hardware Revision String") -) -SOFTWARE_REV_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format( - uuid16_dict.get("Software Revision String") -) -MANUFACTURER_NAME_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format( - uuid16_dict.get("Manufacturer Name String") -) -BATTERY_LEVEL_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format( - uuid16_dict.get("Battery Level") -) -KEY_PRESS_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0xFFE1) +uuid16_lookup = {v: normalize_uuid_16(k) for k, v in uuid16_dict.items()} + +SYSTEM_ID_UUID = uuid16_lookup["System ID"] +MODEL_NBR_UUID = uuid16_lookup["Model Number String"] +DEVICE_NAME_UUID = uuid16_lookup["Device Name"] +FIRMWARE_REV_UUID = uuid16_lookup["Firmware Revision String"] +HARDWARE_REV_UUID = uuid16_lookup["Hardware Revision String"] +SOFTWARE_REV_UUID = uuid16_lookup["Software Revision String"] +MANUFACTURER_NAME_UUID = uuid16_lookup["Manufacturer Name String"] +BATTERY_LEVEL_UUID = uuid16_lookup["Battery Level"] +KEY_PRESS_UUID = normalize_uuid_16(0xFFE1) # I/O test points on SensorTag. IO_DATA_CHAR_UUID = "f000aa65-0451-4000-b000-000000000000" IO_CONFIG_CHAR_UUID = "f000aa66-0451-4000-b000-000000000000" @@ -140,7 +124,7 @@ async def notification_handler(characteristic, data): value = await client.read_gatt_char(IO_DATA_CHAR_UUID) print("I/O Data Pre-Write Value: {0}".format(value)) - await client.write_gatt_char(IO_DATA_CHAR_UUID, write_value) + await client.write_gatt_char(IO_DATA_CHAR_UUID, write_value, response=True) value = await client.read_gatt_char(IO_DATA_CHAR_UUID) print("I/O Data Post-Write Value: {0}".format(value)) @@ -150,7 +134,7 @@ async def notification_handler(characteristic, data): value = await client.read_gatt_char(IO_CONFIG_CHAR_UUID) print("I/O Config Pre-Write Value: {0}".format(value)) - await client.write_gatt_char(IO_CONFIG_CHAR_UUID, write_value) + await client.write_gatt_char(IO_CONFIG_CHAR_UUID, write_value, response=True) value = await client.read_gatt_char(IO_CONFIG_CHAR_UUID) print("I/O Config Post-Write Value: {0}".format(value)) diff --git a/examples/two_devices.py b/examples/two_devices.py index 59bd524d..b5d53ef8 100644 --- a/examples/two_devices.py +++ b/examples/two_devices.py @@ -1,42 +1,163 @@ +import argparse import asyncio +import contextlib +import logging +from typing import Iterable -from bleak import BleakClient +from bleak import BleakScanner, BleakClient -temperatureUUID = "45366e80-cf3a-11e1-9ab4-0002a5d5c51b" -ecgUUID = "46366e80-cf3a-11e1-9ab4-0002a5d5c51b" -notify_uuid = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0xFFE1) +async def connect_to_device( + lock: asyncio.Lock, + by_address: bool, + macos_use_bdaddr: bool, + name_or_address: str, + notify_uuid: str, +): + """ + Scan and connect to a device then print notifications for 10 seconds before + disconnecting. + Args: + lock: + The same lock must be passed to all calls to this function. + by_address: + If true, treat *name_or_address* as an address, otherwise treat + it as a name. + macos_use_bdaddr: + If true, enable hack to allow use of Bluetooth address instead of + UUID on macOS. + name_or_address: + The Bluetooth address/UUID of the device to connect to. + notify_uuid: + The UUID of a characteristic that supports notifications. + """ + logging.info("starting %s task", name_or_address) -def callback(characteristic, data): - print(characteristic, data) + try: + async with contextlib.AsyncExitStack() as stack: + # Trying to establish a connection to two devices at the same time + # can cause errors, so use a lock to avoid this. + async with lock: + logging.info("scanning for %s", name_or_address) -async def connect_to_device(address): - print("starting", address, "loop") - async with BleakClient(address, timeout=5.0) as client: + if by_address: + device = await BleakScanner.find_device_by_address( + name_or_address, macos=dict(use_bdaddr=macos_use_bdaddr) + ) + else: + device = await BleakScanner.find_device_by_name(name_or_address) + + logging.info("stopped scanning for %s", name_or_address) + + if device is None: + logging.error("%s not found", name_or_address) + return + + client = BleakClient(device) + + logging.info("connecting to %s", name_or_address) + + await stack.enter_async_context(client) + + logging.info("connected to %s", name_or_address) + + # This will be called immediately before client.__aexit__ when + # the stack context manager exits. + stack.callback(logging.info, "disconnecting from %s", name_or_address) + + # The lock is released here. The device is still connected and the + # Bluetooth adapter is now free to scan and connect another device + # without disconnecting this one. + + def callback(_, data): + logging.info("%s received %r", name_or_address, data) - print("connect to", address) - try: await client.start_notify(notify_uuid, callback) await asyncio.sleep(10.0) await client.stop_notify(notify_uuid) - except Exception as e: - print(e) - print("disconnect from", address) + # The stack context manager exits here, triggering disconnection. + + logging.info("disconnected from %s", name_or_address) + except Exception: + logging.exception("error with %s", name_or_address) -def main(addresses): - return asyncio.gather(*(connect_to_device(address) for address in addresses)) + +async def main( + by_address: bool, + macos_use_bdaddr: bool, + addresses: Iterable[str], + uuids: Iterable[str], +): + lock = asyncio.Lock() + + await asyncio.gather( + *( + connect_to_device(lock, by_address, macos_use_bdaddr, address, uuid) + for address, uuid in zip(addresses, uuids) + ) + ) if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "device1", + metavar="", + help="Bluetooth name or address of first device connect to", + ) + parser.add_argument( + "uuid1", + metavar="", + help="notification characteristic UUID on first device", + ) + parser.add_argument( + "device2", + metavar="", + help="Bluetooth name or address of second device to connect to", + ) + parser.add_argument( + "uuid2", + metavar="", + help="notification characteristic UUID on second device", + ) + + parser.add_argument( + "--by-address", + action="store_true", + help="when true treat args as Bluetooth address instead of name", + ) + + parser.add_argument( + "--macos-use-bdaddr", + action="store_true", + help="when true use Bluetooth address instead of UUID on macOS", + ) + + parser.add_argument( + "-d", + "--debug", + action="store_true", + help="sets the log level to debug", + ) + + args = parser.parse_args() + + log_level = logging.DEBUG if args.debug else logging.INFO + logging.basicConfig( + level=log_level, + format="%(asctime)-15s %(name)-8s %(levelname)s: %(message)s", + ) + asyncio.run( main( - [ - "B9EA5233-37EF-4DD6-87A8-2A875E821C46", - "F0CBEBD3-299B-4139-A9FC-44618C720157", - ] + args.by_address, + args.macos_use_bdaddr, + (args.device1, args.device2), + (args.uuid1, args.uuid2), ) ) diff --git a/examples/uart_service.py b/examples/uart_service.py index 37dea7e7..3db69dc9 100644 --- a/examples/uart_service.py +++ b/examples/uart_service.py @@ -89,7 +89,7 @@ def handle_rx(_: BleakGATTCharacteristic, data: bytearray): # property to split the data into chunks that will fit. for s in sliced(data, rx_char.max_write_without_response_size): - await client.write_gatt_char(rx_char, s) + await client.write_gatt_char(rx_char, s, response=False) print("sent:", data) diff --git a/poetry.lock b/poetry.lock index 6a1d8b12..ada0e16f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "alabaster" version = "0.7.12" description = "A configurable sidebar-enabled Sphinx theme" -category = "dev" optional = false python-versions = "*" files = [ @@ -16,7 +15,6 @@ files = [ name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -24,26 +22,10 @@ files = [ {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] -[package.dependencies] -typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} - -[[package]] -name = "asynctest" -version = "0.13.0" -description = "Enhance the standard unittest package with features for testing asyncio libraries" -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ - {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, - {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, -] - [[package]] name = "attrs" version = "22.1.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -61,7 +43,6 @@ tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy name = "Babel" version = "2.10.3" description = "Internationalization utilities" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -76,7 +57,6 @@ pytz = ">=2015.7" name = "black" version = "22.8.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -111,7 +91,6 @@ mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] @@ -124,7 +103,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "bleak-winrt" version = "1.2.0" description = "Python WinRT bindings for Bleak" -category = "main" optional = false python-versions = "*" files = [ @@ -145,7 +123,6 @@ files = [ name = "certifi" version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -157,7 +134,6 @@ files = [ name = "charset-normalizer" version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" optional = false python-versions = ">=3.6.0" files = [ @@ -172,7 +148,6 @@ unicode-backport = ["unicodedata2"] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -182,13 +157,11 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" version = "0.4.5" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -200,7 +173,6 @@ files = [ name = "coverage" version = "6.4.4" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -266,7 +238,6 @@ toml = ["tomli"] name = "dbus-fast" version = "1.83.1" description = "A faster version of dbus-next" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -307,7 +278,6 @@ async-timeout = {version = ">=3.0.0", markers = "python_version < \"3.11\""} name = "docutils" version = "0.17.1" description = "Docutils -- Python Documentation Utilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -319,7 +289,6 @@ files = [ name = "flake8" version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -336,7 +305,6 @@ pyflakes = ">=2.5.0,<2.6.0" name = "idna" version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -348,7 +316,6 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -360,7 +327,6 @@ files = [ name = "importlib-metadata" version = "4.12.0" description = "Read metadata from Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -369,7 +335,6 @@ files = [ ] [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] @@ -381,7 +346,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = "*" files = [ @@ -393,7 +357,6 @@ files = [ name = "Jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -411,7 +374,6 @@ i18n = ["Babel (>=2.7)"] name = "MarkupSafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -461,7 +423,6 @@ files = [ name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -473,7 +434,6 @@ files = [ name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" optional = false python-versions = "*" files = [ @@ -485,7 +445,6 @@ files = [ name = "packaging" version = "21.3" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -500,7 +459,6 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" name = "pathspec" version = "0.10.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -512,7 +470,6 @@ files = [ name = "platformdirs" version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -528,7 +485,6 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -536,9 +492,6 @@ files = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -547,7 +500,6 @@ testing = ["pytest", "pytest-benchmark"] name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -559,7 +511,6 @@ files = [ name = "pycodestyle" version = "2.9.1" description = "Python style guide checker" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -571,7 +522,6 @@ files = [ name = "pyflakes" version = "2.5.0" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -580,15 +530,14 @@ files = [ ] [[package]] -name = "Pygments" -version = "2.13.0" +name = "pygments" +version = "2.15.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, - {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, + {file = "Pygments-2.15.0-py3-none-any.whl", hash = "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094"}, + {file = "Pygments-2.15.0.tar.gz", hash = "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"}, ] [package.extras] @@ -596,84 +545,82 @@ plugins = ["importlib-metadata"] [[package]] name = "pyobjc-core" -version = "9.0.1" +version = "9.2" description = "Python<->ObjC Interoperability Module" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pyobjc-core-9.0.1.tar.gz", hash = "sha256:5ce1510bb0bdff527c597079a42b2e13a19b7592e76850be7960a2775b59c929"}, - {file = "pyobjc_core-9.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b614406d46175b1438a9596b664bf61952323116704d19bc1dea68052a0aad98"}, - {file = "pyobjc_core-9.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bd397e729f6271c694fb70df8f5d3d3c9b2f2b8ac02fbbdd1757ca96027b94bb"}, - {file = "pyobjc_core-9.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d919934eaa6d1cf1505ff447a5c2312be4c5651efcb694eb9f59e86f5bd25e6b"}, - {file = "pyobjc_core-9.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:67d67ca8b164f38ceacce28a18025845c3ec69613f3301935d4d2c4ceb22e3fd"}, - {file = "pyobjc_core-9.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:39d11d71f6161ac0bd93cffc8ea210bb0178b56d16a7408bf74283d6ecfa7430"}, - {file = "pyobjc_core-9.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25be1c4d530e473ed98b15063b8d6844f0733c98914de6f09fe1f7652b772bbc"}, + {file = "pyobjc-core-9.2.tar.gz", hash = "sha256:d734b9291fec91ff4e3ae38b9c6839debf02b79c07314476e87da8e90b2c68c3"}, + {file = "pyobjc_core-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fa674a39949f5cde8e5c7bbcd24496446bfc67592b028aedbec7f81dc5fc4daa"}, + {file = "pyobjc_core-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bbc8de304ee322a1ee530b4d2daca135a49b4a49aa3cedc6b2c26c43885f4842"}, + {file = "pyobjc_core-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0fa950f092673883b8bd28bc18397415cabb457bf410920762109b411789ade9"}, + {file = "pyobjc_core-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:586e4cae966282eaa61b21cae66ccdcee9d69c036979def26eebdc08ddebe20f"}, + {file = "pyobjc_core-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41189c2c680931c0395a55691763c481fc681f454f21bb4f1644f98c24a45954"}, + {file = "pyobjc_core-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:2d23ee539f2ba5e9f5653d75a13f575c7e36586fc0086792739e69e4c2617eda"}, + {file = "pyobjc_core-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b9809cf96678797acb72a758f34932fe8e2602d5ab7abec15c5ac68ddb481720"}, ] [[package]] name = "pyobjc-framework-cocoa" -version = "9.0.1" +version = "9.2" description = "Wrappers for the Cocoa frameworks on macOS" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pyobjc-framework-Cocoa-9.0.1.tar.gz", hash = "sha256:a8b53b3426f94307a58e2f8214dc1094c19afa9dcb96f21be12f937d968b2df3"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f94b0f92a62b781e633e58f09bcaded63d612f9b1e15202f5f372ea59e4aebd"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f062c3bb5cc89902e6d164aa9a66ffc03638645dd5f0468b6f525ac997c86e51"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0b374c0a9d32ba4fc5610ab2741cb05a005f1dfb82a47dbf2dbb2b3a34b73ce5"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8928080cebbce91ac139e460d3dfc94c7cb6935be032dcae9c0a51b247f9c2d9"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:9d2bd86a0a98d906f762f5dc59f2fc67cce32ae9633b02ff59ac8c8a33dd862d"}, - {file = "pyobjc_framework_Cocoa-9.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2a41053cbcee30e1e8914efa749c50b70bf782527d5938f2bc2a6393740969ce"}, + {file = "pyobjc-framework-Cocoa-9.2.tar.gz", hash = "sha256:efd78080872d8c8de6c2b97e0e4eac99d6203a5d1637aa135d071d464eb2db53"}, + {file = "pyobjc_framework_Cocoa-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9e02d8a7cc4eb7685377c50ba4f17345701acf4c05b1e7480d421bff9e2f62a4"}, + {file = "pyobjc_framework_Cocoa-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3b1e6287b3149e4c6679cdbccd8e9ef6557a4e492a892e80a77df143f40026d2"}, + {file = "pyobjc_framework_Cocoa-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:312977ce2e3989073c6b324c69ba24283de206fe7acd6dbbbaf3e29238a22537"}, + {file = "pyobjc_framework_Cocoa-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aae7841cf40c26dd915f4dd828f91c6616e6b7998630b72e704750c09e00f334"}, + {file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:739a421e14382a46cbeb9a883f192dceff368ad28ec34d895c48c0ad34cf2c1d"}, + {file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:32d9ac1033fac1b821ddee8c68f972a7074ad8c50bec0bea9a719034c1c2fb94"}, + {file = "pyobjc_framework_Cocoa-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b236bb965e41aeb2e215d4e98a5a230d4b63252c6d26e00924ea2e69540a59d6"}, ] [package.dependencies] -pyobjc-core = ">=9.0.1" +pyobjc-core = ">=9.2" [[package]] name = "pyobjc-framework-corebluetooth" -version = "9.0.1" +version = "9.2" description = "Wrappers for the framework CoreBluetooth on macOS" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pyobjc-framework-CoreBluetooth-9.0.1.tar.gz", hash = "sha256:bf008d7bfe13cda12a43ed82346acfad262e90824086b145394c154531b51841"}, - {file = "pyobjc_framework_CoreBluetooth-9.0.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:62f15fc6e1d864a5e6afd26fe01947e5879b5322af23719d988981ca65b34a30"}, - {file = "pyobjc_framework_CoreBluetooth-9.0.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:15673b480b3695aba87ce9574154bd1997f03a784969642b0da5e990e9679f48"}, - {file = "pyobjc_framework_CoreBluetooth-9.0.1-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:3560c55de7799cd7468b1282d6c2fca4823896ffbcb7d53be69b55c01a44592e"}, + {file = "pyobjc-framework-CoreBluetooth-9.2.tar.gz", hash = "sha256:cb2481b1dfe211ae9ce55f36537dc8155dbf0dc8ff26e0bc2e13f7afb0a291d1"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:53d888742119d0f0c725d0b0c2389f68e8f21f0cba6d6aec288c53260a0196b6"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:179532882126526e38fe716a50fb0ee8f440e0b838d290252c515e622b5d0e49"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:256a5031ea9d8a7406541fa1b0dfac549b1de93deae8284605f9355b13fb58be"}, ] [package.dependencies] -pyobjc-core = ">=9.0.1" -pyobjc-framework-Cocoa = ">=9.0.1" +pyobjc-core = ">=9.2" +pyobjc-framework-Cocoa = ">=9.2" [[package]] name = "pyobjc-framework-libdispatch" -version = "9.0.1" +version = "9.2" description = "Wrappers for libdispatch on macOS" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pyobjc-framework-libdispatch-9.0.1.tar.gz", hash = "sha256:988c4c8608f2059c8b80ac520bc8d20a46ff85f65c50749110c45df610141fce"}, - {file = "pyobjc_framework_libdispatch-9.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6cd32fea76165157a623ef8871f83cfa627ea2e878417704d6ac9c284c4211d5"}, - {file = "pyobjc_framework_libdispatch-9.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a0f8ba6b498a095edef07e7a55f11dda3a6b37706caaa0f954f297c9aa1122e"}, - {file = "pyobjc_framework_libdispatch-9.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:906f4e705b40ea878d0a7feddddac85965f9709f7a951c3d5459260d48efd56f"}, - {file = "pyobjc_framework_libdispatch-9.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0bd94e697e3739eaf093a9b6f5be9a2cc34faa96c66cc21d2c42a996a3b01242"}, - {file = "pyobjc_framework_libdispatch-9.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7f9798c599acdd21251f57970bafabccc7fa723ae2a6d1fbe82f99ecfa3f7cf9"}, - {file = "pyobjc_framework_libdispatch-9.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:10a877b31960ee958873e5228f7b588c664014be8ad4d13a76a764482a18bf41"}, + {file = "pyobjc-framework-libdispatch-9.2.tar.gz", hash = "sha256:542e7f7c2b041939db5ed6f3119c1d67d73ec14a996278b92485f8513039c168"}, + {file = "pyobjc_framework_libdispatch-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88d4091d4bcb5702783d6e86b4107db973425a17d1de491543f56bd348909b60"}, + {file = "pyobjc_framework_libdispatch-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a67b007113328538b57893cc7829a722270764cdbeae6d5e1460a1d911314df"}, + {file = "pyobjc_framework_libdispatch-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6fccea1a57436cf1ac50d9ebc6e3e725bcf77f829ba6b118e62e6ed7866d359d"}, + {file = "pyobjc_framework_libdispatch-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6eba747b7ad91b0463265a7aee59235bb051fb97687f35ca2233690369b5e4e4"}, + {file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2e835495860d04f63c2d2f73ae3dd79da4222864c107096dc0f99e8382700026"}, + {file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1b107e5c3580b09553030961ea6b17abad4a5132101eab1af3ad2cb36d0f08bb"}, + {file = "pyobjc_framework_libdispatch-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:83cdb672acf722717b5ecf004768f215f02ac02d7f7f2a9703da6e921ab02222"}, ] [package.dependencies] -pyobjc-core = ">=9.0.1" +pyobjc-core = ">=9.2" [[package]] name = "pyparsing" version = "3.0.9" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" optional = false python-versions = ">=3.6.8" files = [ @@ -688,7 +635,6 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pytest" version = "7.1.3" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -699,7 +645,6 @@ files = [ [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -713,7 +658,6 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. name = "pytest-asyncio" version = "0.19.0" description = "Pytest support for asyncio" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -723,7 +667,6 @@ files = [ [package.dependencies] pytest = ">=6.1.0" -typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] @@ -732,7 +675,6 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "pytest-cov" version = "3.0.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -751,7 +693,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytz" version = "2022.2.1" description = "World timezone definitions, modern and historical" -category = "dev" optional = false python-versions = "*" files = [ @@ -761,21 +702,20 @@ files = [ [[package]] name = "requests" -version = "2.28.1" +version = "2.31.0" description = "Python HTTP for Humans." -category = "dev" optional = false -python-versions = ">=3.7, <4" +python-versions = ">=3.7" files = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" +urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] @@ -785,7 +725,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" optional = false python-versions = "*" files = [ @@ -797,7 +736,6 @@ files = [ name = "Sphinx" version = "5.1.1" description = "Python documentation generator" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -833,7 +771,6 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed-ast"] name = "sphinx-rtd-theme" version = "1.0.0" description = "Read the Docs theme for Sphinx" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -852,7 +789,6 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] name = "sphinxcontrib-applehelp" version = "1.0.2" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -868,7 +804,6 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -884,7 +819,6 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -900,7 +834,6 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -915,7 +848,6 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -931,7 +863,6 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -947,7 +878,6 @@ test = ["pytest"] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -955,57 +885,21 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] - [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] [[package]] name = "urllib3" version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" files = [ @@ -1018,11 +912,228 @@ brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "winrt-runtime" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-runtime-2.0.0b1.tar.gz", hash = "sha256:28db2ebe7bfb347d110224e9f23fe8079cea45af0fcbd643d039524ced07d22c"}, + {file = "winrt_runtime-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:8f812b01e2c8dd3ca68aa51a7aa02e815cc2ac3c8520a883b4ec7a4fc63afb04"}, + {file = "winrt_runtime-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:f36f6102f9b7a08d917a6809117c085639b66be2c579f4089d3fd47b83e8f87b"}, + {file = "winrt_runtime-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:4a99f267da96edc977623355b816b46c1344c66dc34732857084417d8cf9a96b"}, + {file = "winrt_runtime-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:ba998e3fc452338c5e2d7bf5174a6206580245066d60079ee4130082d0eb61c2"}, + {file = "winrt_runtime-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:e7838f0fdf5653ce245888590214177a1f54884cece2c8dfbfe3d01b2780171e"}, + {file = "winrt_runtime-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:2afa45b7385e99a63d55ccda29096e6a84fcd4c654479005c147b0e65e274abf"}, + {file = "winrt_runtime-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:edda124ff965cec3a6bfdb26fbe88e004f96975dd84115176e30c1efbcb16f4c"}, + {file = "winrt_runtime-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:d8935951efeec6b3d546dce8f48bb203aface57a1ba991c066f0e12e84c8f91e"}, + {file = "winrt_runtime-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:509fb9a03af5e1125433f58522725716ceef040050d33625460b5a5eb98a46ac"}, + {file = "winrt_runtime-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:41138fe4642345d7143e817ce0905d82e60b3832558143e0a17bfea8654c6512"}, + {file = "winrt_runtime-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:081a429fe85c33cb6610c4a799184b7650b30f15ab1d89866f2bda246d3a5c0a"}, + {file = "winrt_runtime-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:e6984604c6ae1f3258973ba2503d1ea5aa15e536ca41d6a131ad305ebbb6519d"}, +] + +[[package]] +name = "winrt-windows-devices-bluetooth" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Bluetooth-2.0.0b1.tar.gz", hash = "sha256:786bd43786b873a083b89debece538974f720584662a2573d6a8a8501a532860"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:79631bf3f96954da260859df9228a028835ffade0d885ba3942c5a86a853d150"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:cd85337a95065d0d2045c06db1a5edd4a447aad47cf7027818f6fb69f831c56c"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:6a963869ed003d260e90e9bedc334129303f263f068ea1c0d994df53317db2bc"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:7c5951943a3911d94a8da190f4355dc70128d7d7f696209316372c834b34d462"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:b0bb154ae92235649ed234982f609c490a467d5049c27d63397be9abbb00730e"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:6688dfb0fc3b7dc517bf8cf40ae00544a50b4dec91470d37be38fc33c4523632"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:613c6ff4125df46189b3bef6d3110d94ec725d357ab734f00eedb11c4116c367"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:59c403b64e9f4e417599c6f6aea6ee6fac960597c21eac6b3fd8a84f64aa387c"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:b7f6e1b9bb6e33be80045adebd252cf25cd648759fad6e86c61a393ddd709f7f"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:eae7a89106eab047e96843e28c3c6ce0886dd7dee60180a1010498925e9503f9"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:8dfd1915c894ac19dd0b24aba38ef676c92c3473c0d9826762ba9616ad7df68b"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:49058587e6d82ba33da0767b97a378ddfea8e3a5991bdeff680faa287bfae57e"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Enumeration[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Radios[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Networking[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-devices-bluetooth-advertisement" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Bluetooth.Advertisement-2.0.0b1.tar.gz", hash = "sha256:d9050faa4377d410d4f0e9cabb5ec555a267531c9747370555ac9ec93ec9f399"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:ac9b703d16adc87c3541585525b8fcf6d84391e2fa010c2f001e714c405cc3b7"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:593cade7853a8b0770e8ef30462b5d5f477b82e17e0aa590094b1c26efd3e05a"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:574698c08895e2cfee7379bdf34a5f319fe440d7dfcc7bc9858f457c08e9712c"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:652a096f8210036bbb539d7f971eaf1f472a3aeb60b7e31278e3d0d30a355292"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:e5cfb866c44dad644fb44b441f4fdbddafc9564075f1f68f756e20f438105c67"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:6c2503eaaf5cd988b5510b86347dba45ad6ee52656f9656a1a97abae6d35386e"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:780c766725a55f4211f921c773c92c2331803e70f65d6ad6676a60f903d39a54"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:39c8633d01039eb2c2f6f20cfc43c045a333b9f3a45229e2ce443f71bb2a562c"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:eaa0d44b4158b16937eac8102249e792f0299dbb0aefc56cc9adc9552e8f9afe"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:d171487e23f7671ad2923544bfa6545d0a29a1a9ae1f5c1d5e5e5f473a5d62b2"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:442eecac87653a03617e65bdb2ef79ddc0582dfdacc2be8af841fba541577f8b"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:b30ab9b8c1ecf818be08bac86bee425ef40f75060c4011d4e6c2e624a7b9916e"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-devices-bluetooth-genericattributeprofile" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1.tar.gz", hash = "sha256:93b745d51ecfb3e9d3a21623165cc065735c9e0146cb7a26744182c164e63e14"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:db740aaedd80cca5b1a390663b26c7733eb08f4c57ade6a04b055d548e9d042b"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:7c81aa6c066cdab58bcc539731f208960e094a6d48b59118898e1e804dbbdf7f"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:92277a6bbcbe2225ad1be92968af597dc77bc37a63cd729690d2d9fb5094ae25"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:6b48209669c1e214165530793cf9916ae44a0ae2618a9be7a489e8c94f7e745f"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:2f17216e6ce748eaef02fb0658213515d3ff31e2dbb18f070a614876f818c90d"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:db798a0f0762e390da5a9f02f822daff00692bd951a492224bf46782713b2938"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:b8d9dba04b9cfa53971c35117fc3c68c94bfa5e2ed18ce680f731743598bf246"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:e5260b3f33dee8a896604297e05efc04d04298329c205a74ded8e2d6333e84b7"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:822ef539389ecb546004345c4dce8b9b7788e2e99a1d6f0947a4b123dceb7fed"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:11e6863e7a94d2b6dd76ddcd19c01e311895810a4ce6ad08c7b5534294753243"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:20de8d04c301c406362c93e78d41912aea0af23c4b430704aba329420d7c2cdf"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:918059796f2f123216163b928ecde8ecec17994fb7a94042af07fda82c132a6d"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Enumeration[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-devices-enumeration" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Enumeration-2.0.0b1.tar.gz", hash = "sha256:8f214040e4edbe57c4943488887db89f4a00d028c34169aafd2205e228026100"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:dcb9e7d230aefec8531a46d393ecb1063b9d4b97c9f3ff2fc537ce22bdfa2444"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:22a3e1fef40786cc8d51320b6f11ff25de6c674475f3ba608a46915e1dadf0f5"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:2edcfeb70a71d40622873cad96982a28e92a7ee71f33968212dd3598b2d8d469"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:ce4eb88add7f5946d2666761a97a3bb04cac2a061d264f03229c1e15dbd7ce91"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:a9001f17991572abdddab7ab074e08046e74e05eeeaf3b2b01b8b47d2879b64c"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:0440b91ce144111e207f084cec6b1277162ef2df452d321951e989ce87dc9ced"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:e4fae13126f13a8d9420b74fb5a5ff6a6b2f91f7718c4be2d4a8dc1337c58f59"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:e352eebc23dc94fb79e67a056c057fb0e16c20c8cb881dc826094c20ed4791e3"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:b43f5c1f053a170e6e4b44ba69838ac223f9051adca1a56506d4c46e98d1485f"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:ed245fad8de6a134d5c3a630204e7f8238aa944a40388005bce0ce3718c410fa"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:22a9eefdbfe520778512266d0b48ff239eaa8d272fce6f5cb1ff352bed0619f4"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:397d43f8fd2621a7719b9eab6a4a8e72a1d6fa2d9c36525a30812f8e7bad3bdf"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.ApplicationModel.Background[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Security.Credentials[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)", "winrt-Windows.UI.Popups[all] (==2.0.0-beta.1)", "winrt-Windows.UI[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-foundation" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Foundation-2.0.0b1.tar.gz", hash = "sha256:976b6da942747a7ca5a179a35729d8dc163f833e03b085cf940332a5e9070d54"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:5337ac1ec260132fbff868603e73a3738d4001911226e72669b3d69c8a256d5e"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:af969e5bb9e2e41e4e86a361802528eafb5eb8fe87ec1dba6048c0702d63caa8"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:bbbfa6b3c444a1074a630fd4a1b71171be7a5c9bb07c827ad9259fadaed56cf2"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:b91bd92b1854c073acd81aa87cf8df571d2151b1dd050b6181aa36f7acc43df4"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:2f5359f25703347e827dbac982150354069030f1deecd616f7ce37ad90cbcb00"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:0f1f1978173ddf0ee6262c2edb458f62d628b9fa0df10cd1e8c78c833af3197e"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:c1d23b737f733104b91c89c507b58d0b3ef5f3234a1b608ef6dfb6dbbb8777ea"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:95de6c29e9083fe63f127b965b54dfa52a6424a93a94ce87cfad4c1900a6e887"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:4707063a5a6980e3f71aebeea5ac93101c753ec13a0b47be9ea4dbc0d5ff361e"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:d0259f1f4a1b8e20d0cbd935a889c0f7234f720645590260f9cf3850fdc1e1fa"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:15c7b324d0f59839fb4492d84bb1c870881c5c67cb94ac24c664a7c4dce1c475"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:16ad741f4d38e99f8409ba5760299d0052003255f970f49f4b8ba2e0b609c8b7"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-foundation-collections" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Foundation.Collections-2.0.0b1.tar.gz", hash = "sha256:185d30f8103934124544a40aac005fa5918a9a7cb3179f45e9863bb86e22ad43"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:042142e916a170778b7154498aae61254a1a94c552954266b73479479d24f01d"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:9f68e66055121fc1e04c4fda627834aceee6fbe922e77d6ccaecf9582e714c57"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:a4609411263cc7f5e93a9a5677b21e2ef130e26f9030bfa960b3e82595324298"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:5296858aa44c53936460a119794b80eedd6bd094016c1bf96822f92cb95ea419"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:3db1e1c80c97474e7c88b6052bd8982ca61723fd58ace11dc91a5522662e0b2a"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:c3a594e660c59f9fab04ae2f40bda7c809e8ec4748bada4424dfb02b43d4bfe1"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:0f355ee943ec5b835e694d97e9e93545a42d6fb984a61f442467789550d62c3f"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:c4a0cd2eb9f47c7ca3b66d12341cc822250bf26854a93fd58ab77f7a48dfab3a"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:744dbef50e8b8f34904083cae9ad43ac6e28facb9e166c4f123ce8e758141067"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:b7c767184aec3a3d7cba2cd84fadcd68106854efabef1a61092052294d6d6f4f"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:7c1ffe99c12f14fc4ab7027757780e6d850fa2fb23ec404a54311fbd9f1970d3"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:870fa040ed36066e4c240c35973d8b2e0d7c38cc6050a42d993715ec9e3b748c"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Foundation[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-storage-streams" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Storage.Streams-2.0.0b1.tar.gz", hash = "sha256:029d67cdc9b092d56c682740fe3c42f267dc5d3346b5c0b12ebc03f38e7d2f1f"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:49c90d4bfd539f6676226dfcb4b3574ddd6be528ffc44aa214c55af88c2de89e"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:22cc82779cada84aa2633841e25b33f3357737d912a1d9ecc1ee5a8b799b5171"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:b1750a111be32466f4f0781cbb5df195ac940690571dff4564492b921b162563"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:e79b1183ab26d9b95cf3e6dbe3f488a40605174a5a112694dbb7dbfb50899daf"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:3e90a1207eb3076f051a7785132f7b056b37343a68e9481a50c6defb3f660099"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:4da06522b4fa9cfcc046b604cc4aa1c6a887cc4bb5b8a637ed9bff8028a860bb"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:6f74f8ab8ac0d8de61c709043315361d8ac63f8144f3098d428472baadf8246a"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:5cf7c8d67836c60392d167bfe4f98ac7abcb691bfba2d19e322d0f9181f58347"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:f7f679f2c0f71791eca835856f57942ee5245094c1840a6c34bc7c2176b1bcd6"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:5beb53429fa9a11ede56b4a7cefe28c774b352dd355f7951f2a4dd7e9ec9b39a"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:f84233c4b500279d8f5840cb8c47776bc040fcecba05c6c9ab9767053698fc8b"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:cfb163ddbb435906f75ef92a768573b0190e194e1438cea5a4c1d4d32a6b9386"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Storage[all] (==2.0.0-beta.1)", "winrt-Windows.System[all] (==2.0.0-beta.1)"] + [[package]] name = "zipp" version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1036,5 +1147,5 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "2.0" -python-versions = "^3.7" -content-hash = "140c9c2d6407cbee7866fba10daa0b9db4138a3aadab89e1c06a20c28095c482" +python-versions = ">=3.8,<3.13" +content-hash = "5a9c2757ded08c2dd057e86dbb904e4c712f71d99a1613b18967b82bc793935d" diff --git a/pyproject.toml b/pyproject.toml index 7e25ef9b..e43408ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bleak" -version = "0.20.2" +version = "0.21.0" description = "Bluetooth Low Energy platform Agnostic Klient" authors = ["Henrik Blidh "] license = "MIT" @@ -22,25 +22,31 @@ classifiers = [ "Issues" = "https://github.com/hbldh/bleak/issues" [tool.poetry.dependencies] -python = "^3.7" +python = ">=3.8,<3.13" async-timeout = { version = ">= 3.0.0, < 5", python = "<3.11" } -typing-extensions = { version = "^4.2.0", python = "<3.8" } -pyobjc-core = { version = "^9.0.1", markers = "platform_system=='Darwin'" } -pyobjc-framework-CoreBluetooth = { version = "^9.0.1", markers = "platform_system=='Darwin'" } -pyobjc-framework-libdispatch = { version = "^9.0.1", markers = "platform_system=='Darwin'" } -bleak-winrt = { version = "^1.2.0", markers = "platform_system=='Windows'" } +typing-extensions = { version = ">=4.7.0", python = "<3.12" } +pyobjc-core = { version = "^9.2", markers = "platform_system=='Darwin'" } +pyobjc-framework-CoreBluetooth = { version = "^9.2", markers = "platform_system=='Darwin'" } +pyobjc-framework-libdispatch = { version = "^9.2", markers = "platform_system=='Darwin'" } +bleak-winrt = { version = "^1.2.0", markers = "platform_system=='Windows'", python = "<3.12" } +"winrt-Windows.Devices.Bluetooth" = { version = "2.0.0b1", allow-prereleases = true, markers = "platform_system=='Windows'", python = ">=3.12" } +"winrt-Windows.Devices.Bluetooth.Advertisement" = { version = "2.0.0b1", allow-prereleases = true, markers = "platform_system=='Windows'", python = ">=3.12" } +"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = { version = "2.0.0b1", allow-prereleases = true, markers = "platform_system=='Windows'", python = ">=3.12" } +"winrt-Windows.Devices.Enumeration" = { version = "2.0.0b1", allow-prereleases = true, markers = "platform_system=='Windows'", python = ">=3.12" } +"winrt-Windows.Foundation" = { version = "2.0.0b1", allow-prereleases = true, markers = "platform_system=='Windows'", python = ">=3.12" } +"winrt-Windows.Foundation.Collections" = { version = "2.0.0b1", allow-prereleases = true, markers = "platform_system=='Windows'", python = ">=3.12" } +"winrt-Windows.Storage.Streams" = { version = "2.0.0b1", allow-prereleases = true, markers = "platform_system=='Windows'", python = ">=3.12" } dbus-fast = { version = "^1.83.0", markers = "platform_system == 'Linux'" } [tool.poetry.group.docs.dependencies] -Sphinx = { version = "^5.1.1", python = ">=3.8" } +Sphinx = "^5.1.1" sphinx-rtd-theme = "^1.0.0" [tool.poetry.group.lint.dependencies] black = "^22.1.0" -flake8 = { version = "^5.0.0", python = ">=3.8" } +flake8 = "^5.0.0" [tool.poetry.group.test.dependencies] -asynctest = { version = "^0.13.0", python = "<3.8" } pytest = "^7.0.0" pytest-asyncio = "^0.19.0" pytest-cov = "^3.0.0 " diff --git a/tests/bleak/backends/bluezdbus/test_utils.py b/tests/bleak/backends/bluezdbus/test_utils.py new file mode 100644 index 00000000..cad1dff3 --- /dev/null +++ b/tests/bleak/backends/bluezdbus/test_utils.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +"""Tests for `bleak.backends.bluezdbus.utils` package.""" + +import pytest +import sys + + +@pytest.mark.skipif( + not sys.platform.startswith("linux"), reason="requires dbus-fast on Linux" +) +def test_device_path_from_characteristic_path(): + """Test device_path_from_characteristic_path.""" + from bleak.backends.bluezdbus.utils import ( # pylint: disable=import-outside-toplevel + device_path_from_characteristic_path, + ) + + assert ( + device_path_from_characteristic_path( + "/org/bluez/hci0/dev_11_22_33_44_55_66/service000c/char000d" + ) + == "/org/bluez/hci0/dev_11_22_33_44_55_66" + ) diff --git a/tests/bleak/backends/bluezdbus/test_version.py b/tests/bleak/backends/bluezdbus/test_version.py index afe3f1dc..6f36b7f5 100644 --- a/tests/bleak/backends/bluezdbus/test_version.py +++ b/tests/bleak/backends/bluezdbus/test_version.py @@ -2,16 +2,10 @@ """Tests for `bleak.backends.bluezdbus.version` package.""" -import sys -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest -if sys.version_info[:2] < (3, 8): - from asynctest.mock import CoroutineMock as AsyncMock -else: - from unittest.mock import AsyncMock - from bleak.backends.bluezdbus.version import BlueZFeatures diff --git a/tests/test_uuid.py b/tests/test_uuid.py index ab97edda..c464d16c 100644 --- a/tests/test_uuid.py +++ b/tests/test_uuid.py @@ -1,8 +1,9 @@ -from bleak.uuids import normalize_uuid_str +from bleak.uuids import normalize_uuid_16, normalize_uuid_32, normalize_uuid_str def test_uuid_length_normalization(): assert normalize_uuid_str("1801") == "00001801-0000-1000-8000-00805f9b34fb" + assert normalize_uuid_str("DAF51C01") == "daf51c01-0000-1000-8000-00805f9b34fb" def test_uuid_case_normalization(): @@ -10,3 +11,13 @@ def test_uuid_case_normalization(): normalize_uuid_str("00001801-0000-1000-8000-00805F9B34FB") == "00001801-0000-1000-8000-00805f9b34fb" ) + + +def test_uuid_16_normalization(): + assert normalize_uuid_16(0x1801) == "00001801-0000-1000-8000-00805f9b34fb" + assert normalize_uuid_16(0x1) == "00000001-0000-1000-8000-00805f9b34fb" + + +def test_uuid_32_normalization(): + assert normalize_uuid_32(0x12345678) == "12345678-0000-1000-8000-00805f9b34fb" + assert normalize_uuid_32(0x1) == "00000001-0000-1000-8000-00805f9b34fb"