diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.github/actions/setup-project/action.yml b/.github/actions/setup-project/action.yml new file mode 100644 index 0000000..4dfb260 --- /dev/null +++ b/.github/actions/setup-project/action.yml @@ -0,0 +1,43 @@ +name: Common Python + Poetry Setup + +inputs: + dependency-groups: + description: 'A comma-separated list of dependency groups to install' + default: 'main' + python-version: + description: 'The Python version to use' + default: '3.10' + +runs: + using: 'composite' + + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install poetry + shell: bash + run: | + python -m pip install poetry + poetry config virtualenvs.in-project true + + - name: Get cache key + id: cache-key + shell: bash + run: | + key=$(echo "${{ inputs.dependency-groups }}" | sed 's/,/+/') + echo "key=$key" >> "$GITHUB_OUTPUT" + + - name: Load cached venv + id: cache-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv-${{ runner.os }}-python-${{ inputs.python-version }}-groups-${{ steps.cache-key.outputs.key }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: steps.cache-dependencies.outputs.cache-hit != 'true' + shell: bash + run: poetry install --with ${{ inputs.dependency-groups }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1f2e5fc..2066d1e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,17 +16,10 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 + + - uses: './.github/actions/setup-project' with: - python-version: '3.10' - - - name: Install dependencies - run: | - python -m pip install poetry - poetry config virtualenvs.in-project true - poetry install + dependency-groups: 'docs' - name: Build documentation run: | diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index a0f29f6..1498bc6 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -3,26 +3,17 @@ name: Pre-commit on: workflow_dispatch: push: - branches-ignore: - - main jobs: - deploy: + check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Install dependencies - run: | - python -m pip install poetry - poetry config virtualenvs.in-project true - poetry install + - uses: './.github/actions/setup-project' + with: + dependency-groups: 'dev,test' - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 91fc823..162d774 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,16 +15,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 + + - uses: './.github/actions/setup-project' with: - python-version: '3.10' - - - name: Install dependencies - run: | - python -m pip install poetry - poetry config virtualenvs.in-project true - poetry install + dependency-groups: 'dev' + + - name: Prepare README + run: ./scripts/refactor_readme.py README.md - name: Build package run: poetry build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6869e20 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,56 @@ +name: Run unit tests + +on: + workflow_dispatch: + push: + +jobs: + versions: + runs-on: ubuntu-latest + + outputs: + py-versions: ${{ steps.supported-versions.outputs.py-versions }} + + steps: + - uses: actions/checkout@v4 + + - uses: './.github/actions/setup-project' + with: + dependency-groups: 'dev' + + - id: supported-versions + name: Get supported versions + run: | + set -e + echo "py-versions=$(poetry run ./scripts/supported_py_versions.py)" >> "$GITHUB_OUTPUT" + + test: + runs-on: ubuntu-latest + + needs: versions + strategy: + matrix: + py-version: ${{ fromJson(needs.versions.outputs.py-versions) }} + + steps: + - uses: actions/checkout@v4 + + - uses: './.github/actions/setup-project' + with: + python-version: ${{ matrix.py-version }} + dependency-groups: 'test' + + - name: Run unit tests + run: poetry run pytest + + results: + runs-on: ubuntu-latest + needs: test + steps: + - run: | + result="${{ needs.test.result }}" + if [[ $result == "success" || $result == "skipped" ]]; then + exit 0 + else + exit 1 + fi diff --git a/.gitignore b/.gitignore index a9c36a3..6abf495 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,4 @@ cython_debug/ account.json airtag.plist DO_NOT_COMMIT* +.direnv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 482936a..41eeaa0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.9 + rev: v0.6.3 hooks: - id: ruff args: ["--fix"] - id: ruff-format - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.350 + rev: v1.1.378 hooks: - id: pyright diff --git a/README.md b/README.md index 4e1f49c..1cde203 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # FindMy.py [![](https://img.shields.io/pypi/v/FindMy)](https://pypi.org/project/FindMy/) +[![](https://img.shields.io/pypi/dm/FindMy)](#) [![](https://img.shields.io/github/license/malmeloo/FindMy.py)](LICENSE.md) [![](https://img.shields.io/pypi/pyversions/FindMy)](#) @@ -19,13 +20,15 @@ application wishing to integrate with the Find My network. > without prior warning. > > You are encouraged to report any issues you can find on the -> [issue tracker](https://github.com/malmeloo/FindMy.py/)! +> [issue tracker](https://github.com/malmeloo/FindMy.py/issues/)! ### Features - [x] Cross-platform: no Mac needed -- [x] Fetch location reports - - [x] Apple acount sign-in +- [x] Fetch and decrypt location reports + - [x] Official accessories (AirTags, iDevices, etc.) + - [x] Custom AirTags (OpenHaystack) +- [x] Apple account sign-in - [x] SMS 2FA support - [x] Trusted Device 2FA support - [x] Scan for nearby FindMy-devices @@ -36,8 +39,7 @@ application wishing to integrate with the Find My network. ### Roadmap - [ ] Local anisette generation (without server) - - Can be done using [pyprovision](https://github.com/Dadoum/pyprovision/), - however I want to wait until Python wheels are available. + - More information: [#2](https://github.com/malmeloo/FindMy.py/issues/2) ## Installation diff --git a/examples/_login.py b/examples/_login.py index eb821e0..5412ac1 100644 --- a/examples/_login.py +++ b/examples/_login.py @@ -1,3 +1,5 @@ +# ruff: noqa: ASYNC230 + import json from pathlib import Path diff --git a/examples/device_scanner.py b/examples/device_scanner.py index 1314a32..8ab61da 100644 --- a/examples/device_scanner.py +++ b/examples/device_scanner.py @@ -1,10 +1,41 @@ import asyncio import logging -from findmy.scanner import OfflineFindingScanner +from findmy import KeyPair +from findmy.scanner import ( + NearbyOfflineFindingDevice, + OfflineFindingScanner, + SeparatedOfflineFindingDevice, +) logging.basicConfig(level=logging.INFO) +# Set if you want to check whether a specific key (or accessory!) is in the scan results. +# Make sure to enter its private key! +# Leave empty (= None) to not check. +CHECK_KEY = KeyPair.from_b64("") + + +def _print_nearby(device: NearbyOfflineFindingDevice) -> None: + print(f"NEARBY Device - {device.mac_address}") + print(f" Status byte: {device.status:x}") + print(" Extra data:") + for k, v in sorted(device.additional_data.items()): + print(f" {k:20}: {v}") + print() + + +def _print_separated(device: SeparatedOfflineFindingDevice) -> None: + print(f"SEPARATED Device - {device.mac_address}") + print(f" Public key: {device.adv_key_b64}") + print(f" Lookup key: {device.hashed_adv_key_b64}") + print(f" Status byte: {device.status:x}") + print(f" Hint byte: {device.hint:x}") + print(" Extra data:") + for k, v in sorted(device.additional_data.items()): + print(f" {k:20}: {v}") + print() + async def scan() -> None: scanner = await OfflineFindingScanner.create() @@ -12,16 +43,25 @@ async def scan() -> None: print("Scanning for FindMy-devices...") print() + scan_device = None + async for device in scanner.scan_for(10, extend_timeout=True): - print(f"Device - {device.mac_address}") - print(f" Public key: {device.adv_key_b64}") - print(f" Lookup key: {device.hashed_adv_key_b64}") - print(f" Status byte: {device.status:x}") - print(f" Hint byte: {device.hint:x}") - print(" Extra data:") - for k, v in sorted(device.additional_data.items()): - print(f" {k:20}: {v}") - print() + if isinstance(device, NearbyOfflineFindingDevice): + _print_nearby(device) + elif isinstance(device, SeparatedOfflineFindingDevice): + _print_separated(device) + else: + print(f"Unknown device: {device}") + print() + continue + + if CHECK_KEY and device.is_from(CHECK_KEY): + scan_device = device + + if scan_device: + print("Key or accessory was found in scan results! :D") + elif CHECK_KEY: + print("Selected key or accessory was not found in scan results... :c") if __name__ == "__main__": diff --git a/examples/fetch_reports.py b/examples/fetch_reports.py index 321a775..3758417 100644 --- a/examples/fetch_reports.py +++ b/examples/fetch_reports.py @@ -1,4 +1,5 @@ import logging +import sys from _login import get_account_sync @@ -8,30 +9,30 @@ # URL to (public or local) anisette server ANISETTE_SERVER = "http://localhost:6969" -# Private base64-encoded key to look up -KEY_PRIV = "" +logging.basicConfig(level=logging.INFO) -# Optional, to verify that advertisement key derivation works for your key -KEY_ADV = "" -logging.basicConfig(level=logging.DEBUG) - - -def fetch_reports(lookup_key: KeyPair) -> None: - anisette = RemoteAnisetteProvider(ANISETTE_SERVER) - acc = get_account_sync(anisette) +def fetch_reports(priv_key: str) -> int: + key = KeyPair.from_b64(priv_key) + acc = get_account_sync( + RemoteAnisetteProvider(ANISETTE_SERVER), + ) print(f"Logged in as: {acc.account_name} ({acc.first_name} {acc.last_name})") # It's that simple! - reports = acc.fetch_last_reports([lookup_key])[lookup_key] + reports = acc.fetch_last_reports(key) for report in sorted(reports): print(report) + return 1 + if __name__ == "__main__": - key = KeyPair.from_b64(KEY_PRIV) - if KEY_ADV: # verify that your adv key is correct :D - assert key.adv_key_b64 == KEY_ADV + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + print(file=sys.stderr) + print("The private key should be base64-encoded.", file=sys.stderr) + sys.exit(1) - fetch_reports(key) + sys.exit(fetch_reports(sys.argv[1])) diff --git a/examples/fetch_reports_async.py b/examples/fetch_reports_async.py index 754a05f..d267a6d 100644 --- a/examples/fetch_reports_async.py +++ b/examples/fetch_reports_async.py @@ -1,5 +1,6 @@ import asyncio import logging +import sys from _login import get_account_async @@ -9,34 +10,33 @@ # URL to (public or local) anisette server ANISETTE_SERVER = "http://localhost:6969" -# Private base64-encoded key to look up -KEY_PRIV = "" +logging.basicConfig(level=logging.INFO) -# Optional, to verify that advertisement key derivation works for your key -KEY_ADV = "" -logging.basicConfig(level=logging.DEBUG) - - -async def fetch_reports(lookup_key: KeyPair) -> None: - anisette = RemoteAnisetteProvider(ANISETTE_SERVER) - - acc = await get_account_async(anisette) +async def fetch_reports(priv_key: str) -> int: + key = KeyPair.from_b64(priv_key) + acc = await get_account_async( + RemoteAnisetteProvider(ANISETTE_SERVER), + ) try: print(f"Logged in as: {acc.account_name} ({acc.first_name} {acc.last_name})") # It's that simple! - reports = await acc.fetch_last_reports([lookup_key]) - print(reports) - + reports = await acc.fetch_last_reports(key) + for report in sorted(reports): + print(report) finally: await acc.close() + return 0 + if __name__ == "__main__": - key = KeyPair.from_b64(KEY_PRIV) - if KEY_ADV: # verify that your adv key is correct :D - assert key.adv_key_b64 == KEY_ADV + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + print(file=sys.stderr) + print("The private key should be base64-encoded.", file=sys.stderr) + sys.exit(1) - asyncio.run(fetch_reports(key)) + asyncio.run(fetch_reports(sys.argv[1])) diff --git a/examples/real_airtag.py b/examples/real_airtag.py index e669050..5eeb858 100644 --- a/examples/real_airtag.py +++ b/examples/real_airtag.py @@ -1,82 +1,52 @@ """ Example showing how to fetch locations of an AirTag, or any other FindMy accessory. """ + from __future__ import annotations -import plistlib -from datetime import datetime, timedelta, timezone +import logging +import sys from pathlib import Path from _login import get_account_sync -from findmy import FindMyAccessory, KeyPair +from findmy import FindMyAccessory from findmy.reports import RemoteAnisetteProvider # URL to (public or local) anisette server ANISETTE_SERVER = "http://localhost:6969" -# Path to a .plist dumped from the Find My app. -PLIST_PATH = Path("airtag.plist") - -# == The variables below are auto-filled from the plist!! == - -with PLIST_PATH.open("rb") as f: - device_data = plistlib.load(f) - -# PRIVATE master key. 28 (?) bytes. -MASTER_KEY = device_data["privateKey"]["key"]["data"][-28:] - -# "Primary" shared secret. 32 bytes. -SKN = device_data["sharedSecret"]["key"]["data"] - -# "Secondary" shared secret. 32 bytes. -SKS = device_data["secondarySharedSecret"]["key"]["data"] - -# "Paired at" timestamp (UTC) -PAIRED_AT = device_data["pairingDate"].replace(tzinfo=timezone.utc) - +logging.basicConfig(level=logging.INFO) -def _gen_keys(airtag: FindMyAccessory, _from: datetime, to: datetime) -> set[KeyPair]: - keys = set() - while _from < to: - keys.update(airtag.keys_at(_from)) - _from += timedelta(minutes=15) - - return keys - - -def main() -> None: +def main(plist_path: str) -> int: # Step 0: create an accessory key generator - airtag = FindMyAccessory(MASTER_KEY, SKN, SKS, PAIRED_AT) - - # Step 1: Generate the accessory's private keys, - # starting from 7 days ago until now (12 hour margin) - fetch_to = datetime.now(tz=timezone.utc).astimezone() + timedelta(hours=12) - fetch_from = fetch_to - timedelta(days=8) + with Path(plist_path).open("rb") as f: + airtag = FindMyAccessory.from_plist(f) - print(f"Generating keys from {fetch_from} to {fetch_to} ...") - lookup_keys = _gen_keys(airtag, fetch_from, fetch_to) - - print(f"Generated {len(lookup_keys)} keys") - - # Step 2: log into an Apple account + # Step 1: log into an Apple account print("Logging into account") anisette = RemoteAnisetteProvider(ANISETTE_SERVER) acc = get_account_sync(anisette) - # step 3: fetch reports! + # step 2: fetch reports! print("Fetching reports") - reports = acc.fetch_reports(list(lookup_keys), fetch_from, fetch_to) + reports = acc.fetch_last_reports(airtag) - # step 4: print 'em - # reports are in {key: [report]} format, but we only really care about the reports + # step 3: print 'em print() print("Location reports:") - reports = sorted([r for rs in reports.values() for r in rs]) - for report in reports: + for report in sorted(reports): print(f" - {report}") + return 0 + if __name__ == "__main__": - main() + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} ", file=sys.stderr) + print(file=sys.stderr) + print("The plist file should be dumped from MacOS's FindMy app.", file=sys.stderr) + sys.exit(1) + + sys.exit(main(sys.argv[1])) diff --git a/findmy/__init__.py b/findmy/__init__.py index f0fc461..7431bad 100644 --- a/findmy/__init__.py +++ b/findmy/__init__.py @@ -1,4 +1,5 @@ """A package providing everything you need to work with Apple's FindMy network.""" + from . import errors, keys, reports, scanner from .accessory import FindMyAccessory from .keys import KeyPair diff --git a/findmy/accessory.py b/findmy/accessory.py index b62f3ad..230239a 100644 --- a/findmy/accessory.py +++ b/findmy/accessory.py @@ -3,11 +3,14 @@ Accessories could be anything ranging from AirTags to iPhones. """ + from __future__ import annotations import logging -from datetime import datetime, timedelta -from typing import Generator, overload +import plistlib +from abc import ABC, abstractmethod +from datetime import datetime, timedelta, timezone +from typing import IO, Generator, overload from typing_extensions import override @@ -17,10 +20,52 @@ logging.getLogger(__name__) -class FindMyAccessory: +class RollingKeyPairSource(ABC): + """A class that generates rolling `KeyPair`s.""" + + @property + @abstractmethod + def interval(self) -> timedelta: + """KeyPair rollover interval.""" + + @abstractmethod + def keys_at(self, ind: int | datetime) -> set[KeyPair]: + """Generate potential key(s) occurring at a certain index or timestamp.""" + raise NotImplementedError + + @overload + def keys_between(self, start: int, end: int) -> set[KeyPair]: + pass + + @overload + def keys_between(self, start: datetime, end: datetime) -> set[KeyPair]: + pass + + def keys_between(self, start: int | datetime, end: int | datetime) -> set[KeyPair]: + """Generate potential key(s) occurring between two indices or timestamps.""" + keys: set[KeyPair] = set() + + if isinstance(start, int) and isinstance(end, int): + while start < end: + keys.update(self.keys_at(start)) + + start += 1 + elif isinstance(start, datetime) and isinstance(end, datetime): + while start < end: + keys.update(self.keys_at(start)) + + start += self.interval + else: + msg = "Invalid start/end type" + raise TypeError(msg) + + return keys + + +class FindMyAccessory(RollingKeyPairSource): """A findable Find My-accessory using official key rollover.""" - def __init__( # noqa: PLR0913 + def __init__( self, master_key: bytes, skn: bytes, @@ -47,8 +92,20 @@ def __init__( # noqa: PLR0913 self._name = name + @property + @override + def interval(self) -> timedelta: + """Official FindMy accessory rollover interval (15 minutes).""" + return timedelta(minutes=15) + + @override def keys_at(self, ind: int | datetime) -> set[KeyPair]: """Get the potential primary and secondary keys active at a certain time or index.""" + if isinstance(ind, datetime) and ind < self._paired_at: + return set() + if isinstance(ind, int) and ind < 0: + return set() + secondary_offset = 0 if isinstance(ind, datetime): @@ -88,6 +145,30 @@ def keys_at(self, ind: int | datetime) -> set[KeyPair]: return possible_keys + @classmethod + def from_plist(cls, plist: IO[bytes]) -> FindMyAccessory: + """Create a FindMyAccessory from a .plist file dumped from the FindMy app.""" + device_data = plistlib.load(plist) + + # PRIVATE master key. 28 (?) bytes. + master_key = device_data["privateKey"]["key"]["data"][-28:] + + # "Primary" shared secret. 32 bytes. + skn = device_data["sharedSecret"]["key"]["data"] + + # "Secondary" shared secret. 32 bytes. + if "secondarySharedSecret" in device_data: + # AirTag + sks = device_data["secondarySharedSecret"]["key"]["data"] + else: + # iDevice + sks = device_data["secureLocationsSharedSecret"]["key"]["data"] + + # "Paired at" timestamp (UTC) + paired_at = device_data["pairingDate"].replace(tzinfo=timezone.utc) + + return cls(master_key, skn, sks, paired_at) + class AccessoryKeyGenerator(KeyGenerator[KeyPair]): """KeyPair generator. Uses the same algorithm internally as FindMy accessories do.""" @@ -155,12 +236,10 @@ def __next__(self) -> KeyPair: return self._get_keypair(self._iter_ind) @overload - def __getitem__(self, val: int) -> KeyPair: - ... + def __getitem__(self, val: int) -> KeyPair: ... @overload - def __getitem__(self, val: slice) -> Generator[KeyPair, None, None]: - ... + def __getitem__(self, val: slice) -> Generator[KeyPair, None, None]: ... @override def __getitem__(self, val: int | slice) -> KeyPair | Generator[KeyPair, None, None]: diff --git a/findmy/errors.py b/findmy/errors.py index 2eda554..fbf88c7 100644 --- a/findmy/errors.py +++ b/findmy/errors.py @@ -5,6 +5,10 @@ class InvalidCredentialsError(Exception): """Raised when credentials are incorrect.""" +class UnauthorizedError(Exception): + """Raised when an authorization error occurs.""" + + class UnhandledProtocolError(RuntimeError): """ Raised when an unexpected error occurs while communicating with Apple servers. diff --git a/findmy/keys.py b/findmy/keys.py index f51c5bf..a5c8932 100644 --- a/findmy/keys.py +++ b/findmy/keys.py @@ -1,4 +1,5 @@ """Module to work with private and public keys as used in FindMy accessories.""" + from __future__ import annotations import base64 @@ -22,28 +23,18 @@ class KeyType(Enum): SECONDARY = 2 -class HasPublicKey(ABC): +class HasHashedPublicKey(ABC): """ - ABC for anything that has a public FindMy-key. + ABC for anything that has a public, hashed FindMy-key. - Also called an "advertisement" key, since it is the key that is advertised by findable devices. + Also called a "hashed advertisement" key or "lookup" key. """ @property @abstractmethod - def adv_key_bytes(self) -> bytes: - """Return the advertised (public) key as bytes.""" - raise NotImplementedError - - @property - def adv_key_b64(self) -> str: - """Return the advertised (public) key as a base64-encoded string.""" - return base64.b64encode(self.adv_key_bytes).decode("ascii") - - @property def hashed_adv_key_bytes(self) -> bytes: """Return the hashed advertised (public) key as bytes.""" - return hashlib.sha256(self.adv_key_bytes).digest() + raise NotImplementedError @property def hashed_adv_key_b64(self) -> str: @@ -52,14 +43,39 @@ def hashed_adv_key_b64(self) -> str: @override def __hash__(self) -> int: - return crypto.bytes_to_int(self.adv_key_bytes) + return crypto.bytes_to_int(self.hashed_adv_key_bytes) @override def __eq__(self, other: object) -> bool: - if not isinstance(other, HasPublicKey): + if not isinstance(other, HasHashedPublicKey): return NotImplemented - return self.adv_key_bytes == other.adv_key_bytes + return self.hashed_adv_key_bytes == other.hashed_adv_key_bytes + + +class HasPublicKey(HasHashedPublicKey, ABC): + """ + ABC for anything that has a public FindMy-key. + + Also called an "advertisement" key, since it is the key that is advertised by findable devices. + """ + + @property + @abstractmethod + def adv_key_bytes(self) -> bytes: + """Return the advertised (public) key as bytes.""" + raise NotImplementedError + + @property + def adv_key_b64(self) -> str: + """Return the advertised (public) key as a base64-encoded string.""" + return base64.b64encode(self.adv_key_bytes).decode("ascii") + + @property + @override + def hashed_adv_key_bytes(self) -> bytes: + """See `HasHashedPublicKey.hashed_adv_key_bytes`.""" + return hashlib.sha256(self.adv_key_bytes).digest() class KeyPair(HasPublicKey): @@ -141,13 +157,11 @@ def __next__(self) -> K: @overload @abstractmethod - def __getitem__(self, val: int) -> K: - ... + def __getitem__(self, val: int) -> K: ... @overload @abstractmethod - def __getitem__(self, val: slice) -> Generator[K, None, None]: - ... + def __getitem__(self, val: slice) -> Generator[K, None, None]: ... @abstractmethod def __getitem__(self, val: int | slice) -> K | Generator[K, None, None]: diff --git a/findmy/reports/__init__.py b/findmy/reports/__init__.py index a5c3851..dfba250 100644 --- a/findmy/reports/__init__.py +++ b/findmy/reports/__init__.py @@ -1,4 +1,5 @@ """Code related to fetching location reports.""" + from .account import AppleAccount, AsyncAppleAccount from .anisette import BaseAnisetteProvider, RemoteAnisetteProvider from .state import LoginState diff --git a/findmy/reports/account.py b/findmy/reports/account.py index 1efb967..59dbe3d 100644 --- a/findmy/reports/account.py +++ b/findmy/reports/account.py @@ -1,4 +1,5 @@ """Module containing most of the code necessary to interact with an Apple account.""" + from __future__ import annotations import asyncio @@ -14,22 +15,26 @@ TYPE_CHECKING, Any, Callable, - Concatenate, - ParamSpec, Sequence, TypedDict, TypeVar, cast, + overload, ) import bs4 import srp._pysrp as srp -from typing_extensions import override +from typing_extensions import Concatenate, ParamSpec, override -from findmy.errors import InvalidCredentialsError, InvalidStateError, UnhandledProtocolError +from findmy.errors import ( + InvalidCredentialsError, + InvalidStateError, + UnauthorizedError, + UnhandledProtocolError, +) from findmy.util import crypto from findmy.util.closable import Closable -from findmy.util.http import HttpSession, decode_plist +from findmy.util.http import HttpResponse, HttpSession, decode_plist from .reports import LocationReport, LocationReportsFetcher from .state import LoginState @@ -44,7 +49,8 @@ ) if TYPE_CHECKING: - from findmy.keys import KeyPair + from findmy.accessory import RollingKeyPairSource + from findmy.keys import HasHashedPublicKey from findmy.util.types import MaybeCoro from .anisette import BaseAnisetteProvider @@ -215,28 +221,79 @@ def td_2fa_submit(self, code: str) -> MaybeCoro[LoginState]: """ raise NotImplementedError + @overload @abstractmethod def fetch_reports( self, - keys: Sequence[KeyPair], + keys: HasHashedPublicKey, date_from: datetime, date_to: datetime | None, - ) -> MaybeCoro[dict[KeyPair, list[LocationReport]]]: + ) -> MaybeCoro[list[LocationReport]]: ... + + @overload + @abstractmethod + def fetch_reports( + self, + keys: Sequence[HasHashedPublicKey], + date_from: datetime, + date_to: datetime | None, + ) -> MaybeCoro[dict[HasHashedPublicKey, list[LocationReport]]]: ... + + @overload + @abstractmethod + def fetch_reports( + self, + keys: RollingKeyPairSource, + date_from: datetime, + date_to: datetime | None, + ) -> MaybeCoro[list[LocationReport]]: ... + + @abstractmethod + def fetch_reports( + self, + keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource, + date_from: datetime, + date_to: datetime | None, + ) -> MaybeCoro[list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]]: """ - Fetch location reports for a sequence of `KeyPair`s between `date_from` and `date_end`. + Fetch location reports for `HasHashedPublicKey`s between `date_from` and `date_end`. - Returns a dictionary mapping `KeyPair`s to a list of their location reports. + Returns a dictionary mapping `HasHashedPublicKey`s to a list of their location reports. """ raise NotImplementedError + @overload + @abstractmethod + def fetch_last_reports( + self, + keys: HasHashedPublicKey, + hours: int = 7 * 24, + ) -> MaybeCoro[list[LocationReport]]: ... + + @overload @abstractmethod def fetch_last_reports( self, - keys: Sequence[KeyPair], + keys: Sequence[HasHashedPublicKey], hours: int = 7 * 24, - ) -> MaybeCoro[dict[KeyPair, list[LocationReport]]]: + ) -> MaybeCoro[dict[HasHashedPublicKey, list[LocationReport]]]: ... + + @overload + @abstractmethod + def fetch_last_reports( + self, + keys: RollingKeyPairSource, + hours: int = 7 * 24, + ) -> MaybeCoro[list[LocationReport]]: ... + + @abstractmethod + def fetch_last_reports( + self, + keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource, + hours: int = 7 * 24, + ) -> MaybeCoro[list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]]: """ - Fetch location reports for a sequence of `KeyPair`s for the last `hours` hours. + Fetch location reports for a sequence of `HasHashedPublicKey`s for the last `hours` hours. Utility method as an alternative to using `BaseAppleAccount.fetch_reports` directly. """ @@ -526,27 +583,72 @@ async def fetch_raw_reports(self, start: int, end: int, ids: list[str]) -> dict[ ) data = {"search": [{"startDate": start, "endDate": end, "ids": ids}]} - r = await self._http.post( - self._ENDPOINT_REPORTS_FETCH, - auth=auth, - headers=await self.get_anisette_headers(), - json=data, - ) - resp = r.json() - if not r.ok or resp["statusCode"] != "200": - msg = f"Failed to fetch reports: {resp['statusCode']}" + async def _do_request() -> HttpResponse: + return await self._http.post( + self._ENDPOINT_REPORTS_FETCH, + auth=auth, + headers=await self.get_anisette_headers(), + json=data, + ) + + r = await _do_request() + if r.status_code == 401: + logging.info("Got 401 while fetching reports, redoing login") + + new_state = await self._gsa_authenticate() + if new_state != LoginState.AUTHENTICATED: + msg = f"Unexpected login state after reauth: {new_state}. Please log in again." + raise UnauthorizedError(msg) + await self._login_mobileme() + + r = await _do_request() + + if r.status_code == 401: + msg = "Not authorized to fetch reports." + raise UnauthorizedError(msg) + + try: + resp = r.json() + except json.JSONDecodeError: + resp = {} + if not r.ok or resp.get("statusCode") != "200": + msg = f"Failed to fetch reports: {resp.get('statusCode')}" raise UnhandledProtocolError(msg) return resp + @overload + async def fetch_reports( + self, + keys: HasHashedPublicKey, + date_from: datetime, + date_to: datetime | None, + ) -> list[LocationReport]: ... + + @overload + async def fetch_reports( + self, + keys: Sequence[HasHashedPublicKey], + date_from: datetime, + date_to: datetime | None, + ) -> dict[HasHashedPublicKey, list[LocationReport]]: ... + + @overload + async def fetch_reports( + self, + keys: RollingKeyPairSource, + date_from: datetime, + date_to: datetime | None, + ) -> list[LocationReport]: ... + @require_login_state(LoginState.LOGGED_IN) @override async def fetch_reports( self, - keys: Sequence[KeyPair], + keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource, date_from: datetime, date_to: datetime | None, - ) -> dict[KeyPair, list[LocationReport]]: + ) -> list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]: """See `BaseAppleAccount.fetch_reports`.""" date_to = date_to or datetime.now().astimezone() @@ -556,20 +658,41 @@ async def fetch_reports( keys, ) + @overload + async def fetch_last_reports( + self, + keys: HasHashedPublicKey, + hours: int = 7 * 24, + ) -> list[LocationReport]: ... + + @overload + async def fetch_last_reports( + self, + keys: Sequence[HasHashedPublicKey], + hours: int = 7 * 24, + ) -> dict[HasHashedPublicKey, list[LocationReport]]: ... + + @overload + async def fetch_last_reports( + self, + keys: RollingKeyPairSource, + hours: int = 7 * 24, + ) -> list[LocationReport]: ... + @require_login_state(LoginState.LOGGED_IN) @override async def fetch_last_reports( self, - keys: Sequence[KeyPair], + keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource, hours: int = 7 * 24, - ) -> dict[KeyPair, list[LocationReport]]: + ) -> list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]: """See `BaseAppleAccount.fetch_last_reports`.""" end = datetime.now(tz=timezone.utc) start = end - timedelta(hours=hours) return await self.fetch_reports(keys, start, end) - @require_login_state(LoginState.LOGGED_OUT, LoginState.REQUIRE_2FA) + @require_login_state(LoginState.LOGGED_OUT, LoginState.REQUIRE_2FA, LoginState.LOGGED_IN) async def _gsa_authenticate( self, username: str | None = None, @@ -599,13 +722,13 @@ async def _gsa_authenticate( msg = "Email verification failed: " + r["Status"].get("em") raise InvalidCredentialsError(msg) sp = r.get("sp") - if sp != "s2k": - msg = f"This implementation only supports s2k. Server returned {sp}" + if not isinstance(sp, str) or sp not in {"s2k", "s2k_fo"}: + msg = f"This implementation only supports s2k and sk2_fo. Server returned {sp}" raise UnhandledProtocolError(msg) logging.debug("Attempting password challenge") - usr.p = crypto.encrypt_password(self._password, r["s"], r["i"]) + usr.p = crypto.encrypt_password(self._password, r["s"], r["i"], sp) m1 = usr.process_challenge(r["s"], r["B"]) if m1 is None: msg = "Failed to process challenge" @@ -695,9 +818,9 @@ async def _login_mobileme(self) -> LoginState: data = resp.plist() mobileme_data = data.get("delegates", {}).get("com.apple.mobileme", {}) - status = mobileme_data.get("status") + status = mobileme_data.get("status") or data.get("status") if status != 0: - status_message = mobileme_data.get("status-message") + status_message = mobileme_data.get("status-message") or data.get("status-message") msg = f"com.apple.mobileme login failed with status {status}: {status_message}" raise UnhandledProtocolError(msg) @@ -894,23 +1017,68 @@ def td_2fa_submit(self, code: str) -> LoginState: coro = self._asyncacc.td_2fa_submit(code) return self._evt_loop.run_until_complete(coro) + @overload + def fetch_reports( + self, + keys: HasHashedPublicKey, + date_from: datetime, + date_to: datetime | None, + ) -> list[LocationReport]: ... + + @overload + def fetch_reports( + self, + keys: Sequence[HasHashedPublicKey], + date_from: datetime, + date_to: datetime | None, + ) -> dict[HasHashedPublicKey, list[LocationReport]]: ... + + @overload + def fetch_reports( + self, + keys: RollingKeyPairSource, + date_from: datetime, + date_to: datetime | None, + ) -> list[LocationReport]: ... + @override def fetch_reports( self, - keys: Sequence[KeyPair], + keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource, date_from: datetime, date_to: datetime | None, - ) -> dict[KeyPair, list[LocationReport]]: + ) -> list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]: """See `AsyncAppleAccount.fetch_reports`.""" coro = self._asyncacc.fetch_reports(keys, date_from, date_to) return self._evt_loop.run_until_complete(coro) + @overload + def fetch_last_reports( + self, + keys: HasHashedPublicKey, + hours: int = 7 * 24, + ) -> list[LocationReport]: ... + + @overload + def fetch_last_reports( + self, + keys: Sequence[HasHashedPublicKey], + hours: int = 7 * 24, + ) -> dict[HasHashedPublicKey, list[LocationReport]]: ... + + @overload + def fetch_last_reports( + self, + keys: RollingKeyPairSource, + hours: int = 7 * 24, + ) -> list[LocationReport]: ... + @override def fetch_last_reports( self, - keys: Sequence[KeyPair], + keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource, hours: int = 7 * 24, - ) -> dict[KeyPair, list[LocationReport]]: + ) -> list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]: """See `AsyncAppleAccount.fetch_last_reports`.""" coro = self._asyncacc.fetch_last_reports(keys, hours) return self._evt_loop.run_until_complete(coro) diff --git a/findmy/reports/anisette.py b/findmy/reports/anisette.py index 3bfe4e9..bcf2bf1 100644 --- a/findmy/reports/anisette.py +++ b/findmy/reports/anisette.py @@ -1,9 +1,11 @@ """Module for Anisette header providers.""" + from __future__ import annotations import base64 import locale import logging +import time from abc import ABC, abstractmethod from datetime import datetime, timezone @@ -160,6 +162,8 @@ async def get_cpd( class RemoteAnisetteProvider(BaseAnisetteProvider): """Anisette provider. Fetches headers from a remote Anisette server.""" + _ANISETTE_DATA_VALID_FOR = 30 + def __init__(self, server_url: str) -> None: """Initialize the provider with URL to te remote server.""" super().__init__() @@ -169,6 +173,7 @@ def __init__(self, server_url: str) -> None: self._http = HttpSession() self._anisette_data: dict[str, str] | None = None + self._anisette_data_expires_at: float = 0 @property @override @@ -197,11 +202,12 @@ async def get_headers( with_client_info: bool = False, ) -> dict[str, str]: """See `BaseAnisetteProvider.get_headers`_.""" - if self._anisette_data is None: + if self._anisette_data is None or time.time() >= self._anisette_data_expires_at: logging.info("Fetching anisette data from %s", self._server_url) r = await self._http.get(self._server_url) self._anisette_data = r.json() + self._anisette_data_expires_at = time.time() + self._ANISETTE_DATA_VALID_FOR return await super().get_headers(user_id, device_id, serial, with_client_info) diff --git a/findmy/reports/reports.py b/findmy/reports/reports.py index a8a83b9..3f17be3 100644 --- a/findmy/reports/reports.py +++ b/findmy/reports/reports.py @@ -1,11 +1,12 @@ """Module providing functionality to look up location reports.""" + from __future__ import annotations import base64 import hashlib import logging import struct -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Sequence, overload from cryptography.hazmat.backends import default_backend @@ -13,7 +14,8 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from typing_extensions import override -from findmy.keys import KeyPair +from findmy.accessory import RollingKeyPairSource +from findmy.keys import HasHashedPublicKey, KeyPair if TYPE_CHECKING: from .account import AsyncAppleAccount @@ -21,128 +23,177 @@ logging.getLogger(__name__) -def _decrypt_payload(payload: bytes, key: KeyPair) -> bytes: - eph_key = ec.EllipticCurvePublicKey.from_encoded_point( - ec.SECP224R1(), - payload[5:62], - ) - shared_key = key.dh_exchange(eph_key) - symmetric_key = hashlib.sha256( - shared_key + b"\x00\x00\x00\x01" + payload[5:62], - ).digest() - - decryption_key = symmetric_key[:16] - iv = symmetric_key[16:] - enc_data = payload[62:72] - tag = payload[72:] - - decryptor = Cipher( - algorithms.AES(decryption_key), - modes.GCM(iv, tag), - default_backend(), - ).decryptor() - return decryptor.update(enc_data) + decryptor.finalize() - +class LocationReport(HasHashedPublicKey): + """Location report corresponding to a certain `HasHashedPublicKey`.""" -class LocationReport: - """Location report corresponding to a certain `KeyPair`.""" - - def __init__( # noqa: PLR0913 + def __init__( self, - key: KeyPair, - publish_date: datetime, - timestamp: datetime, - description: str, - lat: float, - lng: float, - confidence: int, - status: int, + payload: bytes, + hashed_adv_key: bytes, + published_at: datetime, + description: str = "", ) -> None: """Initialize a `KeyReport`. You should probably use `KeyReport.from_payload` instead.""" - self._key = key - self._publish_date = publish_date - self._timestamp = timestamp - self._description = description + self._payload: bytes = payload + self._hashed_adv_key: bytes = hashed_adv_key + self._published_at: datetime = published_at + self._description: str = description - self._lat = lat - self._lng = lng - self._confidence = confidence + self._decrypted_data: tuple[KeyPair, bytes] | None = None - self._status = status + @property + @override + def hashed_adv_key_bytes(self) -> bytes: + """See `HasHashedPublicKey.hashed_adv_key_bytes`.""" + return self._hashed_adv_key @property def key(self) -> KeyPair: - """The `KeyPair` corresponding to this location report.""" - return self._key + """`KeyPair` using which this report was decrypted.""" + if not self.is_decrypted: + msg = "Full key is unavailable while the report is encrypted." + raise RuntimeError(msg) + assert self._decrypted_data is not None + + return self._decrypted_data[0] @property - def published_at(self) -> datetime: - """The `datetime` when this report was published by a device.""" - return self._publish_date + def payload(self) -> bytes: + """Full (partially encrypted) payload of the report, as retrieved from Apple.""" + return self._payload @property - def timestamp(self) -> datetime: - """The `datetime` when this report was recorded by a device.""" - return self._timestamp + def is_decrypted(self) -> bool: + """Whether the report is currently decrypted.""" + return self._decrypted_data is not None + + def decrypt(self, key: KeyPair) -> None: + """Decrypt the report using its corresponding `KeyPair`.""" + if key.hashed_adv_key_bytes != self._hashed_adv_key: + msg = "Cannot decrypt with this key!" + raise ValueError(msg) + + if self.is_decrypted: + return + + encrypted_data = self._payload[4:] + + # Fix decryption for new report format via MacOS 14+ + # See: https://github.com/MatthewKuKanich/FindMyFlipper/issues/61#issuecomment-2065003410 + if len(encrypted_data) == 85: + encrypted_data = encrypted_data[1:] + + eph_key = ec.EllipticCurvePublicKey.from_encoded_point( + ec.SECP224R1(), + encrypted_data[1:58], + ) + shared_key = key.dh_exchange(eph_key) + symmetric_key = hashlib.sha256( + shared_key + b"\x00\x00\x00\x01" + encrypted_data[1:58], + ).digest() + + decryption_key = symmetric_key[:16] + iv = symmetric_key[16:] + enc_data = encrypted_data[58:68] + tag = encrypted_data[68:] + + decryptor = Cipher( + algorithms.AES(decryption_key), + modes.GCM(iv, tag), + default_backend(), + ).decryptor() + decrypted_payload = decryptor.update(enc_data) + decryptor.finalize() + + self._decrypted_data = (key, decrypted_payload) + + @property + def published_at(self) -> datetime: + """The `datetime` when this report was published by a device.""" + return self._published_at @property def description(self) -> str: """Description of the location report as published by Apple.""" return self._description + @property + def timestamp(self) -> datetime: + """The `datetime` when this report was recorded by a device.""" + timestamp_int = int.from_bytes(self._payload[0:4], "big") + (60 * 60 * 24 * 11323) + return datetime.fromtimestamp(timestamp_int, tz=timezone.utc).astimezone() + @property def latitude(self) -> float: """Latitude of the location of this report.""" - return self._lat + if not self.is_decrypted: + msg = "Latitude is unavailable while the report is encrypted." + raise RuntimeError(msg) + assert self._decrypted_data is not None + + lat_bytes = self._decrypted_data[1][:4] + return struct.unpack(">i", lat_bytes)[0] / 10000000 @property def longitude(self) -> float: """Longitude of the location of this report.""" - return self._lng + if not self.is_decrypted: + msg = "Longitude is unavailable while the report is encrypted." + raise RuntimeError(msg) + assert self._decrypted_data is not None + + lon_bytes = self._decrypted_data[1][4:8] + return struct.unpack(">i", lon_bytes)[0] / 10000000 @property def confidence(self) -> int: """Confidence of the location of this report.""" - return self._confidence + if not self.is_decrypted: + msg = "Confidence is unavailable while the report is encrypted." + raise RuntimeError(msg) + assert self._decrypted_data is not None + + conf_bytes = self._decrypted_data[1][8:9] + return int.from_bytes(conf_bytes, "big") @property def status(self) -> int: """Status byte of the accessory as recorded by a device, as an integer.""" - return self._status - - @classmethod - def from_payload( - cls, - key: KeyPair, - publish_date: datetime, - description: str, - payload: bytes, - ) -> LocationReport: + if not self.is_decrypted: + msg = "Status byte is unavailable while the report is encrypted." + raise RuntimeError(msg) + assert self._decrypted_data is not None + + status_bytes = self._decrypted_data[1][9:10] + return int.from_bytes(status_bytes, "big") + + @override + def __eq__(self, other: object) -> bool: """ - Create a `KeyReport` from fields and a payload as reported by Apple. + Compare two report instances. - Requires a `KeyPair` to decrypt the report's payload. + Two reports are considered equal iff they correspond to the same key, + were reported at the same timestamp and represent the same physical location. """ - timestamp_int = int.from_bytes(payload[0:4], "big") + (60 * 60 * 24 * 11323) - timestamp = datetime.fromtimestamp(timestamp_int, tz=timezone.utc).astimezone() - - data = _decrypt_payload(payload, key) - latitude = struct.unpack(">i", data[0:4])[0] / 10000000 - longitude = struct.unpack(">i", data[4:8])[0] / 10000000 - confidence = int.from_bytes(data[8:9], "big") - status = int.from_bytes(data[9:10], "big") - - return cls( - key, - publish_date, - timestamp, - description, - latitude, - longitude, - confidence, - status, + if not isinstance(other, LocationReport): + return NotImplemented + + return ( + super().__eq__(other) + and self.timestamp == other.timestamp + and self.latitude == other.latitude + and self.longitude == other.longitude ) + @override + def __hash__(self) -> int: + """ + Get the hash of this instance. + + Two instances will have the same hash iff they correspond to the same key, + were reported at the same timestamp and represent the same physical location. + """ + return hash((self.hashed_adv_key_bytes, self.timestamp, self.latitude, self.longitude)) + def __lt__(self, other: LocationReport) -> bool: """ Compare against another `KeyReport`. @@ -157,10 +208,11 @@ def __lt__(self, other: LocationReport) -> bool: @override def __repr__(self) -> str: """Human-readable string representation of the location report.""" - return ( - f"KeyReport(key={self._key.hashed_adv_key_b64}, timestamp={self._timestamp}," - f" lat={self._lat}, lng={self._lng})" - ) + msg = f"KeyReport(hashed_adv_key={self.hashed_adv_key_b64}, timestamp={self.timestamp}" + if self.is_decrypted: + msg += f", lat={self.latitude}, lon={self.longitude}" + msg += ")" + return msg class LocationReportsFetcher: @@ -179,53 +231,79 @@ async def fetch_reports( self, date_from: datetime, date_to: datetime, - device: KeyPair, - ) -> list[LocationReport]: - ... + device: HasHashedPublicKey, + ) -> list[LocationReport]: ... + + @overload + async def fetch_reports( + self, + date_from: datetime, + date_to: datetime, + device: Sequence[HasHashedPublicKey], + ) -> dict[HasHashedPublicKey, list[LocationReport]]: ... @overload async def fetch_reports( self, date_from: datetime, date_to: datetime, - device: Sequence[KeyPair], - ) -> dict[KeyPair, list[LocationReport]]: - ... + device: RollingKeyPairSource, + ) -> list[LocationReport]: ... async def fetch_reports( self, date_from: datetime, date_to: datetime, - device: KeyPair | Sequence[KeyPair], - ) -> list[LocationReport] | dict[KeyPair, list[LocationReport]]: + device: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource, + ) -> list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]: """ Fetch location reports for a certain device. - When ``device`` is a single :class:`.KeyPair`, this method will return - a list of location reports corresponding to that pair. - When ``device`` is a sequence of :class:`.KeyPair`s, it will return a dictionary - with the :class:`.KeyPair` as key, and a list of location reports as value. + When ``device`` is a single :class:`.HasHashedPublicKey`, this method will return + a list of location reports corresponding to that key. + When ``device`` is a sequence of :class:`.HasHashedPublicKey`s, it will return a dictionary + with the :class:`.HasHashedPublicKey` as key, and a list of location reports as value. + When ``device`` is a :class:`.RollingKeyPairSource`, it will return a list of + location reports corresponding to that source. """ - # single KeyPair - if isinstance(device, KeyPair): + # single key + if isinstance(device, HasHashedPublicKey): return await self._fetch_reports(date_from, date_to, [device]) - # sequence of KeyPairs (fetch 256 max at a time) + # key generator + # add 12h margin to the generator + if isinstance(device, RollingKeyPairSource): + keys = list( + device.keys_between( + date_from - timedelta(hours=12), + date_to + timedelta(hours=12), + ), + ) + else: + keys = device + + # sequence of keys (fetch 256 max at a time) reports: list[LocationReport] = [] - for key_offset in range(0, len(device), 256): - chunk = device[key_offset : key_offset + 256] + for key_offset in range(0, len(keys), 256): + chunk = keys[key_offset : key_offset + 256] reports.extend(await self._fetch_reports(date_from, date_to, chunk)) - res: dict[KeyPair, list[LocationReport]] = {key: [] for key in device} + if isinstance(device, RollingKeyPairSource): + return reports + + res: dict[HasHashedPublicKey, list[LocationReport]] = {key: [] for key in keys} for report in reports: - res[report.key].append(report) + for key in res: + if key.hashed_adv_key_bytes == report.hashed_adv_key_bytes: + res[key].append(report) + break return res async def _fetch_reports( self, date_from: datetime, date_to: datetime, - keys: Sequence[KeyPair], + keys: Sequence[HasHashedPublicKey], ) -> list[LocationReport]: logging.debug("Fetching reports for %s keys", len(keys)) @@ -234,17 +312,24 @@ async def _fetch_reports( ids = [key.hashed_adv_key_b64 for key in keys] data = await self._account.fetch_raw_reports(start_date, end_date, ids) - id_to_key: dict[str, KeyPair] = {key.hashed_adv_key_b64: key for key in keys} + id_to_key: dict[bytes, HasHashedPublicKey] = {key.hashed_adv_key_bytes: key for key in keys} reports: list[LocationReport] = [] for report in data.get("results", []): - key = id_to_key[report["id"]] + payload = base64.b64decode(report["payload"]) + hashed_adv_key = base64.b64decode(report["id"]) date_published = datetime.fromtimestamp( report.get("datePublished", 0) / 1000, tz=timezone.utc, ).astimezone() description = report.get("description", "") - payload = base64.b64decode(report["payload"]) - reports.append(LocationReport.from_payload(key, date_published, description, payload)) + loc_report = LocationReport(payload, hashed_adv_key, date_published, description) + + # pre-decrypt if possible + key = id_to_key[hashed_adv_key] + if isinstance(key, KeyPair): + loc_report.decrypt(key) + + reports.append(loc_report) return reports diff --git a/findmy/reports/state.py b/findmy/reports/state.py index c818a8d..165f5ae 100644 --- a/findmy/reports/state.py +++ b/findmy/reports/state.py @@ -1,4 +1,5 @@ """Account login state.""" + from enum import Enum from typing_extensions import override diff --git a/findmy/reports/twofactor.py b/findmy/reports/twofactor.py index 307a3be..6a51e5b 100644 --- a/findmy/reports/twofactor.py +++ b/findmy/reports/twofactor.py @@ -1,4 +1,5 @@ """Public classes related to handling two-factor authentication.""" + from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Generic, TypeVar diff --git a/findmy/scanner/__init__.py b/findmy/scanner/__init__.py index 9336c6e..b2c0954 100644 --- a/findmy/scanner/__init__.py +++ b/findmy/scanner/__init__.py @@ -1,4 +1,13 @@ """Utilities related to physically discoverable FindMy-devices.""" -from .scanner import OfflineFindingScanner -__all__ = ("OfflineFindingScanner",) +from .scanner import ( + NearbyOfflineFindingDevice, + OfflineFindingScanner, + SeparatedOfflineFindingDevice, +) + +__all__ = ( + "OfflineFindingScanner", + "NearbyOfflineFindingDevice", + "SeparatedOfflineFindingDevice", +) diff --git a/findmy/scanner/scanner.py b/findmy/scanner/scanner.py index dab1efa..810abbf 100644 --- a/findmy/scanner/scanner.py +++ b/findmy/scanner/scanner.py @@ -1,14 +1,18 @@ """Airtag scanner.""" + from __future__ import annotations import asyncio import logging import time +from abc import ABC, abstractmethod +from datetime import datetime from typing import TYPE_CHECKING, Any, AsyncGenerator from bleak import BleakScanner from typing_extensions import override +from findmy.accessory import RollingKeyPairSource from findmy.keys import HasPublicKey if TYPE_CHECKING: @@ -18,27 +22,30 @@ logging.getLogger(__name__) -class OfflineFindingDevice(HasPublicKey): +class OfflineFindingDevice(ABC): """Device discoverable through Apple's bluetooth-based Offline Finding protocol.""" OF_HEADER_SIZE = 2 OF_TYPE = 0x12 - OF_DATA_LEN = 25 - def __init__( # noqa: PLR0913 + @classmethod + @property + @abstractmethod + def payload_len(cls) -> int: + """Length of OfflineFinding data payload in bytes.""" + raise NotImplementedError + + def __init__( self, mac_bytes: bytes, - status: int, - public_key: bytes, - hint: int, + status_byte: int, + detected_at: datetime, additional_data: dict[Any, Any] | None = None, ) -> None: - """Initialize an `OfflineFindingDevice`.""" + """Instantiate an OfflineFindingDevice.""" self._mac_bytes: bytes = mac_bytes - self._status: int = status - self._public_key: bytes = public_key - self._hint: int = hint - + self._status: int = status_byte + self._detected_at: datetime = detected_at self._additional_data: dict[Any, Any] = additional_data or {} @property @@ -53,60 +60,230 @@ def status(self) -> int: return self._status % 255 @property - def hint(self) -> int: - """Hint value as reported by the device.""" - return self._hint % 255 + def detected_at(self) -> datetime: + """Timezone-aware datetime of when the device was detected.""" + return self._detected_at @property def additional_data(self) -> dict[Any, Any]: """Any additional data. No guarantees about the contents of this dictionary.""" return self._additional_data + @abstractmethod + def is_from(self, other_device: HasPublicKey | RollingKeyPairSource) -> bool: + """Check whether the OF device's identity originates from a specific key source.""" + raise NotImplementedError + + @classmethod + @abstractmethod + def from_payload( + cls, + mac_address: str, + payload: bytes, + detected_at: datetime, + additional_data: dict[Any, Any] | None, + ) -> OfflineFindingDevice | None: + """Get a NearbyOfflineFindingDevice object from an OF message payload.""" + raise NotImplementedError + + @classmethod + def from_ble_payload( + cls, + mac_address: str, + ble_payload: bytes, + detected_at: datetime | None = None, + additional_data: dict[Any, Any] | None = None, + ) -> OfflineFindingDevice | None: + """Get a NearbyOfflineFindingDevice object from a BLE packet payload.""" + if len(ble_payload) < cls.OF_HEADER_SIZE: + logging.error("Not enough bytes to decode: %s", len(ble_payload)) + return None + if ble_payload[0] != cls.OF_TYPE: + logging.debug("Unsupported OF type: %s", ble_payload[0]) + return None + + device_type = next( + (dev for dev in cls.__subclasses__() if dev.payload_len == ble_payload[1]), + None, + ) + if device_type is None: + logging.error("Invalid OF payload length: %s", ble_payload[1]) + return None + + return device_type.from_payload( + mac_address, + ble_payload[cls.OF_HEADER_SIZE :], + detected_at or datetime.now().astimezone(), + additional_data, + ) + + @override + def __eq__(self, other: object) -> bool: + if isinstance(other, OfflineFindingDevice): + return self.mac_address == other.mac_address + + return NotImplemented + + @override + def __hash__(self) -> int: + return int.from_bytes(self._mac_bytes, "big") + + +class NearbyOfflineFindingDevice(OfflineFindingDevice): + """Offline-Finding device in nearby state.""" + + @classmethod + @property + @override + def payload_len(cls) -> int: + """Length of OfflineFinding data payload in bytes.""" + return 0x02 # 2 + + def __init__( + self, + mac_bytes: bytes, + status_byte: int, + first_adv_key_bytes: bytes, + detected_at: datetime, + additional_data: dict[Any, Any] | None = None, + ) -> None: + """Instantiate a NearbyOfflineFindingDevice.""" + super().__init__(mac_bytes, status_byte, detected_at, additional_data) + + self._first_adv_key_bytes: bytes = first_adv_key_bytes + + @override + def is_from(self, other_device: HasPublicKey | RollingKeyPairSource) -> bool: + """Check whether the OF device's identity originates from a specific key source.""" + if isinstance(other_device, HasPublicKey): + return other_device.adv_key_bytes.startswith(self._first_adv_key_bytes) + if isinstance(other_device, RollingKeyPairSource): + return any(self.is_from(key) for key in other_device.keys_at(self.detected_at)) + + msg = f"Cannot compare against {type(other_device)}" + raise ValueError(msg) + + @classmethod + @override + def from_payload( + cls, + mac_address: str, + payload: bytes, + detected_at: datetime, + additional_data: dict[Any, Any] | None = None, + ) -> NearbyOfflineFindingDevice | None: + """Get a NearbyOfflineFindingDevice object from an OF message payload.""" + if len(payload) != cls.payload_len: + logging.error( + "Invalid OF data length: %s instead of %s", + len(payload), + payload[1], + ) + + mac_bytes = bytes.fromhex(mac_address.replace(":", "").replace("-", "")) + status_byte = payload[0] + + pubkey_middle = mac_bytes[1:] + pubkey_start_ms = payload[1] << 6 + pubkey_start_ls = mac_bytes[0] & 0b00111111 + pubkey_start = (pubkey_start_ms | pubkey_start_ls).to_bytes(1, "big") + partial_pubkey = pubkey_start + pubkey_middle + + return NearbyOfflineFindingDevice( + mac_bytes, + status_byte, + partial_pubkey, + detected_at, + additional_data, + ) + + +class SeparatedOfflineFindingDevice(OfflineFindingDevice, HasPublicKey): + """Offline-Finding device in separated state.""" + + @classmethod + @property + @override + def payload_len(cls) -> int: + """Length of OfflineFinding data in bytes.""" + return 0x19 # 25 + + def __init__( # noqa: PLR0913 + self, + mac_bytes: bytes, + status: int, + public_key: bytes, + hint: int, + detected_at: datetime, + additional_data: dict[Any, Any] | None = None, + ) -> None: + """Initialize a `SeparatedOfflineFindingDevice`.""" + super().__init__(mac_bytes, status, detected_at, additional_data) + + self._public_key: bytes = public_key + self._hint: int = hint + + @property + def hint(self) -> int: + """Hint value as reported by the device.""" + return self._hint % 255 + @property @override def adv_key_bytes(self) -> bytes: """See `HasPublicKey.adv_key_bytes`.""" return self._public_key + @override + def is_from(self, other_device: HasPublicKey | RollingKeyPairSource) -> bool: + """Check whether the OF device's identity originates from a specific key source.""" + if isinstance(other_device, HasPublicKey): + return self.adv_key_bytes == other_device.adv_key_bytes + if isinstance(other_device, RollingKeyPairSource): + return any(self.is_from(key) for key in other_device.keys_at(self.detected_at)) + + msg = f"Cannot compare against {type(other_device)}" + raise ValueError(msg) + @classmethod + @override def from_payload( cls, mac_address: str, payload: bytes, - additional_data: dict[Any, Any], - ) -> OfflineFindingDevice | None: - """Get an OfflineFindingDevice object from a BLE payload.""" - if len(payload) < cls.OF_HEADER_SIZE: - logging.error("Not enough bytes to decode: %s", len(payload)) - return None - if payload[0] != cls.OF_TYPE: - logging.debug("Unsupported OF type: %s", payload[0]) - return None - if payload[1] != cls.OF_DATA_LEN: - logging.debug("Unknown OF data length: %s", payload[1]) - return None - if len(payload) != cls.OF_HEADER_SIZE + cls.OF_DATA_LEN: - logging.debug( + detected_at: datetime, + additional_data: dict[Any, Any] | None = None, + ) -> SeparatedOfflineFindingDevice | None: + """Get a SeparatedOfflineFindingDevice object from an OF message payload.""" + if len(payload) != cls.payload_len: + logging.error( "Invalid OF data length: %s instead of %s", - len(payload) - cls.OF_HEADER_SIZE, + len(payload), payload[1], ) return None mac_bytes = bytes.fromhex(mac_address.replace(":", "").replace("-", "")) - status = payload[cls.OF_HEADER_SIZE + 0] + status = payload[0] - pubkey_end = payload[cls.OF_HEADER_SIZE + 1 : cls.OF_HEADER_SIZE + 23] + pubkey_end = payload[1:23] pubkey_middle = mac_bytes[1:] - pubkey_start_ms = payload[cls.OF_HEADER_SIZE + 23] << 6 + pubkey_start_ms = payload[23] << 6 pubkey_start_ls = mac_bytes[0] & 0b00111111 pubkey_start = (pubkey_start_ms | pubkey_start_ls).to_bytes(1, "big") pubkey = pubkey_start + pubkey_middle + pubkey_end - hint = payload[cls.OF_HEADER_SIZE + 24] + hint = payload[24] - return OfflineFindingDevice(mac_bytes, status, pubkey, hint, additional_data) + return SeparatedOfflineFindingDevice( + mac_bytes, + status, + pubkey, + hint, + detected_at, + additional_data, + ) @override def __repr__(self) -> str: @@ -173,13 +350,20 @@ async def _wait_for_device(self, timeout: float) -> OfflineFindingDevice | None: if not apple_data: return None + detected_at = datetime.now().astimezone() + try: additional_data = device.details.get("props", {}) except AttributeError: # Likely Windows host, where details is a '_RawAdvData' object. # See: https://github.com/malmeloo/FindMy.py/issues/24 additional_data = {} - return OfflineFindingDevice.from_payload(device.address, apple_data, additional_data) + return OfflineFindingDevice.from_ble_payload( + device.address, + apple_data, + detected_at, + additional_data, + ) async def scan_for( self, diff --git a/findmy/util/__init__.py b/findmy/util/__init__.py index 6ed65f1..3f4b47c 100644 --- a/findmy/util/__init__.py +++ b/findmy/util/__init__.py @@ -1,4 +1,5 @@ """Utility functions and classes. Intended for internal use.""" + from .http import HttpResponse, HttpSession from .parsers import decode_plist diff --git a/findmy/util/closable.py b/findmy/util/closable.py index 406a037..d210bdc 100644 --- a/findmy/util/closable.py +++ b/findmy/util/closable.py @@ -1,4 +1,5 @@ """ABC for async classes that need to be cleaned up before exiting.""" + from __future__ import annotations import asyncio @@ -29,6 +30,9 @@ def __del__(self) -> None: """Attempt to automatically clean up when garbage collected.""" try: loop = self._loop or asyncio.get_running_loop() - loop.call_soon_threadsafe(loop.create_task, self.close()) + if loop.is_running(): + loop.call_soon_threadsafe(loop.create_task, self.close()) + else: + loop.run_until_complete(self.close()) except RuntimeError: pass diff --git a/findmy/util/crypto.py b/findmy/util/crypto.py index 810e92b..329bf17 100644 --- a/findmy/util/crypto.py +++ b/findmy/util/crypto.py @@ -11,9 +11,12 @@ P224_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D -def encrypt_password(password: str, salt: bytes, iterations: int) -> bytes: +def encrypt_password(password: str, salt: bytes, iterations: int, protocol: str) -> bytes: """Encrypt password using PBKDF2-HMAC.""" + assert protocol in ["s2k", "s2k_fo"] p = hashlib.sha256(password.encode("utf-8")).digest() + if protocol == "s2k_fo": + p = p.hex().encode("utf-8") kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, diff --git a/findmy/util/http.py b/findmy/util/http.py index 0c941e7..027694f 100644 --- a/findmy/util/http.py +++ b/findmy/util/http.py @@ -1,9 +1,10 @@ """Module to simplify asynchronous HTTP calls.""" + from __future__ import annotations import json import logging -from typing import Any, TypedDict +from typing import Any, TypedDict, cast from aiohttp import BasicAuth, ClientSession, ClientTimeout from typing_extensions import Unpack, override @@ -14,13 +15,20 @@ logging.getLogger(__name__) -class _HttpRequestOptions(TypedDict, total=False): +class _RequestOptions(TypedDict, total=False): json: dict[str, Any] | None headers: dict[str, str] - auth: tuple[str, str] | BasicAuth data: bytes +class _AiohttpRequestOptions(_RequestOptions): + auth: BasicAuth + + +class _HttpRequestOptions(_RequestOptions, total=False): + auth: BasicAuth | tuple[str, str] + + class HttpResponse: """Response of a request made by `HttpSession`.""" @@ -94,15 +102,19 @@ async def request( """ session = await self._get_session() + # cast from http options to library supported options auth = kwargs.get("auth") if isinstance(auth, tuple): kwargs["auth"] = BasicAuth(auth[0], auth[1]) + else: + kwargs.pop("auth") + options = cast(_AiohttpRequestOptions, kwargs) async with await session.request( method, url, ssl=False, - **kwargs, + **options, ) as r: return HttpResponse(r.status, await r.content.read()) diff --git a/findmy/util/parsers.py b/findmy/util/parsers.py index 74b5dae..2f06769 100644 --- a/findmy/util/parsers.py +++ b/findmy/util/parsers.py @@ -1,4 +1,5 @@ """Parsers for various forms of data formats.""" + import plistlib from typing import Any diff --git a/findmy/util/types.py b/findmy/util/types.py index 8c754aa..feb3a9c 100644 --- a/findmy/util/types.py +++ b/findmy/util/types.py @@ -1,7 +1,9 @@ """Utility types.""" -from typing import Coroutine, TypeVar +from typing import Coroutine, TypeVar, Union T = TypeVar("T") -MaybeCoro = T | Coroutine[None, None, T] +# Cannot use `|` operator (PEP 604) in python 3.9, +# even with __future__ import since it is evaluated directly +MaybeCoro = Union[T, Coroutine[None, None, T]] diff --git a/poetry.lock b/poetry.lock index a254b8a..2372b2d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiohttp" @@ -121,17 +121,6 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] -[[package]] -name = "anyascii" -version = "0.3.2" -description = "Unicode to ASCII transliteration" -optional = false -python-versions = ">=3.3" -files = [ - {file = "anyascii-0.3.2-py3-none-any.whl", hash = "sha256:3b3beef6fc43d9036d3b0529050b0c48bfad8bc960e9e562d7223cfb94fe45d4"}, - {file = "anyascii-0.3.2.tar.gz", hash = "sha256:9d5d32ef844fe225b8bc7cba7f950534fae4da27a9bf3a6bea2cb0ea46ce4730"}, -] - [[package]] name = "astroid" version = "3.1.0" @@ -213,30 +202,31 @@ lxml = ["lxml"] [[package]] name = "bleak" -version = "0.21.1" +version = "0.22.2" description = "Bluetooth Low Energy platform Agnostic Klient" optional = false -python-versions = ">=3.8,<3.13" +python-versions = "<3.13,>=3.8" files = [ - {file = "bleak-0.21.1-py3-none-any.whl", hash = "sha256:ccec260a0f5ec02dd133d68b0351c0151b2ecf3ddd0bcabc4c04a1cdd7f33256"}, - {file = "bleak-0.21.1.tar.gz", hash = "sha256:ec4a1a2772fb315b992cbaa1153070c7e26968a52b0e2727035f443a1af5c18f"}, + {file = "bleak-0.22.2-py3-none-any.whl", hash = "sha256:8395c9e096f28e0ba1f3e6a8619fa21c327c484f720b7af3ea578d04f498a458"}, + {file = "bleak-0.22.2.tar.gz", hash = "sha256:09010c0f4bd843e7dcaa1652e1bfb2450ce690da08d4c6163f0723aaa986e9fe"}, ] [package.dependencies] async-timeout = {version = ">=3.0.0,<5", markers = "python_version < \"3.11\""} bleak-winrt = {version = ">=1.2.0,<2.0.0", markers = "platform_system == \"Windows\" and python_version < \"3.12\""} dbus-fast = {version = ">=1.83.0,<3", markers = "platform_system == \"Linux\""} -pyobjc-core = {version = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} -pyobjc-framework-CoreBluetooth = {version = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} -pyobjc-framework-libdispatch = {version = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} +pyobjc-core = {version = ">=10.0,<11.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-CoreBluetooth = {version = ">=10.0,<11.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-libdispatch = {version = ">=10.0,<11.0", markers = "platform_system == \"Darwin\""} typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.12\""} -"winrt-Windows.Devices.Bluetooth" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} -"winrt-Windows.Devices.Bluetooth.Advertisement" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} -"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} -"winrt-Windows.Devices.Enumeration" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} -"winrt-Windows.Foundation" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} -"winrt-Windows.Foundation.Collections" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} -"winrt-Windows.Storage.Streams" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +winrt-runtime = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Bluetooth" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Bluetooth.Advertisement" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Enumeration" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Foundation" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Foundation.Collections" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Storage.Streams" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} [[package]] name = "bleak-winrt" @@ -573,6 +563,20 @@ files = [ {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "filelock" version = "3.13.4" @@ -747,6 +751,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "jinja2" version = "3.1.3" @@ -1028,13 +1043,13 @@ setuptools = "*" [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -1053,15 +1068,30 @@ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx- test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] type = ["mypy (>=1.8)"] +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pre-commit" -version = "3.7.0" +version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, - {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, ] [package.dependencies] @@ -1099,87 +1129,89 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyobjc-core" -version = "9.2" +version = "10.3.1" description = "Python<->ObjC Interoperability Module" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {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"}, + {file = "pyobjc_core-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ea46d2cda17921e417085ac6286d43ae448113158afcf39e0abe484c58fb3d78"}, + {file = "pyobjc_core-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:899d3c84d2933d292c808f385dc881a140cf08632907845043a333a9d7c899f9"}, + {file = "pyobjc_core-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6ff5823d13d0a534cdc17fa4ad47cf5bee4846ce0fd27fc40012e12b46db571b"}, + {file = "pyobjc_core-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2581e8e68885bcb0e11ec619e81ef28e08ee3fac4de20d8cc83bc5af5bcf4a90"}, + {file = "pyobjc_core-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea98d4c2ec39ca29e62e0327db21418696161fb138ee6278daf2acbedf7ce504"}, + {file = "pyobjc_core-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:4c179c26ee2123d0aabffb9dbc60324b62b6f8614fb2c2328b09386ef59ef6d8"}, + {file = "pyobjc_core-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cb901fce65c9be420c40d8a6ee6fff5ff27c6945f44fd7191989b982baa66dea"}, + {file = "pyobjc_core-10.3.1.tar.gz", hash = "sha256:b204a80ccc070f9ab3f8af423a3a25a6fd787e228508d00c4c30f8ac538ba720"}, ] [[package]] name = "pyobjc-framework-cocoa" -version = "9.2" +version = "10.3.1" description = "Wrappers for the Cocoa frameworks on macOS" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {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"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4cb4f8491ab4d9b59f5187e42383f819f7a46306a4fa25b84f126776305291d1"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5f31021f4f8fdf873b57a97ee1f3c1620dbe285e0b4eaed73dd0005eb72fd773"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11b4e0bad4bbb44a4edda128612f03cdeab38644bbf174de0c13129715497296"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:de5e62e5ccf2871a94acf3bf79646b20ea893cc9db78afa8d1fe1b0d0f7cbdb0"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c5af24610ab639bd1f521ce4500484b40787f898f691b7a23da3339e6bc8b90"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a7151186bb7805deea434fae9a4423335e6371d105f29e73cc2036c6779a9dbc"}, + {file = "pyobjc_framework_Cocoa-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:743d2a1ac08027fd09eab65814c79002a1d0421d7c0074ffd1217b6560889744"}, + {file = "pyobjc_framework_cocoa-10.3.1.tar.gz", hash = "sha256:1cf20714daaa986b488fb62d69713049f635c9d41a60c8da97d835710445281a"}, ] [package.dependencies] -pyobjc-core = ">=9.2" +pyobjc-core = ">=10.3.1" [[package]] name = "pyobjc-framework-corebluetooth" -version = "9.2" +version = "10.3.1" description = "Wrappers for the framework CoreBluetooth on macOS" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {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"}, + {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_13_universal2.whl", hash = "sha256:c89ee6fba0ed359c46b4908a7d01f88f133be025bd534cbbf4fb9c183e62fc97"}, + {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2f261a386aa6906f9d4601d35ff71a13315dbca1a0698bf1f1ecfe3971de4648"}, + {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5211df0da2e8be511d9a54a48505dd7af0c4d04546fe2027dd723801d633c6ba"}, + {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:b8becd4e406be289a2d423611d3ad40730532a1f6728effb2200e68c9c04c3e8"}, + {file = "pyobjc_framework_corebluetooth-10.3.1.tar.gz", hash = "sha256:dc5d326ab5541b8b68e7e920aa8363851e779cb8c33842f6cfeef4674cc62f94"}, ] [package.dependencies] -pyobjc-core = ">=9.2" -pyobjc-framework-Cocoa = ">=9.2" +pyobjc-core = ">=10.3.1" +pyobjc-framework-Cocoa = ">=10.3.1" [[package]] name = "pyobjc-framework-libdispatch" -version = "9.2" +version = "10.3.1" description = "Wrappers for libdispatch on macOS" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {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"}, + {file = "pyobjc_framework_libdispatch-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5543aea8acd53fb02bcf962b003a2a9c2bdacf28dc290c31a3d2de7543ef8392"}, + {file = "pyobjc_framework_libdispatch-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3e0db3138aae333f0b87b42586bc016430a76638af169aab9cef6afee4e5f887"}, + {file = "pyobjc_framework_libdispatch-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b209dbc9338cd87e053ede4d782b8c445bcc0b9a3d0365a6ffa1f9cd5143c301"}, + {file = "pyobjc_framework_libdispatch-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a74e62314376dc2d34bc5d4a86cedaf5795786178ebccd0553c58e8fa73400a3"}, + {file = "pyobjc_framework_libdispatch-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8e8fb27ac86d48605eb2107ac408ed8de281751df81f5430fe66c8228d7626b8"}, + {file = "pyobjc_framework_libdispatch-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0a7a19afef70c98b3b527fb2c9adb025444bcb50f65c8d7b949f1efb51bde577"}, + {file = "pyobjc_framework_libdispatch-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:109044cddecb3332cbb75f14819cd01b98aacfefe91204c776b491eccc58a112"}, + {file = "pyobjc_framework_libdispatch-10.3.1.tar.gz", hash = "sha256:f5c3475498cb32f54d75e21952670e4a32c8517fb2db2e90869f634edc942446"}, ] [package.dependencies] -pyobjc-core = ">=9.2" +pyobjc-core = ">=10.3.1" +pyobjc-framework-Cocoa = ">=10.3.1" [[package]] name = "pyright" -version = "1.1.359" +version = "1.1.378" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.359-py3-none-any.whl", hash = "sha256:5582777be7eab73512277ac7da7b41e15bc0737f488629cb9babd96e0769be61"}, - {file = "pyright-1.1.359.tar.gz", hash = "sha256:f0eab50f3dafce8a7302caeafd6a733f39901a2bf5170bb23d77fd607c8a8dbc"}, + {file = "pyright-1.1.378-py3-none-any.whl", hash = "sha256:8853776138b01bc284da07ac481235be7cc89d3176b073d2dba73636cb95be79"}, + {file = "pyright-1.1.378.tar.gz", hash = "sha256:78a043be2876d12d0af101d667e92c7734f3ebb9db71dccc2c220e7e7eb89ca2"}, ] [package.dependencies] @@ -1189,6 +1221,28 @@ nodeenv = ">=1.6.0" all = ["twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"] +[[package]] +name = "pytest" +version = "8.3.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + [[package]] name = "pyyaml" version = "6.0.1" @@ -1270,6 +1324,33 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "ruff" +version = "0.6.3" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"}, + {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"}, + {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"}, + {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"}, + {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"}, + {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"}, + {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"}, +] + [[package]] name = "setuptools" version = "69.5.1" @@ -1357,17 +1438,16 @@ test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools [[package]] name = "sphinx-autoapi" -version = "3.0.0" +version = "3.3.1" description = "Sphinx API documentation generator" optional = false python-versions = ">=3.8" files = [ - {file = "sphinx-autoapi-3.0.0.tar.gz", hash = "sha256:09ebd674a32b44467222b0fb8a917b97c89523f20dbf05b52cb8a3f0e15714de"}, - {file = "sphinx_autoapi-3.0.0-py2.py3-none-any.whl", hash = "sha256:ea207793cba1feff7b2ded0e29364f2995a4d157303a98603cee0ce94cea2688"}, + {file = "sphinx_autoapi-3.3.1-py2.py3-none-any.whl", hash = "sha256:c31a5f41eabc9705d277b75f98e983d653e9af24e294dd576b2afa1719f72c1f"}, + {file = "sphinx_autoapi-3.3.1.tar.gz", hash = "sha256:e44a225827d0ef7178748225a66f30c95454dfd00ee3c22afbdfb8056f7dffb5"}, ] [package.dependencies] -anyascii = "*" astroid = [ {version = ">=2.7", markers = "python_version < \"3.12\""}, {version = ">=3.0.0a1", markers = "python_version >= \"3.12\""}, @@ -1375,6 +1455,7 @@ astroid = [ Jinja2 = "*" PyYAML = "*" sphinx = ">=6.1.0" +stdlib-list = {version = "*", markers = "python_version < \"3.10\""} [package.extras] docs = ["furo", "sphinx", "sphinx-design"] @@ -1492,18 +1573,36 @@ test = ["pytest"] [[package]] name = "srp" -version = "1.0.20" +version = "1.0.21" description = "Secure Remote Password" optional = false python-versions = "*" files = [ - {file = "srp-1.0.20-py3-none-any.whl", hash = "sha256:ad55b94e26e1152db83b57b50d7b365a7a9b6c39d0d1cd762f0642e478b4bdc0"}, - {file = "srp-1.0.20.tar.gz", hash = "sha256:2db453bdce26b9eead367a7b5783074ef80e8482bf30c0140a7b89836a054707"}, + {file = "srp-1.0.21-py3-none-any.whl", hash = "sha256:e49ad6e2b8b1189c5879874664d33e4e1e403598c3e0903541a1bde03f7becae"}, + {file = "srp-1.0.21.tar.gz", hash = "sha256:866813bcf521189a1563e6ca3112b6f54fdf725a410a2dbebb6f0d84b82a1f1d"}, ] [package.dependencies] six = "*" +[[package]] +name = "stdlib-list" +version = "0.10.0" +description = "A list of Python Standard Libraries (2.7 through 3.12)." +optional = false +python-versions = ">=3.7" +files = [ + {file = "stdlib_list-0.10.0-py3-none-any.whl", hash = "sha256:b3a911bc441d03e0332dd1a9e7d0870ba3bb0a542a74d7524f54fb431256e214"}, + {file = "stdlib_list-0.10.0.tar.gz", hash = "sha256:6519c50d645513ed287657bfe856d527f277331540691ddeaf77b25459964a14"}, +] + +[package.extras] +dev = ["build", "stdlib-list[doc,lint,test]"] +doc = ["furo", "sphinx"] +lint = ["black", "mypy", "ruff"] +support = ["sphobjinv"] +test = ["coverage[toml]", "pytest", "pytest-cov"] + [[package]] name = "tomli" version = "2.0.1" @@ -1517,13 +1616,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -1565,221 +1664,245 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "winrt-runtime" -version = "2.0.0b1" +version = "2.2.0" description = "Python projection of Windows Runtime (WinRT) APIs" optional = false -python-versions = "<3.13,>=3.9" +python-versions = "<3.14,>=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"}, + {file = "winrt_runtime-2.2.0-cp310-cp310-win32.whl", hash = "sha256:ab034330d6b64ce93683bdc14d4f3f83dfafbf1f72b45893505f7d684e5e7fe1"}, + {file = "winrt_runtime-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad9927a1838dea47ceb2d773c0269242bcee7cb5379ed801547788ab435da502"}, + {file = "winrt_runtime-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:87745ae54d054957a99c70875c1ac3c89cca258ed06836ae308fbbb7dda4ef61"}, + {file = "winrt_runtime-2.2.0-cp311-cp311-win32.whl", hash = "sha256:7ee2397934c1c4a090f9d889292def90b8f673dc1d320f1f07931ad1cb6e49bf"}, + {file = "winrt_runtime-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f110b0f451b514cf09c4fa0e73bab54d4b598c3092df9dd87940403998e81f30"}, + {file = "winrt_runtime-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:27606e7a393a26e484f03db699c4d7c206d180a3736a6cd68fba3b3896e364a4"}, + {file = "winrt_runtime-2.2.0-cp312-cp312-win32.whl", hash = "sha256:5a769bfb4e264b7fd306027da90c6e4e615667e9afdd8e5d712bc45bdabaf0d2"}, + {file = "winrt_runtime-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ef30ea7446a1e37660265b76e586fcffc0e83a859b7729141cdf68cbedf808a8"}, + {file = "winrt_runtime-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:d8f6338fb8433b4df900c8f173959a5ae9ac63b0b20faddb338e76a6e9391bc9"}, + {file = "winrt_runtime-2.2.0-cp313-cp313-win32.whl", hash = "sha256:6d8c1122158edc96cac956a5ab62bc06a56e088bdf83d0993a455216b3fd1cac"}, + {file = "winrt_runtime-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:76b2dc846e6802375113c9ce9e7fcc4292926bd788445f34d404bae72d2b4f4b"}, + {file = "winrt_runtime-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:faacc05577573702cb135e7da4d619f4990c768063dc869362f13d856a0738e3"}, + {file = "winrt_runtime-2.2.0-cp39-cp39-win32.whl", hash = "sha256:f00334e3304a43e1742514bed2dc736a9242e831676f605fdfb5d62932714b18"}, + {file = "winrt_runtime-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ef1b2dc31576d686cce088a349b539fc0f47bdf2f66fb8ea63a6964dc069d00d"}, + {file = "winrt_runtime-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:1c9e8a609cf00acc426eae2ed4ad866991a0f33f196ec9dc69af95ae43b4373b"}, + {file = "winrt_runtime-2.2.0.tar.gz", hash = "sha256:37a673b295ebd5f6dc5a3b42fd52c8e4589ca3e605deb54c26d0877d2575ec85"}, ] [[package]] name = "winrt-windows-devices-bluetooth" -version = "2.0.0b1" +version = "2.2.0" 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"}, +python-versions = "<3.14,>=3.9" +files = [ + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win32.whl", hash = "sha256:f3ced50ded44f74ac901d05f99cdd0bdf78e3a939a42d3cd80c33e510b4b8569"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:241a8f0ab06f6178d2e5757e7bc1f6c37e00e65ab6858ae676a1723a6445fa92"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3abefa3d11b4af9d9731d9d1a71083b1ef301fa30f7006a6c1f341426dd6d733"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4215c45595201f5f43f98b1e8911ff5cb0b303fe3298fa4d91a7bdc6d5523853"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cda69842b30bf56b10ea1a747d01b295abc910d9ccc10e9c97e8f554cd536e0"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7c12a28cd04eb05bacc73d8025ba135a929b9d511d21f20d0072d735853e8a2"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win32.whl", hash = "sha256:c929ea5215942fb26081b26aae094a2f70551cc0a59499ab2c9ea1f6d6b991f9"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1444e2031f3e69990d412b9edf75413a09280744bbc088a6b0760d94d356d4b"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f2d06ce6c43e37ea09ac073805ac6f9f62ae10ce552c90ae6eca978accd3f434"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win32.whl", hash = "sha256:b44a45c60f1d9fa288a12119991060ef7998793c6b93baa84308cfb090492788"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:fb698a55d06dc34643437b370c35fa064bd28762561e880715a30463c359fa44"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:cb350bfe21bab3573c9cd84006efad9c46a395a2943ab474105aed8b21bb88a4"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp39-cp39-win32.whl", hash = "sha256:7ee056e4c1a542352bcacbb95f898b7ae2739b3e0a63f7ab1290a7e2569f6393"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:f919cee2a49c3c48d1ef9dd84b419a6438000ef43bc35a7a349291c162cab4f3"}, + {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:f223af93675f6f92ab87de08c6d413ecc8ab19014b7438893437c42dcb2b0969"}, + {file = "winrt_windows_devices_bluetooth-2.2.0.tar.gz", hash = "sha256:95a5cf9c1e915557a28a4f017ea1ff7357039ee23526258f9cc161cf080b4577"}, ] [package.dependencies] -winrt-runtime = "2.0.0-beta.1" +winrt-runtime = "2.2.0" [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)"] +all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (==2.2.0)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (==2.2.0)", "winrt-Windows.Devices.Enumeration[all] (==2.2.0)", "winrt-Windows.Devices.Radios[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Networking[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)"] [[package]] name = "winrt-windows-devices-bluetooth-advertisement" -version = "2.0.0b1" +version = "2.2.0" 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"}, +python-versions = "<3.14,>=3.9" +files = [ + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win32.whl", hash = "sha256:3d5fddffd5f6eeafebe1bcbaa096b8962c28c9236490f6f887ac2ed3ee4ed62c"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1cb5a835dc3574b0c47a613fa49eeeccdd9aa5801d43d7b7606ad5ce3614a54"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:9c2530c4972671ffb8a6e54621490c6c7a8c13b4d57e6474e05b62f211bbaab6"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win32.whl", hash = "sha256:28b36b3be137bdb6bdaad0d7a620c1a8b156e3c2737d08b9827af02b3c9d52bf"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:52948f17ecfc70c58b07077191985712172b518b5e3f4874e5708d175b7ace72"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:338296b76c01840c1dc10799a405b76460346bf677af11e6ab324311fd58e1a9"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win32.whl", hash = "sha256:4c14f48ac1886a3d374ee511467f0a61f26d88a321bf97d47429859730ee9248"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:89a658e901de88373e6a17a98273b8555e3f80563f2cc362b7f75817a7f9d915"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3b2b1b34f37a3329cf72793a089dd13fefd7b582c3e3a53a69a1353fd18940a3"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win32.whl", hash = "sha256:1b2d42c3d90b3e985954196b9a9e4007e22ff468d3d020c5a4acdee2821018fe"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d964c599670ea21b97afe2435e7638ca26e04936aacc0550474b6ec3fea988f"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:add4f459f0a02d1da38d579c3af887cfc3fe54f7782d779cf4ffe7f24404f1ff"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp39-cp39-win32.whl", hash = "sha256:756aeb2408bd59983a34da7f2552690d9e1071ad75de96aff15b365e1137b157"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9d19ef4cb00f58e10bdd0a2eb497eabecb3a2a5586fdcacebae6f0009585f3f1"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:1008641262bbbe130b6fcda76b9c890327aa416ef5b240a6a2cbb895d37dd3c7"}, + {file = "winrt_windows_devices_bluetooth_advertisement-2.2.0.tar.gz", hash = "sha256:bcbf246994b60e5de4bea9eb3fa01c5d6452200789004d14df70b27be9aa4775"}, ] [package.dependencies] -winrt-runtime = "2.0.0-beta.1" +winrt-runtime = "2.2.0" [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)"] +all = ["winrt-Windows.Devices.Bluetooth[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)"] [[package]] name = "winrt-windows-devices-bluetooth-genericattributeprofile" -version = "2.0.0b1" +version = "2.2.0" 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"}, +python-versions = "<3.14,>=3.9" +files = [ + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win32.whl", hash = "sha256:1472f89b9d6527137e1c58dfb46f22faf2753c477a9d4f85f789b3266ad282a9"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:e25702f1aa6d4ecdf335805a50048e70ee2206499cfd7ed4fbe1a92358bdcc16"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:d07d27a6f8f7a1f52aa978724d5a09d43053b428c71563892b70df409049a37a"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win32.whl", hash = "sha256:5c6c863daaa99b0bb670730296137b7c718d94726c112ff44ec73c8b27a12ded"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbee7c90c0a155477eba09eb09297711b2cb32f6ede4c01d0afe58cb3776f06a"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:655777193fd338e1a8c30ebbb8460c017d08548c54ddec9fc5503f1605c47332"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win32.whl", hash = "sha256:45a48ab8da94eee1590f22826c084f4b1f8c32107a023f05d6a03437931a6852"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:395cb2fecd0835a402c3c4f274395bc689549b2a6b4155d3ad97b29ec87ee4f2"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:25063b43550c5630f188cfb263ab09acc920db97d1625c48e24baa6e7d445b6e"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win32.whl", hash = "sha256:d1d26512fe45c3be0dbeb932dbd75abd580cd46ccfc278fcf51042eff302fa9c"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:21786840502a34958dd5fb137381f9144a6437b49ee90a877beb3148ead6cfe9"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d98852458b639e875bb4895a9ad2d5626059bc99c5f745be0560d235502d648"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp39-cp39-win32.whl", hash = "sha256:827b390b1a47c9aa6bfd717b66822f4fc698b0c02c8678924e2bc6ac37093b65"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:727567b725ca94b677bda97a6f725d58fc1a4652d4cc232b44cc57dd7ba9ee87"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:ac901d17d2350785bce18282cd29d002d2c4da8adff5160891c4115ae010a2d0"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-2.2.0.tar.gz", hash = "sha256:0de4ee5f57223107f25c20f6bb2739947670a2f8cf09907f3e611efc81e7c6e0"}, ] [package.dependencies] -winrt-runtime = "2.0.0-beta.1" +winrt-runtime = "2.2.0" [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)"] +all = ["winrt-Windows.Devices.Bluetooth[all] (==2.2.0)", "winrt-Windows.Devices.Enumeration[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)"] [[package]] name = "winrt-windows-devices-enumeration" -version = "2.0.0b1" +version = "2.2.0" 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"}, +python-versions = "<3.14,>=3.9" +files = [ + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win32.whl", hash = "sha256:69e87ba0ae5c31f60bc07d0558d91af96213d8b8b2b1be0ccf3e5824cab466ef"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6993d5305ff750c5c51f57253935458996fb45c049891f2fb00772cc6ece6b3"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bb54aa94b17052d65fe4fa5777183cf9bfb697574c3461759114d3ec0c802cec"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win32.whl", hash = "sha256:fef83263e73c2611d223f06735d2c2a16629d723f74e1964dc882f90b6e1cda1"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:cf3cec5a6fba069ecbd4f3efa95e9f197aeebdd05a60bcd52b953888169ab7ee"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:d9ce308c492c1e9f2417f91ad02e366f4269cc1c6d271f0be4092b758df4c9bf"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win32.whl", hash = "sha256:5bea21988749fad21574ea789b4090cfbfbb982a5f9a42b2d6f05b3ad47f68bd"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c9718d7033550a029e0c2848ff620bf063a519cb22ab9d880d64ceb302763a48"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:69f67f01aa519304e4af04a1a23261bd8b57136395de2e08d56968f9c6daa18e"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win32.whl", hash = "sha256:84447916282773d7b7e5a445eae0ab273c21105f1bbcdfb7d8e21cd41403d5c1"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:1bb9d97f8d2518bb5b331f825431814277de4341811a1776e79d51767e79700c"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:2a5408423f680f6b36d7accad7151336ea16ad1eaa2652f60ed88e2cbd14562c"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp39-cp39-win32.whl", hash = "sha256:51f4c9b6f3376913e3009bfe232cfc082357b24d6eeec098cf53f361527e1c1f"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e6895d5538539d0c6bd081374e7646684901038d4d2dede7841b63adfaf8086"}, + {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0845fca0841003ae446650ab6695c38d45623bc1e8e40a43e839e450a874fd6f"}, + {file = "winrt_windows_devices_enumeration-2.2.0.tar.gz", hash = "sha256:cfe1780101e3ef9c5b4716cca608aa6b6ddf19f1d7a2a70434241d438db19d3d"}, ] [package.dependencies] -winrt-runtime = "2.0.0-beta.1" +winrt-runtime = "2.2.0" [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)"] +all = ["winrt-Windows.ApplicationModel.Background[all] (==2.2.0)", "winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Security.Credentials[all] (==2.2.0)", "winrt-Windows.Storage.Streams[all] (==2.2.0)", "winrt-Windows.UI.Popups[all] (==2.2.0)", "winrt-Windows.UI[all] (==2.2.0)"] [[package]] name = "winrt-windows-foundation" -version = "2.0.0b1" +version = "2.2.0" 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"}, +python-versions = "<3.14,>=3.9" +files = [ + {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win32.whl", hash = "sha256:cb86bbf04f72d983e4ae13db0a48784638b36214bb2c44809f39686ef3314354"}, + {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2dbd0957216c07db4b91a144a0ffa7c8892cc668b19ca15b78067255445741b2"}, + {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:5345f7d0504aa1a605be5b5fe0d1944b322591f7669c2c86b7c45384924c8c9b"}, + {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win32.whl", hash = "sha256:f6711adf8a34e48c94183e792f153de5f3796f8f3c045356544605384bbcb7e1"}, + {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:0a5bfe2647659e7ec288d8552e61e577a931914531ccc9cb958469d85f049d6b"}, + {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9eabbd1b179fd04f167884fa0feaa17ccd67d89f6eac4099b16c6c0dc22e9f32"}, + {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win32.whl", hash = "sha256:0f0319659f00d04d13fc5db45f574479a396147c955628dc2dda056397a0df28"}, + {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:8bc605242d268cd8ccce68c78ec4a967b8e5431c3a969c9e7a01d454696dfb3f"}, + {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f901b20c3a874a2cf9dcb1e97bbcff329d95fd3859a873be314a5a58073b4690"}, + {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win32.whl", hash = "sha256:c5cf43bb1dccf3a302d16572d53f26479d277e02606531782c364056c2323678"}, + {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:10c84276ff182a06da6deb1ba9ad375f9b3fbc15c3684a160e775005d915197a"}, + {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:473cc57664bfd5401ec171c8f55079cdc8a980210f2c82fb2945361ea640bfbf"}, + {file = "winrt_Windows.Foundation-2.2.0-cp39-cp39-win32.whl", hash = "sha256:32578bd31eda714bc5cb5b10f0e778c720a2e45bc9b3c60690faa1615336047d"}, + {file = "winrt_Windows.Foundation-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bfb62127959f56fdacad6a817176a8b22cf6917a0d5c3e5d25cdad33a90173a"}, + {file = "winrt_Windows.Foundation-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:07ea5a2f05cb9fb433371e55f70fbe27f32a6eb07ae28042f01678b4d82d823a"}, + {file = "winrt_windows_foundation-2.2.0.tar.gz", hash = "sha256:9a76291204900cd92008163fbe273ae43c9a925ca4a5a29cdd736e59cd397bf1"}, ] [package.dependencies] -winrt-runtime = "2.0.0-beta.1" +winrt-runtime = "2.2.0" [package.extras] -all = ["winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)"] +all = ["winrt-Windows.Foundation.Collections[all] (==2.2.0)"] [[package]] name = "winrt-windows-foundation-collections" -version = "2.0.0b1" +version = "2.2.0" 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"}, +python-versions = "<3.14,>=3.9" +files = [ + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win32.whl", hash = "sha256:92a031fca53910c8bce683391888ba3427db178fc47653310de16fb7e9131e9d"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:a71925d738a443cf27522f34ced84730f1b325f69ccdd0145580e6078d4481c5"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:74c9419b26b510e6e95182e02dc55a78094b6f2af5002330467d030ae6d0b765"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win32.whl", hash = "sha256:8a76d79be0af1840b9c5ac1879dcf5aa65b512accd8278ac6424dcbfdb2a6fe1"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:b18dcd7bc8cf70758b965397e26da725ac345dd9f16b922b0204e8f21ed4d7e6"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d6b0b04683e98989dd611940b5fe36c1338f6d91f43c1bdc88f2f2f1956a968"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win32.whl", hash = "sha256:ade4ea4584ba96e39d2b34f1036d8cb40ff2e9609a090562cfd2b8837dc7f828"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:1e896291c5efe0566db84eab13888bee7300392a6811ae85c55ced51bac0b147"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:e44e13027597fcc638073459dcc159a21c57f9dbe0e9a2282326e32386c25bd0"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win32.whl", hash = "sha256:ea7fa3a7ecb754eb09408e7127cd960d316cc1ba60a6440e191a81f14b42265c"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:f338860e27a8a67b386273c73ad10c680a9f40a42e0185cc6443d208a7425ece"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:dd705d4c62bd8c109f2bc667a0c76dc30ef9a1b2ced3e7bd95253a31e39781df"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp39-cp39-win32.whl", hash = "sha256:6798595621ad58473fe9e86f5f58d732628d88f06535b68c4d86cb5aed78f2b3"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c8ac098a60dad586e950a8236bab09ae57b6a08147d36db6b0aed135a9a81831"}, + {file = "winrt_Windows.Foundation.Collections-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:c67105ebd88faf10d2941516c0ea9f73d9282fb8a7d2a73163a7a7e013bba839"}, + {file = "winrt_windows_foundation_collections-2.2.0.tar.gz", hash = "sha256:10db64da49185af3e14465cd65ec4055eb122a96daedb73b774889f3b7fcfa63"}, ] [package.dependencies] -winrt-runtime = "2.0.0-beta.1" +winrt-runtime = "2.2.0" [package.extras] -all = ["winrt-Windows.Foundation[all] (==2.0.0-beta.1)"] +all = ["winrt-Windows.Foundation[all] (==2.2.0)"] [[package]] name = "winrt-windows-storage-streams" -version = "2.0.0b1" +version = "2.2.0" 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"}, +python-versions = "<3.14,>=3.9" +files = [ + {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win32.whl", hash = "sha256:e888ae08f1245f8b6d53783487581fc664683bb29778f2acca6bafb6a78bcc22"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9213576d566398657142372aa34354b9f7b8ce0581cff308c7afbc0d908368a1"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:49d2bdd749994fb81c813f02f3c506fff580f358083b65a123308f322c2fe6cf"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win32.whl", hash = "sha256:db4ebe7ed79a585a1bb78a3f8cea05f7d74a6a8bc913f61b31ddfe3ae10d134d"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9f77c5398eb90c58645c62b6f278f701d2636c0007817cc6fc28256adbebdcb"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:894c2616eeae887275a1a64a4233964f9466ee1281b8c11ec7c06d64aafec88a"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win32.whl", hash = "sha256:85a2eefb2935db92d10b8e9be836c431d47298b566b55da633b11f822c63838d"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f88cdc6204219c7f1b58d793826ea2eff013a45306fbb340d61c10896c237547"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:78af200d0db5ebe151b1df194de97f1e71c2d5f5cba4da09798c15402f4ab91d"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win32.whl", hash = "sha256:6408184ba5d17e0d408d7c0b85357a58f13c775521d17a8730f1a680553e0061"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:ad9cd8e97cf4115ba074ec153ab273c370e690abb010d8b3b970339d20f94321"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c467cf04005b72efd769ea99c7c15973db44d5ac6084a7c7714af85e49981abd"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp39-cp39-win32.whl", hash = "sha256:f72559b5de7c3a0cab97cd50ab594a0e3278df4d38e03f79b5b2d2e13e926c4c"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:37bf5bb801aa1e4a4c6f3ddfe2b8c9b05d7726ebfdfc8b9bfe41bdcc3866749b"}, + {file = "winrt_Windows.Storage.Streams-2.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:2dcab77a7affb1136503edec82a755b82716abd882fadd5f50ce260438b9c21b"}, + {file = "winrt_windows_storage_streams-2.2.0.tar.gz", hash = "sha256:46a8718c4e00a129d305f03571789f4bed530c05e135c2476494af93f374b68a"}, ] [package.dependencies] -winrt-runtime = "2.0.0-beta.1" +winrt-runtime = "2.2.0" [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)"] +all = ["winrt-Windows.Foundation.Collections[all] (==2.2.0)", "winrt-Windows.Foundation[all] (==2.2.0)", "winrt-Windows.Storage[all] (==2.2.0)", "winrt-Windows.System[all] (==2.2.0)"] [[package]] name = "yarl" @@ -1899,10 +2022,7 @@ files = [ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] -[extras] -scan = ["bleak"] - [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "828fc3307e8314148461691a7ef95572699b2e9597713a118c469a5532c65d61" +content-hash = "91a68ea081419a03ce35f7be2401ca292fe077b35bbd38f901a5cb0ead58cbd6" diff --git a/pyproject.toml b/pyproject.toml index c7a5aad..b63e849 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "FindMy" -version = "0.5.0" +version = "v0.7.3" description = "Everything you need to work with Apple's Find My network!" authors = ["Mike Almeloo "] readme = "README.md" @@ -8,20 +8,35 @@ packages = [{ include = "findmy" }] [tool.poetry.dependencies] python = ">=3.9,<3.13" -srp = "^1.0.20" -cryptography = "^42.0.5" -beautifulsoup4 = "^4.12.2" -aiohttp = "^3.9.1" -bleak = "^0.21.1" +srp = "^1.0.21" +cryptography = ">=42.0.0,<44.0.0" +beautifulsoup4 = "^4.12.3" +aiohttp = "^3.9.5" +bleak = "^0.22.2" +typing-extensions = "^4.12.2" -[tool.poetry.extras] -scan = ["bleak"] +[tool.poetry.group.dev] +optional = true [tool.poetry.group.dev.dependencies] -pre-commit = "^3.6.0" +pre-commit = "^3.8.0" +pyright = "1.1.378" +ruff = "0.6.3" +tomli = "^2.0.1" +packaging = "^24.1" + +[tool.poetry.group.test] +optional = true + +[tool.poetry.group.test.dependencies] +pytest = "^8.3.2" + +[tool.poetry.group.docs] +optional = true + +[tool.poetry.group.docs.dependencies] sphinx = "^7.2.6" -sphinx-autoapi = "^3.0.0" -pyright = "^1.1.350" +sphinx-autoapi = "3.3.1" furo = "^2024.1.29" myst-parser = "^2.0.0" @@ -33,11 +48,20 @@ venv = ".venv" typeCheckingMode = "standard" reportImplicitOverride = true +# examples should be run from their own directory +executionEnvironments = [ + { root = "examples/" } +] + [tool.ruff] +line-length = 100 + exclude = [ "docs/", + "tests/" ] +[tool.ruff.lint] select = [ "ALL", ] @@ -50,12 +74,13 @@ ignore = [ "D212", # multi-line docstring start at first line "D105", # docstrings in magic methods + "S101", # assert statements + "S603", # false-positive subprocess call (https://github.com/astral-sh/ruff/issues/4045) + "PLR2004", # "magic" values >.> "FBT", # boolean "traps" ] -line-length = 100 - [tool.ruff.lint.per-file-ignores] "examples/*" = [ "T201", # use of "print" @@ -63,6 +88,10 @@ line-length = 100 "D", # documentation "INP001", # namespacing ] +"scripts/*" = [ + "T201", # use of "print" + "D", # documentation +] [build-system] requires = ["poetry-core"] diff --git a/scripts/refactor_readme.py b/scripts/refactor_readme.py new file mode 100755 index 0000000..ea6ac4d --- /dev/null +++ b/scripts/refactor_readme.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +"""Script to resolve relative URLs in README prior to release.""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + + +def main(args: list[str]) -> int: + if len(args) < 1: + print("No README path supplied.") + return 1 + + remote_url = ( + subprocess.run( + ["/usr/bin/env", "git", "remote", "get-url", "origin"], + check=True, + capture_output=True, + ) + .stdout.decode("utf-8") + .strip() + ) + + # Convert SSH remote URLs to HTTPS + remote_url = re.sub(r"^ssh://git@", "https://", remote_url) + + readme_path = Path(args[0]) + readme_content = readme_path.read_text("utf-8") + + new_content = re.sub( + r"(\[[^]]+]\()((?!https?:)[^)]+)(\))", + lambda m: m.group(1) + remote_url + "/blob/main/" + m.group(2) + m.group(3), + readme_content, + ) + + readme_path.write_text(new_content) + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/scripts/supported_py_versions.py b/scripts/supported_py_versions.py new file mode 100755 index 0000000..23c9743 --- /dev/null +++ b/scripts/supported_py_versions.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import json +from itertools import count +from pathlib import Path +from typing import Generator + +import tomli +from packaging.specifiers import SpecifierSet +from packaging.version import Version + + +def get_python_versions() -> Generator[str, None, None]: + """Get all python versions this package is compatible with.""" + with Path("pyproject.toml").open("rb") as f: + pyproject_data = tomli.load(f) + + specifier = SpecifierSet(pyproject_data["tool"]["poetry"]["dependencies"]["python"]) + + below_spec = True + for v_minor in count(): + version = Version(f"3.{v_minor}") + + # in specifier: yield + if version in specifier: + below_spec = False + yield str(version) + continue + + # below specifier: skip + if below_spec: + continue + + # above specifier: return + return + + +print(json.dumps(list(get_python_versions()))) diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..21ec685 --- /dev/null +++ b/shell.nix @@ -0,0 +1,14 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + packages = with pkgs; [ + python312 + poetry + ]; + + shellHook = '' + if [[ -d .venv/ ]]; then + source .venv/bin/activate + fi + ''; +} \ No newline at end of file diff --git a/tests/test_keygen.py b/tests/test_keygen.py new file mode 100644 index 0000000..3b51e6e --- /dev/null +++ b/tests/test_keygen.py @@ -0,0 +1,11 @@ +import pytest + + +@pytest.mark.parametrize('execution_number', range(100)) +def test_import(execution_number): + import findmy + + kp = findmy.KeyPair.new() + assert len(kp.private_key_bytes) == 28 + assert len(kp.adv_key_bytes) == 28 + assert len(kp.hashed_adv_key_bytes) == 32