Merge branch 'main' into feat/better-docs

This commit is contained in:
Mike A.
2024-09-03 22:02:30 +02:00
38 changed files with 1529 additions and 598 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use nix

View File

@@ -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 }}

View File

@@ -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: |

View File

@@ -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

View File

@@ -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

56
.github/workflows/test.yml vendored Normal file
View File

@@ -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

1
.gitignore vendored
View File

@@ -163,3 +163,4 @@ cython_debug/
account.json
airtag.plist
DO_NOT_COMMIT*
.direnv/

View File

@@ -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

View File

@@ -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

View File

@@ -1,3 +1,5 @@
# ruff: noqa: ASYNC230
import json
from pathlib import Path

View File

@@ -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__":

View File

@@ -1,4 +1,5 @@
import logging
import sys
from _login import get_account_sync
@@ -8,30 +9,30 @@ from findmy.reports import RemoteAnisetteProvider
# URL to (public or local) anisette server
ANISETTE_SERVER = "http://localhost:6969"
# Private base64-encoded key to look up
KEY_PRIV = ""
# Optional, to verify that advertisement key derivation works for your key
KEY_ADV = ""
logging.basicConfig(level=logging.DEBUG)
logging.basicConfig(level=logging.INFO)
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]} <private key>", 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]))

View File

@@ -1,5 +1,6 @@
import asyncio
import logging
import sys
from _login import get_account_async
@@ -9,34 +10,33 @@ from findmy.reports import RemoteAnisetteProvider
# URL to (public or local) anisette server
ANISETTE_SERVER = "http://localhost:6969"
# Private base64-encoded key to look up
KEY_PRIV = ""
# Optional, to verify that advertisement key derivation works for your key
KEY_ADV = ""
logging.basicConfig(level=logging.DEBUG)
logging.basicConfig(level=logging.INFO)
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]} <private key>", 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]))

View File

@@ -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)
with Path(plist_path).open("rb") as f:
airtag = FindMyAccessory.from_plist(f)
# 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)
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]} <path to accessory plist>", 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]))

View File

@@ -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

View File

@@ -3,11 +3,14 @@ Module to interact with accessories that implement Find My.
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 @@ from .util import crypto
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 @@ class FindMyAccessory:
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 @@ class FindMyAccessory:
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 @@ class AccessoryKeyGenerator(KeyGenerator[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]:

View File

@@ -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.

View File

@@ -1,4 +1,5 @@
"""Module to work with private and public keys as used in FindMy accessories."""
from __future__ import annotations
import base64
@@ -22,7 +23,37 @@ class KeyType(Enum):
SECONDARY = 2
class HasPublicKey(ABC):
class HasHashedPublicKey(ABC):
"""
ABC for anything that has a public, hashed FindMy-key.
Also called a "hashed advertisement" key or "lookup" key.
"""
@property
@abstractmethod
def hashed_adv_key_bytes(self) -> bytes:
"""Return the hashed advertised (public) key as bytes."""
raise NotImplementedError
@property
def hashed_adv_key_b64(self) -> str:
"""Return the hashed advertised (public) key as a base64-encoded string."""
return base64.b64encode(self.hashed_adv_key_bytes).decode("ascii")
@override
def __hash__(self) -> int:
return crypto.bytes_to_int(self.hashed_adv_key_bytes)
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, HasHashedPublicKey):
return NotImplemented
return self.hashed_adv_key_bytes == other.hashed_adv_key_bytes
class HasPublicKey(HasHashedPublicKey, ABC):
"""
ABC for anything that has a public FindMy-key.
@@ -41,26 +72,11 @@ class HasPublicKey(ABC):
return base64.b64encode(self.adv_key_bytes).decode("ascii")
@property
@override
def hashed_adv_key_bytes(self) -> bytes:
"""Return the hashed advertised (public) key as bytes."""
"""See `HasHashedPublicKey.hashed_adv_key_bytes`."""
return hashlib.sha256(self.adv_key_bytes).digest()
@property
def hashed_adv_key_b64(self) -> str:
"""Return the hashed advertised (public) key as a base64-encoded string."""
return base64.b64encode(self.hashed_adv_key_bytes).decode("ascii")
@override
def __hash__(self) -> int:
return crypto.bytes_to_int(self.adv_key_bytes)
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, HasPublicKey):
return NotImplemented
return self.adv_key_bytes == other.adv_key_bytes
class KeyPair(HasPublicKey):
"""A private-public keypair for a trackable FindMy accessory."""
@@ -141,13 +157,11 @@ class KeyGenerator(ABC, Generic[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]:

View File

@@ -1,4 +1,5 @@
"""Code related to fetching location reports."""
from .account import AppleAccount, AsyncAppleAccount
from .anisette import BaseAnisetteProvider, RemoteAnisetteProvider
from .state import LoginState

View File

@@ -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 @@ from typing import (
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 @@ from .twofactor import (
)
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 @@ class BaseAppleAccount(Closable, ABC):
"""
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]]]:
"""
Fetch location reports for a sequence of `KeyPair`s between `date_from` and `date_end`.
) -> MaybeCoro[list[LocationReport]]: ...
Returns a dictionary mapping `KeyPair`s to a list of their location reports.
@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 `HasHashedPublicKey`s between `date_from` and `date_end`.
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[HasHashedPublicKey],
hours: int = 7 * 24,
) -> 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: Sequence[KeyPair],
keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource,
hours: int = 7 * 24,
) -> MaybeCoro[dict[KeyPair, list[LocationReport]]]:
) -> 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 @@ class AsyncAppleAccount(BaseAppleAccount):
)
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 @@ class AsyncAppleAccount(BaseAppleAccount):
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 @@ class AsyncAppleAccount(BaseAppleAccount):
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 @@ class AsyncAppleAccount(BaseAppleAccount):
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 @@ class AppleAccount(BaseAppleAccount):
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)

View File

@@ -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 @@ class BaseAnisetteProvider(Closable, ABC):
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 @@ class RemoteAnisetteProvider(BaseAnisetteProvider):
self._http = HttpSession()
self._anisette_data: dict[str, str] | None = None
self._anisette_data_expires_at: float = 0
@property
@override
@@ -197,11 +202,12 @@ class RemoteAnisetteProvider(BaseAnisetteProvider):
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)

View File

@@ -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.asymmetric import ec
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 @@ if TYPE_CHECKING:
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()
class LocationReport(HasHashedPublicKey):
"""Location report corresponding to a certain `HasHashedPublicKey`."""
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:
"""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 payload(self) -> bytes:
"""Full (partially encrypted) payload of the report, as retrieved from Apple."""
return self._payload
@property
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._publish_date
@property
def timestamp(self) -> datetime:
"""The `datetime` when this report was recorded by a device."""
return self._timestamp
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
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
@classmethod
def from_payload(
cls,
key: KeyPair,
publish_date: datetime,
description: str,
payload: bytes,
) -> LocationReport:
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()
if not isinstance(other, LocationReport):
return NotImplemented
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,
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 @@ class LocationReport:
@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 @@ class LocationReportsFetcher:
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[KeyPair],
) -> dict[KeyPair, list[LocationReport]]:
...
device: Sequence[HasHashedPublicKey],
) -> dict[HasHashedPublicKey, list[LocationReport]]: ...
@overload
async def fetch_reports(
self,
date_from: datetime,
date_to: datetime,
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 @@ class LocationReportsFetcher:
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

View File

@@ -1,4 +1,5 @@
"""Account login state."""
from enum import Enum
from typing_extensions import override

View File

@@ -1,4 +1,5 @@
"""Public classes related to handling two-factor authentication."""
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Generic, TypeVar

View File

@@ -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",
)

View File

@@ -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 @@ if TYPE_CHECKING:
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 @@ class OfflineFindingDevice(HasPublicKey):
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 @@ class OfflineFindingScanner:
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,

View File

@@ -1,4 +1,5 @@
"""Utility functions and classes. Intended for internal use."""
from .http import HttpResponse, HttpSession
from .parsers import decode_plist

View File

@@ -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 @@ class Closable(ABC):
"""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

View File

@@ -11,9 +11,12 @@ from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
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,

View File

@@ -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 @@ from .parsers import decode_plist
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 @@ class HttpSession(Closable):
"""
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())

View File

@@ -1,4 +1,5 @@
"""Parsers for various forms of data formats."""
import plistlib
from typing import Any

View File

@@ -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]]

568
poetry.lock generated
View File

@@ -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"
python-versions = "<3.14,>=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"},
{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"
python-versions = "<3.14,>=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"},
{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"
python-versions = "<3.14,>=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"},
{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"
python-versions = "<3.14,>=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"},
{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"
python-versions = "<3.14,>=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"},
{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"
python-versions = "<3.14,>=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"},
{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"
python-versions = "<3.14,>=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"},
{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"

View File

@@ -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 <git@mikealmel.ooo>"]
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
[tool.ruff]
exclude = [
"docs/",
# 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"]

46
scripts/refactor_readme.py Executable file
View File

@@ -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:]))

View File

@@ -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())))

14
shell.nix Normal file
View File

@@ -0,0 +1,14 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
packages = with pkgs; [
python312
poetry
];
shellHook = ''
if [[ -d .venv/ ]]; then
source .venv/bin/activate
fi
'';
}

11
tests/test_keygen.py Normal file
View File

@@ -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