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

@@ -17,16 +17,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - uses: './.github/actions/setup-project'
uses: actions/setup-python@v5
with: with:
python-version: '3.10' dependency-groups: 'docs'
- name: Install dependencies
run: |
python -m pip install poetry
poetry config virtualenvs.in-project true
poetry install
- name: Build documentation - name: Build documentation
run: | run: |

View File

@@ -3,26 +3,17 @@ name: Pre-commit
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches-ignore:
- main
jobs: jobs:
deploy: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - uses: './.github/actions/setup-project'
uses: actions/setup-python@v5
with: with:
python-version: '3.10' dependency-groups: 'dev,test'
- name: Install dependencies
run: |
python -m pip install poetry
poetry config virtualenvs.in-project true
poetry install
- uses: pre-commit/action@v3.0.1 - uses: pre-commit/action@v3.0.1

View File

@@ -15,16 +15,13 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies - uses: './.github/actions/setup-project'
run: | with:
python -m pip install poetry dependency-groups: 'dev'
poetry config virtualenvs.in-project true
poetry install - name: Prepare README
run: ./scripts/refactor_readme.py README.md
- name: Build package - name: Build package
run: poetry build 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 account.json
airtag.plist airtag.plist
DO_NOT_COMMIT* DO_NOT_COMMIT*
.direnv/

View File

@@ -1,11 +1,11 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.9 rev: v0.6.3
hooks: hooks:
- id: ruff - id: ruff
args: ["--fix"] args: ["--fix"]
- id: ruff-format - id: ruff-format
- repo: https://github.com/RobertCraigie/pyright-python - repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.350 rev: v1.1.378
hooks: hooks:
- id: pyright - id: pyright

View File

@@ -1,6 +1,7 @@
# FindMy.py # FindMy.py
[![](https://img.shields.io/pypi/v/FindMy)](https://pypi.org/project/FindMy/) [![](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/github/license/malmeloo/FindMy.py)](LICENSE.md)
[![](https://img.shields.io/pypi/pyversions/FindMy)](#) [![](https://img.shields.io/pypi/pyversions/FindMy)](#)
@@ -19,13 +20,15 @@ application wishing to integrate with the Find My network.
> without prior warning. > without prior warning.
> >
> You are encouraged to report any issues you can find on the > 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 ### Features
- [x] Cross-platform: no Mac needed - [x] Cross-platform: no Mac needed
- [x] Fetch location reports - [x] Fetch and decrypt location reports
- [x] Apple acount sign-in - [x] Official accessories (AirTags, iDevices, etc.)
- [x] Custom AirTags (OpenHaystack)
- [x] Apple account sign-in
- [x] SMS 2FA support - [x] SMS 2FA support
- [x] Trusted Device 2FA support - [x] Trusted Device 2FA support
- [x] Scan for nearby FindMy-devices - [x] Scan for nearby FindMy-devices
@@ -36,8 +39,7 @@ application wishing to integrate with the Find My network.
### Roadmap ### Roadmap
- [ ] Local anisette generation (without server) - [ ] Local anisette generation (without server)
- Can be done using [pyprovision](https://github.com/Dadoum/pyprovision/), - More information: [#2](https://github.com/malmeloo/FindMy.py/issues/2)
however I want to wait until Python wheels are available.
## Installation ## Installation

View File

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

View File

@@ -1,19 +1,32 @@
import asyncio import asyncio
import logging import logging
from findmy.scanner import OfflineFindingScanner from findmy import KeyPair
from findmy.scanner import (
NearbyOfflineFindingDevice,
OfflineFindingScanner,
SeparatedOfflineFindingDevice,
)
logging.basicConfig(level=logging.INFO) 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("")
async def scan() -> None:
scanner = await OfflineFindingScanner.create()
print("Scanning for FindMy-devices...") 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() print()
async for device in scanner.scan_for(10, extend_timeout=True):
print(f"Device - {device.mac_address}") def _print_separated(device: SeparatedOfflineFindingDevice) -> None:
print(f"SEPARATED Device - {device.mac_address}")
print(f" Public key: {device.adv_key_b64}") print(f" Public key: {device.adv_key_b64}")
print(f" Lookup key: {device.hashed_adv_key_b64}") print(f" Lookup key: {device.hashed_adv_key_b64}")
print(f" Status byte: {device.status:x}") print(f" Status byte: {device.status:x}")
@@ -24,5 +37,32 @@ async def scan() -> None:
print() print()
async def scan() -> None:
scanner = await OfflineFindingScanner.create()
print("Scanning for FindMy-devices...")
print()
scan_device = None
async for device in scanner.scan_for(10, extend_timeout=True):
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__": if __name__ == "__main__":
asyncio.run(scan()) asyncio.run(scan())

View File

@@ -1,4 +1,5 @@
import logging import logging
import sys
from _login import get_account_sync from _login import get_account_sync
@@ -8,30 +9,30 @@ from findmy.reports import RemoteAnisetteProvider
# URL to (public or local) anisette server # URL to (public or local) anisette server
ANISETTE_SERVER = "http://localhost:6969" ANISETTE_SERVER = "http://localhost:6969"
# Private base64-encoded key to look up logging.basicConfig(level=logging.INFO)
KEY_PRIV = ""
# Optional, to verify that advertisement key derivation works for your key
KEY_ADV = ""
logging.basicConfig(level=logging.DEBUG)
def fetch_reports(lookup_key: KeyPair) -> None: def fetch_reports(priv_key: str) -> int:
anisette = RemoteAnisetteProvider(ANISETTE_SERVER) key = KeyPair.from_b64(priv_key)
acc = get_account_sync(anisette) acc = get_account_sync(
RemoteAnisetteProvider(ANISETTE_SERVER),
)
print(f"Logged in as: {acc.account_name} ({acc.first_name} {acc.last_name})") print(f"Logged in as: {acc.account_name} ({acc.first_name} {acc.last_name})")
# It's that simple! # It's that simple!
reports = acc.fetch_last_reports([lookup_key])[lookup_key] reports = acc.fetch_last_reports(key)
for report in sorted(reports): for report in sorted(reports):
print(report) print(report)
return 1
if __name__ == "__main__": if __name__ == "__main__":
key = KeyPair.from_b64(KEY_PRIV) if len(sys.argv) < 2:
if KEY_ADV: # verify that your adv key is correct :D print(f"Usage: {sys.argv[0]} <private key>", file=sys.stderr)
assert key.adv_key_b64 == KEY_ADV 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 asyncio
import logging import logging
import sys
from _login import get_account_async from _login import get_account_async
@@ -9,34 +10,33 @@ from findmy.reports import RemoteAnisetteProvider
# URL to (public or local) anisette server # URL to (public or local) anisette server
ANISETTE_SERVER = "http://localhost:6969" ANISETTE_SERVER = "http://localhost:6969"
# Private base64-encoded key to look up logging.basicConfig(level=logging.INFO)
KEY_PRIV = ""
# Optional, to verify that advertisement key derivation works for your key
KEY_ADV = ""
logging.basicConfig(level=logging.DEBUG)
async def fetch_reports(lookup_key: KeyPair) -> None: async def fetch_reports(priv_key: str) -> int:
anisette = RemoteAnisetteProvider(ANISETTE_SERVER) key = KeyPair.from_b64(priv_key)
acc = await get_account_async(
acc = await get_account_async(anisette) RemoteAnisetteProvider(ANISETTE_SERVER),
)
try: try:
print(f"Logged in as: {acc.account_name} ({acc.first_name} {acc.last_name})") print(f"Logged in as: {acc.account_name} ({acc.first_name} {acc.last_name})")
# It's that simple! # It's that simple!
reports = await acc.fetch_last_reports([lookup_key]) reports = await acc.fetch_last_reports(key)
print(reports) for report in sorted(reports):
print(report)
finally: finally:
await acc.close() await acc.close()
return 0
if __name__ == "__main__": if __name__ == "__main__":
key = KeyPair.from_b64(KEY_PRIV) if len(sys.argv) < 2:
if KEY_ADV: # verify that your adv key is correct :D print(f"Usage: {sys.argv[0]} <private key>", file=sys.stderr)
assert key.adv_key_b64 == KEY_ADV 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. Example showing how to fetch locations of an AirTag, or any other FindMy accessory.
""" """
from __future__ import annotations from __future__ import annotations
import plistlib import logging
from datetime import datetime, timedelta, timezone import sys
from pathlib import Path from pathlib import Path
from _login import get_account_sync from _login import get_account_sync
from findmy import FindMyAccessory, KeyPair from findmy import FindMyAccessory
from findmy.reports import RemoteAnisetteProvider from findmy.reports import RemoteAnisetteProvider
# URL to (public or local) anisette server # URL to (public or local) anisette server
ANISETTE_SERVER = "http://localhost:6969" ANISETTE_SERVER = "http://localhost:6969"
# Path to a .plist dumped from the Find My app. logging.basicConfig(level=logging.INFO)
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)
def _gen_keys(airtag: FindMyAccessory, _from: datetime, to: datetime) -> set[KeyPair]: def main(plist_path: str) -> int:
keys = set()
while _from < to:
keys.update(airtag.keys_at(_from))
_from += timedelta(minutes=15)
return keys
def main() -> None:
# Step 0: create an accessory key generator # 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, # Step 1: log into an Apple account
# 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
print("Logging into account") print("Logging into account")
anisette = RemoteAnisetteProvider(ANISETTE_SERVER) anisette = RemoteAnisetteProvider(ANISETTE_SERVER)
acc = get_account_sync(anisette) acc = get_account_sync(anisette)
# step 3: fetch reports! # step 2: fetch reports!
print("Fetching 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 # step 3: print 'em
# reports are in {key: [report]} format, but we only really care about the reports
print() print()
print("Location reports:") print("Location reports:")
reports = sorted([r for rs in reports.values() for r in rs]) for report in sorted(reports):
for report in reports:
print(f" - {report}") print(f" - {report}")
return 0
if __name__ == "__main__": 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.""" """A package providing everything you need to work with Apple's FindMy network."""
from . import errors, keys, reports, scanner from . import errors, keys, reports, scanner
from .accessory import FindMyAccessory from .accessory import FindMyAccessory
from .keys import KeyPair 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. Accessories could be anything ranging from AirTags to iPhones.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
from datetime import datetime, timedelta import plistlib
from typing import Generator, overload from abc import ABC, abstractmethod
from datetime import datetime, timedelta, timezone
from typing import IO, Generator, overload
from typing_extensions import override from typing_extensions import override
@@ -17,10 +20,52 @@ from .util import crypto
logging.getLogger(__name__) 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.""" """A findable Find My-accessory using official key rollover."""
def __init__( # noqa: PLR0913 def __init__(
self, self,
master_key: bytes, master_key: bytes,
skn: bytes, skn: bytes,
@@ -47,8 +92,20 @@ class FindMyAccessory:
self._name = name 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]: def keys_at(self, ind: int | datetime) -> set[KeyPair]:
"""Get the potential primary and secondary keys active at a certain time or index.""" """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 secondary_offset = 0
if isinstance(ind, datetime): if isinstance(ind, datetime):
@@ -88,6 +145,30 @@ class FindMyAccessory:
return possible_keys 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]): class AccessoryKeyGenerator(KeyGenerator[KeyPair]):
"""KeyPair generator. Uses the same algorithm internally as FindMy accessories do.""" """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) return self._get_keypair(self._iter_ind)
@overload @overload
def __getitem__(self, val: int) -> KeyPair: def __getitem__(self, val: int) -> KeyPair: ...
...
@overload @overload
def __getitem__(self, val: slice) -> Generator[KeyPair, None, None]: def __getitem__(self, val: slice) -> Generator[KeyPair, None, None]: ...
...
@override @override
def __getitem__(self, val: int | slice) -> KeyPair | Generator[KeyPair, None, None]: 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.""" """Raised when credentials are incorrect."""
class UnauthorizedError(Exception):
"""Raised when an authorization error occurs."""
class UnhandledProtocolError(RuntimeError): class UnhandledProtocolError(RuntimeError):
""" """
Raised when an unexpected error occurs while communicating with Apple servers. 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.""" """Module to work with private and public keys as used in FindMy accessories."""
from __future__ import annotations from __future__ import annotations
import base64 import base64
@@ -22,7 +23,37 @@ class KeyType(Enum):
SECONDARY = 2 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. 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") return base64.b64encode(self.adv_key_bytes).decode("ascii")
@property @property
@override
def hashed_adv_key_bytes(self) -> bytes: 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() 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): class KeyPair(HasPublicKey):
"""A private-public keypair for a trackable FindMy accessory.""" """A private-public keypair for a trackable FindMy accessory."""
@@ -141,13 +157,11 @@ class KeyGenerator(ABC, Generic[K]):
@overload @overload
@abstractmethod @abstractmethod
def __getitem__(self, val: int) -> K: def __getitem__(self, val: int) -> K: ...
...
@overload @overload
@abstractmethod @abstractmethod
def __getitem__(self, val: slice) -> Generator[K, None, None]: def __getitem__(self, val: slice) -> Generator[K, None, None]: ...
...
@abstractmethod @abstractmethod
def __getitem__(self, val: int | slice) -> K | Generator[K, None, None]: def __getitem__(self, val: int | slice) -> K | Generator[K, None, None]:

View File

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

View File

@@ -1,4 +1,5 @@
"""Module containing most of the code necessary to interact with an Apple account.""" """Module containing most of the code necessary to interact with an Apple account."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@@ -14,22 +15,26 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Callable, Callable,
Concatenate,
ParamSpec,
Sequence, Sequence,
TypedDict, TypedDict,
TypeVar, TypeVar,
cast, cast,
overload,
) )
import bs4 import bs4
import srp._pysrp as srp 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 import crypto
from findmy.util.closable import Closable 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 .reports import LocationReport, LocationReportsFetcher
from .state import LoginState from .state import LoginState
@@ -44,7 +49,8 @@ from .twofactor import (
) )
if TYPE_CHECKING: 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 findmy.util.types import MaybeCoro
from .anisette import BaseAnisetteProvider from .anisette import BaseAnisetteProvider
@@ -215,28 +221,79 @@ class BaseAppleAccount(Closable, ABC):
""" """
raise NotImplementedError raise NotImplementedError
@overload
@abstractmethod @abstractmethod
def fetch_reports( def fetch_reports(
self, self,
keys: Sequence[KeyPair], keys: HasHashedPublicKey,
date_from: datetime, date_from: datetime,
date_to: datetime | None, date_to: datetime | None,
) -> MaybeCoro[dict[KeyPair, list[LocationReport]]]: ) -> MaybeCoro[list[LocationReport]]: ...
"""
Fetch location reports for a sequence of `KeyPair`s between `date_from` and `date_end`.
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 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 @abstractmethod
def fetch_last_reports( def fetch_last_reports(
self, self,
keys: Sequence[KeyPair], keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource,
hours: int = 7 * 24, 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. 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}]} data = {"search": [{"startDate": start, "endDate": end, "ids": ids}]}
r = await self._http.post( async def _do_request() -> HttpResponse:
return await self._http.post(
self._ENDPOINT_REPORTS_FETCH, self._ENDPOINT_REPORTS_FETCH,
auth=auth, auth=auth,
headers=await self.get_anisette_headers(), headers=await self.get_anisette_headers(),
json=data, 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() resp = r.json()
if not r.ok or resp["statusCode"] != "200": except json.JSONDecodeError:
msg = f"Failed to fetch reports: {resp['statusCode']}" resp = {}
if not r.ok or resp.get("statusCode") != "200":
msg = f"Failed to fetch reports: {resp.get('statusCode')}"
raise UnhandledProtocolError(msg) raise UnhandledProtocolError(msg)
return resp 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) @require_login_state(LoginState.LOGGED_IN)
@override @override
async def fetch_reports( async def fetch_reports(
self, self,
keys: Sequence[KeyPair], keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource,
date_from: datetime, date_from: datetime,
date_to: datetime | None, date_to: datetime | None,
) -> dict[KeyPair, list[LocationReport]]: ) -> list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]:
"""See `BaseAppleAccount.fetch_reports`.""" """See `BaseAppleAccount.fetch_reports`."""
date_to = date_to or datetime.now().astimezone() date_to = date_to or datetime.now().astimezone()
@@ -556,20 +658,41 @@ class AsyncAppleAccount(BaseAppleAccount):
keys, 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) @require_login_state(LoginState.LOGGED_IN)
@override @override
async def fetch_last_reports( async def fetch_last_reports(
self, self,
keys: Sequence[KeyPair], keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource,
hours: int = 7 * 24, hours: int = 7 * 24,
) -> dict[KeyPair, list[LocationReport]]: ) -> list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]:
"""See `BaseAppleAccount.fetch_last_reports`.""" """See `BaseAppleAccount.fetch_last_reports`."""
end = datetime.now(tz=timezone.utc) end = datetime.now(tz=timezone.utc)
start = end - timedelta(hours=hours) start = end - timedelta(hours=hours)
return await self.fetch_reports(keys, start, end) 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( async def _gsa_authenticate(
self, self,
username: str | None = None, username: str | None = None,
@@ -599,13 +722,13 @@ class AsyncAppleAccount(BaseAppleAccount):
msg = "Email verification failed: " + r["Status"].get("em") msg = "Email verification failed: " + r["Status"].get("em")
raise InvalidCredentialsError(msg) raise InvalidCredentialsError(msg)
sp = r.get("sp") sp = r.get("sp")
if sp != "s2k": if not isinstance(sp, str) or sp not in {"s2k", "s2k_fo"}:
msg = f"This implementation only supports s2k. Server returned {sp}" msg = f"This implementation only supports s2k and sk2_fo. Server returned {sp}"
raise UnhandledProtocolError(msg) raise UnhandledProtocolError(msg)
logging.debug("Attempting password challenge") 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"]) m1 = usr.process_challenge(r["s"], r["B"])
if m1 is None: if m1 is None:
msg = "Failed to process challenge" msg = "Failed to process challenge"
@@ -695,9 +818,9 @@ class AsyncAppleAccount(BaseAppleAccount):
data = resp.plist() data = resp.plist()
mobileme_data = data.get("delegates", {}).get("com.apple.mobileme", {}) 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: 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}" msg = f"com.apple.mobileme login failed with status {status}: {status_message}"
raise UnhandledProtocolError(msg) raise UnhandledProtocolError(msg)
@@ -894,23 +1017,68 @@ class AppleAccount(BaseAppleAccount):
coro = self._asyncacc.td_2fa_submit(code) coro = self._asyncacc.td_2fa_submit(code)
return self._evt_loop.run_until_complete(coro) 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 @override
def fetch_reports( def fetch_reports(
self, self,
keys: Sequence[KeyPair], keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource,
date_from: datetime, date_from: datetime,
date_to: datetime | None, date_to: datetime | None,
) -> dict[KeyPair, list[LocationReport]]: ) -> list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]:
"""See `AsyncAppleAccount.fetch_reports`.""" """See `AsyncAppleAccount.fetch_reports`."""
coro = self._asyncacc.fetch_reports(keys, date_from, date_to) coro = self._asyncacc.fetch_reports(keys, date_from, date_to)
return self._evt_loop.run_until_complete(coro) 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 @override
def fetch_last_reports( def fetch_last_reports(
self, self,
keys: Sequence[KeyPair], keys: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource,
hours: int = 7 * 24, hours: int = 7 * 24,
) -> dict[KeyPair, list[LocationReport]]: ) -> list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]:
"""See `AsyncAppleAccount.fetch_last_reports`.""" """See `AsyncAppleAccount.fetch_last_reports`."""
coro = self._asyncacc.fetch_last_reports(keys, hours) coro = self._asyncacc.fetch_last_reports(keys, hours)
return self._evt_loop.run_until_complete(coro) return self._evt_loop.run_until_complete(coro)

View File

@@ -1,9 +1,11 @@
"""Module for Anisette header providers.""" """Module for Anisette header providers."""
from __future__ import annotations from __future__ import annotations
import base64 import base64
import locale import locale
import logging import logging
import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -160,6 +162,8 @@ class BaseAnisetteProvider(Closable, ABC):
class RemoteAnisetteProvider(BaseAnisetteProvider): class RemoteAnisetteProvider(BaseAnisetteProvider):
"""Anisette provider. Fetches headers from a remote Anisette server.""" """Anisette provider. Fetches headers from a remote Anisette server."""
_ANISETTE_DATA_VALID_FOR = 30
def __init__(self, server_url: str) -> None: def __init__(self, server_url: str) -> None:
"""Initialize the provider with URL to te remote server.""" """Initialize the provider with URL to te remote server."""
super().__init__() super().__init__()
@@ -169,6 +173,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider):
self._http = HttpSession() self._http = HttpSession()
self._anisette_data: dict[str, str] | None = None self._anisette_data: dict[str, str] | None = None
self._anisette_data_expires_at: float = 0
@property @property
@override @override
@@ -197,11 +202,12 @@ class RemoteAnisetteProvider(BaseAnisetteProvider):
with_client_info: bool = False, with_client_info: bool = False,
) -> dict[str, str]: ) -> dict[str, str]:
"""See `BaseAnisetteProvider.get_headers`_.""" """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) logging.info("Fetching anisette data from %s", self._server_url)
r = await self._http.get(self._server_url) r = await self._http.get(self._server_url)
self._anisette_data = r.json() 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) 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.""" """Module providing functionality to look up location reports."""
from __future__ import annotations from __future__ import annotations
import base64 import base64
import hashlib import hashlib
import logging import logging
import struct import struct
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Sequence, overload from typing import TYPE_CHECKING, Sequence, overload
from cryptography.hazmat.backends import default_backend 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 cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from typing_extensions import override from typing_extensions import override
from findmy.keys import KeyPair from findmy.accessory import RollingKeyPairSource
from findmy.keys import HasHashedPublicKey, KeyPair
if TYPE_CHECKING: if TYPE_CHECKING:
from .account import AsyncAppleAccount from .account import AsyncAppleAccount
@@ -21,128 +23,177 @@ if TYPE_CHECKING:
logging.getLogger(__name__) logging.getLogger(__name__)
def _decrypt_payload(payload: bytes, key: KeyPair) -> bytes: class LocationReport(HasHashedPublicKey):
"""Location report corresponding to a certain `HasHashedPublicKey`."""
def __init__(
self,
payload: bytes,
hashed_adv_key: bytes,
published_at: datetime,
description: str = "",
) -> None:
"""Initialize a `KeyReport`. You should probably use `KeyReport.from_payload` instead."""
self._payload: bytes = payload
self._hashed_adv_key: bytes = hashed_adv_key
self._published_at: datetime = published_at
self._description: str = description
self._decrypted_data: tuple[KeyPair, bytes] | None = None
@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:
"""`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( eph_key = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP224R1(), ec.SECP224R1(),
payload[5:62], encrypted_data[1:58],
) )
shared_key = key.dh_exchange(eph_key) shared_key = key.dh_exchange(eph_key)
symmetric_key = hashlib.sha256( symmetric_key = hashlib.sha256(
shared_key + b"\x00\x00\x00\x01" + payload[5:62], shared_key + b"\x00\x00\x00\x01" + encrypted_data[1:58],
).digest() ).digest()
decryption_key = symmetric_key[:16] decryption_key = symmetric_key[:16]
iv = symmetric_key[16:] iv = symmetric_key[16:]
enc_data = payload[62:72] enc_data = encrypted_data[58:68]
tag = payload[72:] tag = encrypted_data[68:]
decryptor = Cipher( decryptor = Cipher(
algorithms.AES(decryption_key), algorithms.AES(decryption_key),
modes.GCM(iv, tag), modes.GCM(iv, tag),
default_backend(), default_backend(),
).decryptor() ).decryptor()
return decryptor.update(enc_data) + decryptor.finalize() decrypted_payload = decryptor.update(enc_data) + decryptor.finalize()
self._decrypted_data = (key, decrypted_payload)
class LocationReport:
"""Location report corresponding to a certain `KeyPair`."""
def __init__( # noqa: PLR0913
self,
key: KeyPair,
publish_date: datetime,
timestamp: datetime,
description: str,
lat: float,
lng: float,
confidence: int,
status: int,
) -> 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._lat = lat
self._lng = lng
self._confidence = confidence
self._status = status
@property
def key(self) -> KeyPair:
"""The `KeyPair` corresponding to this location report."""
return self._key
@property @property
def published_at(self) -> datetime: def published_at(self) -> datetime:
"""The `datetime` when this report was published by a device.""" """The `datetime` when this report was published by a device."""
return self._publish_date return self._published_at
@property
def timestamp(self) -> datetime:
"""The `datetime` when this report was recorded by a device."""
return self._timestamp
@property @property
def description(self) -> str: def description(self) -> str:
"""Description of the location report as published by Apple.""" """Description of the location report as published by Apple."""
return self._description 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 @property
def latitude(self) -> float: def latitude(self) -> float:
"""Latitude of the location of this report.""" """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 @property
def longitude(self) -> float: def longitude(self) -> float:
"""Longitude of the location of this report.""" """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 @property
def confidence(self) -> int: def confidence(self) -> int:
"""Confidence of the location of this report.""" """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 @property
def status(self) -> int: def status(self) -> int:
"""Status byte of the accessory as recorded by a device, as an integer.""" """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 status_bytes = self._decrypted_data[1][9:10]
def from_payload( return int.from_bytes(status_bytes, "big")
cls,
key: KeyPair, @override
publish_date: datetime, def __eq__(self, other: object) -> bool:
description: str,
payload: bytes,
) -> LocationReport:
""" """
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) if not isinstance(other, LocationReport):
timestamp = datetime.fromtimestamp(timestamp_int, tz=timezone.utc).astimezone() return NotImplemented
data = _decrypt_payload(payload, key) return (
latitude = struct.unpack(">i", data[0:4])[0] / 10000000 super().__eq__(other)
longitude = struct.unpack(">i", data[4:8])[0] / 10000000 and self.timestamp == other.timestamp
confidence = int.from_bytes(data[8:9], "big") and self.latitude == other.latitude
status = int.from_bytes(data[9:10], "big") and self.longitude == other.longitude
return cls(
key,
publish_date,
timestamp,
description,
latitude,
longitude,
confidence,
status,
) )
@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: def __lt__(self, other: LocationReport) -> bool:
""" """
Compare against another `KeyReport`. Compare against another `KeyReport`.
@@ -157,10 +208,11 @@ class LocationReport:
@override @override
def __repr__(self) -> str: def __repr__(self) -> str:
"""Human-readable string representation of the location report.""" """Human-readable string representation of the location report."""
return ( msg = f"KeyReport(hashed_adv_key={self.hashed_adv_key_b64}, timestamp={self.timestamp}"
f"KeyReport(key={self._key.hashed_adv_key_b64}, timestamp={self._timestamp}," if self.is_decrypted:
f" lat={self._lat}, lng={self._lng})" msg += f", lat={self.latitude}, lon={self.longitude}"
) msg += ")"
return msg
class LocationReportsFetcher: class LocationReportsFetcher:
@@ -179,53 +231,79 @@ class LocationReportsFetcher:
self, self,
date_from: datetime, date_from: datetime,
date_to: datetime, date_to: datetime,
device: KeyPair, device: HasHashedPublicKey,
) -> list[LocationReport]: ) -> list[LocationReport]: ...
...
@overload @overload
async def fetch_reports( async def fetch_reports(
self, self,
date_from: datetime, date_from: datetime,
date_to: datetime, date_to: datetime,
device: Sequence[KeyPair], device: Sequence[HasHashedPublicKey],
) -> dict[KeyPair, list[LocationReport]]: ) -> dict[HasHashedPublicKey, list[LocationReport]]: ...
...
@overload
async def fetch_reports(
self,
date_from: datetime,
date_to: datetime,
device: RollingKeyPairSource,
) -> list[LocationReport]: ...
async def fetch_reports( async def fetch_reports(
self, self,
date_from: datetime, date_from: datetime,
date_to: datetime, date_to: datetime,
device: KeyPair | Sequence[KeyPair], device: HasHashedPublicKey | Sequence[HasHashedPublicKey] | RollingKeyPairSource,
) -> list[LocationReport] | dict[KeyPair, list[LocationReport]]: ) -> list[LocationReport] | dict[HasHashedPublicKey, list[LocationReport]]:
""" """
Fetch location reports for a certain device. Fetch location reports for a certain device.
When ``device`` is a single :class:`.KeyPair`, this method will return When ``device`` is a single :class:`.HasHashedPublicKey`, this method will return
a list of location reports corresponding to that pair. a list of location reports corresponding to that key.
When ``device`` is a sequence of :class:`.KeyPair`s, it will return a dictionary When ``device`` is a sequence of :class:`.HasHashedPublicKey`s, it will return a dictionary
with the :class:`.KeyPair` as key, and a list of location reports as value. 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 # single key
if isinstance(device, KeyPair): if isinstance(device, HasHashedPublicKey):
return await self._fetch_reports(date_from, date_to, [device]) 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] = [] reports: list[LocationReport] = []
for key_offset in range(0, len(device), 256): for key_offset in range(0, len(keys), 256):
chunk = device[key_offset : key_offset + 256] chunk = keys[key_offset : key_offset + 256]
reports.extend(await self._fetch_reports(date_from, date_to, chunk)) 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: 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 return res
async def _fetch_reports( async def _fetch_reports(
self, self,
date_from: datetime, date_from: datetime,
date_to: datetime, date_to: datetime,
keys: Sequence[KeyPair], keys: Sequence[HasHashedPublicKey],
) -> list[LocationReport]: ) -> list[LocationReport]:
logging.debug("Fetching reports for %s keys", len(keys)) 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] ids = [key.hashed_adv_key_b64 for key in keys]
data = await self._account.fetch_raw_reports(start_date, end_date, ids) 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] = [] reports: list[LocationReport] = []
for report in data.get("results", []): 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( date_published = datetime.fromtimestamp(
report.get("datePublished", 0) / 1000, report.get("datePublished", 0) / 1000,
tz=timezone.utc, tz=timezone.utc,
).astimezone() ).astimezone()
description = report.get("description", "") 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 return reports

View File

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

View File

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

View File

@@ -1,4 +1,13 @@
"""Utilities related to physically discoverable FindMy-devices.""" """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.""" """Airtag scanner."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging import logging
import time import time
from abc import ABC, abstractmethod
from datetime import datetime
from typing import TYPE_CHECKING, Any, AsyncGenerator from typing import TYPE_CHECKING, Any, AsyncGenerator
from bleak import BleakScanner from bleak import BleakScanner
from typing_extensions import override from typing_extensions import override
from findmy.accessory import RollingKeyPairSource
from findmy.keys import HasPublicKey from findmy.keys import HasPublicKey
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -18,27 +22,30 @@ if TYPE_CHECKING:
logging.getLogger(__name__) logging.getLogger(__name__)
class OfflineFindingDevice(HasPublicKey): class OfflineFindingDevice(ABC):
"""Device discoverable through Apple's bluetooth-based Offline Finding protocol.""" """Device discoverable through Apple's bluetooth-based Offline Finding protocol."""
OF_HEADER_SIZE = 2 OF_HEADER_SIZE = 2
OF_TYPE = 0x12 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, self,
mac_bytes: bytes, mac_bytes: bytes,
status: int, status_byte: int,
public_key: bytes, detected_at: datetime,
hint: int,
additional_data: dict[Any, Any] | None = None, additional_data: dict[Any, Any] | None = None,
) -> None: ) -> None:
"""Initialize an `OfflineFindingDevice`.""" """Instantiate an OfflineFindingDevice."""
self._mac_bytes: bytes = mac_bytes self._mac_bytes: bytes = mac_bytes
self._status: int = status self._status: int = status_byte
self._public_key: bytes = public_key self._detected_at: datetime = detected_at
self._hint: int = hint
self._additional_data: dict[Any, Any] = additional_data or {} self._additional_data: dict[Any, Any] = additional_data or {}
@property @property
@@ -53,60 +60,230 @@ class OfflineFindingDevice(HasPublicKey):
return self._status % 255 return self._status % 255
@property @property
def hint(self) -> int: def detected_at(self) -> datetime:
"""Hint value as reported by the device.""" """Timezone-aware datetime of when the device was detected."""
return self._hint % 255 return self._detected_at
@property @property
def additional_data(self) -> dict[Any, Any]: def additional_data(self) -> dict[Any, Any]:
"""Any additional data. No guarantees about the contents of this dictionary.""" """Any additional data. No guarantees about the contents of this dictionary."""
return self._additional_data 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 @property
@override @override
def adv_key_bytes(self) -> bytes: def adv_key_bytes(self) -> bytes:
"""See `HasPublicKey.adv_key_bytes`.""" """See `HasPublicKey.adv_key_bytes`."""
return self._public_key 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 @classmethod
@override
def from_payload( def from_payload(
cls, cls,
mac_address: str, mac_address: str,
payload: bytes, payload: bytes,
additional_data: dict[Any, Any], detected_at: datetime,
) -> OfflineFindingDevice | None: additional_data: dict[Any, Any] | None = None,
"""Get an OfflineFindingDevice object from a BLE payload.""" ) -> SeparatedOfflineFindingDevice | None:
if len(payload) < cls.OF_HEADER_SIZE: """Get a SeparatedOfflineFindingDevice object from an OF message payload."""
logging.error("Not enough bytes to decode: %s", len(payload)) if len(payload) != cls.payload_len:
return None logging.error(
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(
"Invalid OF data length: %s instead of %s", "Invalid OF data length: %s instead of %s",
len(payload) - cls.OF_HEADER_SIZE, len(payload),
payload[1], payload[1],
) )
return None return None
mac_bytes = bytes.fromhex(mac_address.replace(":", "").replace("-", "")) 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_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_ls = mac_bytes[0] & 0b00111111
pubkey_start = (pubkey_start_ms | pubkey_start_ls).to_bytes(1, "big") pubkey_start = (pubkey_start_ms | pubkey_start_ls).to_bytes(1, "big")
pubkey = pubkey_start + pubkey_middle + pubkey_end 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 @override
def __repr__(self) -> str: def __repr__(self) -> str:
@@ -173,13 +350,20 @@ class OfflineFindingScanner:
if not apple_data: if not apple_data:
return None return None
detected_at = datetime.now().astimezone()
try: try:
additional_data = device.details.get("props", {}) additional_data = device.details.get("props", {})
except AttributeError: except AttributeError:
# Likely Windows host, where details is a '_RawAdvData' object. # Likely Windows host, where details is a '_RawAdvData' object.
# See: https://github.com/malmeloo/FindMy.py/issues/24 # See: https://github.com/malmeloo/FindMy.py/issues/24
additional_data = {} 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( async def scan_for(
self, self,

View File

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

View File

@@ -1,4 +1,5 @@
"""ABC for async classes that need to be cleaned up before exiting.""" """ABC for async classes that need to be cleaned up before exiting."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@@ -29,6 +30,9 @@ class Closable(ABC):
"""Attempt to automatically clean up when garbage collected.""" """Attempt to automatically clean up when garbage collected."""
try: try:
loop = self._loop or asyncio.get_running_loop() loop = self._loop or asyncio.get_running_loop()
if loop.is_running():
loop.call_soon_threadsafe(loop.create_task, self.close()) loop.call_soon_threadsafe(loop.create_task, self.close())
else:
loop.run_until_complete(self.close())
except RuntimeError: except RuntimeError:
pass pass

View File

@@ -11,9 +11,12 @@ from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
P224_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D 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.""" """Encrypt password using PBKDF2-HMAC."""
assert protocol in ["s2k", "s2k_fo"]
p = hashlib.sha256(password.encode("utf-8")).digest() p = hashlib.sha256(password.encode("utf-8")).digest()
if protocol == "s2k_fo":
p = p.hex().encode("utf-8")
kdf = PBKDF2HMAC( kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(), algorithm=hashes.SHA256(),
length=32, length=32,

View File

@@ -1,9 +1,10 @@
"""Module to simplify asynchronous HTTP calls.""" """Module to simplify asynchronous HTTP calls."""
from __future__ import annotations from __future__ import annotations
import json import json
import logging import logging
from typing import Any, TypedDict from typing import Any, TypedDict, cast
from aiohttp import BasicAuth, ClientSession, ClientTimeout from aiohttp import BasicAuth, ClientSession, ClientTimeout
from typing_extensions import Unpack, override from typing_extensions import Unpack, override
@@ -14,13 +15,20 @@ from .parsers import decode_plist
logging.getLogger(__name__) logging.getLogger(__name__)
class _HttpRequestOptions(TypedDict, total=False): class _RequestOptions(TypedDict, total=False):
json: dict[str, Any] | None json: dict[str, Any] | None
headers: dict[str, str] headers: dict[str, str]
auth: tuple[str, str] | BasicAuth
data: bytes data: bytes
class _AiohttpRequestOptions(_RequestOptions):
auth: BasicAuth
class _HttpRequestOptions(_RequestOptions, total=False):
auth: BasicAuth | tuple[str, str]
class HttpResponse: class HttpResponse:
"""Response of a request made by `HttpSession`.""" """Response of a request made by `HttpSession`."""
@@ -94,15 +102,19 @@ class HttpSession(Closable):
""" """
session = await self._get_session() session = await self._get_session()
# cast from http options to library supported options
auth = kwargs.get("auth") auth = kwargs.get("auth")
if isinstance(auth, tuple): if isinstance(auth, tuple):
kwargs["auth"] = BasicAuth(auth[0], auth[1]) kwargs["auth"] = BasicAuth(auth[0], auth[1])
else:
kwargs.pop("auth")
options = cast(_AiohttpRequestOptions, kwargs)
async with await session.request( async with await session.request(
method, method,
url, url,
ssl=False, ssl=False,
**kwargs, **options,
) as r: ) as r:
return HttpResponse(r.status, await r.content.read()) return HttpResponse(r.status, await r.content.read())

View File

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

View File

@@ -1,7 +1,9 @@
"""Utility types.""" """Utility types."""
from typing import Coroutine, TypeVar from typing import Coroutine, TypeVar, Union
T = TypeVar("T") 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]] [[package]]
name = "aiohttp" name = "aiohttp"
@@ -121,17 +121,6 @@ files = [
{file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, {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]] [[package]]
name = "astroid" name = "astroid"
version = "3.1.0" version = "3.1.0"
@@ -213,30 +202,31 @@ lxml = ["lxml"]
[[package]] [[package]]
name = "bleak" name = "bleak"
version = "0.21.1" version = "0.22.2"
description = "Bluetooth Low Energy platform Agnostic Klient" description = "Bluetooth Low Energy platform Agnostic Klient"
optional = false optional = false
python-versions = ">=3.8,<3.13" python-versions = "<3.13,>=3.8"
files = [ files = [
{file = "bleak-0.21.1-py3-none-any.whl", hash = "sha256:ccec260a0f5ec02dd133d68b0351c0151b2ecf3ddd0bcabc4c04a1cdd7f33256"}, {file = "bleak-0.22.2-py3-none-any.whl", hash = "sha256:8395c9e096f28e0ba1f3e6a8619fa21c327c484f720b7af3ea578d04f498a458"},
{file = "bleak-0.21.1.tar.gz", hash = "sha256:ec4a1a2772fb315b992cbaa1153070c7e26968a52b0e2727035f443a1af5c18f"}, {file = "bleak-0.22.2.tar.gz", hash = "sha256:09010c0f4bd843e7dcaa1652e1bfb2450ce690da08d4c6163f0723aaa986e9fe"},
] ]
[package.dependencies] [package.dependencies]
async-timeout = {version = ">=3.0.0,<5", markers = "python_version < \"3.11\""} 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\""} 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\""} dbus-fast = {version = ">=1.83.0,<3", markers = "platform_system == \"Linux\""}
pyobjc-core = {version = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} pyobjc-core = {version = ">=10.0,<11.0", markers = "platform_system == \"Darwin\""}
pyobjc-framework-CoreBluetooth = {version = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} pyobjc-framework-CoreBluetooth = {version = ">=10.0,<11.0", markers = "platform_system == \"Darwin\""}
pyobjc-framework-libdispatch = {version = ">=9.2,<10.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\""} 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-runtime = {version = ">=2,<3", 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" = {version = ">=2,<3", 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.Bluetooth.Advertisement" = {version = ">=2,<3", 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.Devices.Bluetooth.GenericAttributeProfile" = {version = ">=2,<3", 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.Devices.Enumeration" = {version = ">=2,<3", 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.Foundation" = {version = ">=2,<3", 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-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]] [[package]]
name = "bleak-winrt" name = "bleak-winrt"
@@ -573,6 +563,20 @@ files = [
{file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, {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]] [[package]]
name = "filelock" name = "filelock"
version = "3.13.4" version = "3.13.4"
@@ -747,6 +751,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link
perf = ["ipython"] 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)"] 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]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.3" version = "3.1.3"
@@ -1028,13 +1043,13 @@ setuptools = "*"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "24.0" version = "24.1"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
] ]
[[package]] [[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)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"] 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]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "3.7.0" version = "3.8.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks." description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
{file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
] ]
[package.dependencies] [package.dependencies]
@@ -1099,87 +1129,89 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "pyobjc-core" name = "pyobjc-core"
version = "9.2" version = "10.3.1"
description = "Python<->ObjC Interoperability Module" description = "Python<->ObjC Interoperability Module"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "pyobjc-core-9.2.tar.gz", hash = "sha256:d734b9291fec91ff4e3ae38b9c6839debf02b79c07314476e87da8e90b2c68c3"}, {file = "pyobjc_core-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ea46d2cda17921e417085ac6286d43ae448113158afcf39e0abe484c58fb3d78"},
{file = "pyobjc_core-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fa674a39949f5cde8e5c7bbcd24496446bfc67592b028aedbec7f81dc5fc4daa"}, {file = "pyobjc_core-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:899d3c84d2933d292c808f385dc881a140cf08632907845043a333a9d7c899f9"},
{file = "pyobjc_core-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bbc8de304ee322a1ee530b4d2daca135a49b4a49aa3cedc6b2c26c43885f4842"}, {file = "pyobjc_core-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6ff5823d13d0a534cdc17fa4ad47cf5bee4846ce0fd27fc40012e12b46db571b"},
{file = "pyobjc_core-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0fa950f092673883b8bd28bc18397415cabb457bf410920762109b411789ade9"}, {file = "pyobjc_core-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2581e8e68885bcb0e11ec619e81ef28e08ee3fac4de20d8cc83bc5af5bcf4a90"},
{file = "pyobjc_core-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:586e4cae966282eaa61b21cae66ccdcee9d69c036979def26eebdc08ddebe20f"}, {file = "pyobjc_core-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea98d4c2ec39ca29e62e0327db21418696161fb138ee6278daf2acbedf7ce504"},
{file = "pyobjc_core-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41189c2c680931c0395a55691763c481fc681f454f21bb4f1644f98c24a45954"}, {file = "pyobjc_core-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:4c179c26ee2123d0aabffb9dbc60324b62b6f8614fb2c2328b09386ef59ef6d8"},
{file = "pyobjc_core-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:2d23ee539f2ba5e9f5653d75a13f575c7e36586fc0086792739e69e4c2617eda"}, {file = "pyobjc_core-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cb901fce65c9be420c40d8a6ee6fff5ff27c6945f44fd7191989b982baa66dea"},
{file = "pyobjc_core-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b9809cf96678797acb72a758f34932fe8e2602d5ab7abec15c5ac68ddb481720"}, {file = "pyobjc_core-10.3.1.tar.gz", hash = "sha256:b204a80ccc070f9ab3f8af423a3a25a6fd787e228508d00c4c30f8ac538ba720"},
] ]
[[package]] [[package]]
name = "pyobjc-framework-cocoa" name = "pyobjc-framework-cocoa"
version = "9.2" version = "10.3.1"
description = "Wrappers for the Cocoa frameworks on macOS" description = "Wrappers for the Cocoa frameworks on macOS"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "pyobjc-framework-Cocoa-9.2.tar.gz", hash = "sha256:efd78080872d8c8de6c2b97e0e4eac99d6203a5d1637aa135d071d464eb2db53"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4cb4f8491ab4d9b59f5187e42383f819f7a46306a4fa25b84f126776305291d1"},
{file = "pyobjc_framework_Cocoa-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9e02d8a7cc4eb7685377c50ba4f17345701acf4c05b1e7480d421bff9e2f62a4"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5f31021f4f8fdf873b57a97ee1f3c1620dbe285e0b4eaed73dd0005eb72fd773"},
{file = "pyobjc_framework_Cocoa-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3b1e6287b3149e4c6679cdbccd8e9ef6557a4e492a892e80a77df143f40026d2"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11b4e0bad4bbb44a4edda128612f03cdeab38644bbf174de0c13129715497296"},
{file = "pyobjc_framework_Cocoa-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:312977ce2e3989073c6b324c69ba24283de206fe7acd6dbbbaf3e29238a22537"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:de5e62e5ccf2871a94acf3bf79646b20ea893cc9db78afa8d1fe1b0d0f7cbdb0"},
{file = "pyobjc_framework_Cocoa-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aae7841cf40c26dd915f4dd828f91c6616e6b7998630b72e704750c09e00f334"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c5af24610ab639bd1f521ce4500484b40787f898f691b7a23da3339e6bc8b90"},
{file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:739a421e14382a46cbeb9a883f192dceff368ad28ec34d895c48c0ad34cf2c1d"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a7151186bb7805deea434fae9a4423335e6371d105f29e73cc2036c6779a9dbc"},
{file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:32d9ac1033fac1b821ddee8c68f972a7074ad8c50bec0bea9a719034c1c2fb94"}, {file = "pyobjc_framework_Cocoa-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:743d2a1ac08027fd09eab65814c79002a1d0421d7c0074ffd1217b6560889744"},
{file = "pyobjc_framework_Cocoa-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b236bb965e41aeb2e215d4e98a5a230d4b63252c6d26e00924ea2e69540a59d6"}, {file = "pyobjc_framework_cocoa-10.3.1.tar.gz", hash = "sha256:1cf20714daaa986b488fb62d69713049f635c9d41a60c8da97d835710445281a"},
] ]
[package.dependencies] [package.dependencies]
pyobjc-core = ">=9.2" pyobjc-core = ">=10.3.1"
[[package]] [[package]]
name = "pyobjc-framework-corebluetooth" name = "pyobjc-framework-corebluetooth"
version = "9.2" version = "10.3.1"
description = "Wrappers for the framework CoreBluetooth on macOS" description = "Wrappers for the framework CoreBluetooth on macOS"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "pyobjc-framework-CoreBluetooth-9.2.tar.gz", hash = "sha256:cb2481b1dfe211ae9ce55f36537dc8155dbf0dc8ff26e0bc2e13f7afb0a291d1"}, {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_13_universal2.whl", hash = "sha256:c89ee6fba0ed359c46b4908a7d01f88f133be025bd534cbbf4fb9c183e62fc97"},
{file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:53d888742119d0f0c725d0b0c2389f68e8f21f0cba6d6aec288c53260a0196b6"}, {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2f261a386aa6906f9d4601d35ff71a13315dbca1a0698bf1f1ecfe3971de4648"},
{file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:179532882126526e38fe716a50fb0ee8f440e0b838d290252c515e622b5d0e49"}, {file = "pyobjc_framework_CoreBluetooth-10.3.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5211df0da2e8be511d9a54a48505dd7af0c4d04546fe2027dd723801d633c6ba"},
{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_11_0_universal2.whl", hash = "sha256:b8becd4e406be289a2d423611d3ad40730532a1f6728effb2200e68c9c04c3e8"},
{file = "pyobjc_framework_corebluetooth-10.3.1.tar.gz", hash = "sha256:dc5d326ab5541b8b68e7e920aa8363851e779cb8c33842f6cfeef4674cc62f94"},
] ]
[package.dependencies] [package.dependencies]
pyobjc-core = ">=9.2" pyobjc-core = ">=10.3.1"
pyobjc-framework-Cocoa = ">=9.2" pyobjc-framework-Cocoa = ">=10.3.1"
[[package]] [[package]]
name = "pyobjc-framework-libdispatch" name = "pyobjc-framework-libdispatch"
version = "9.2" version = "10.3.1"
description = "Wrappers for libdispatch on macOS" description = "Wrappers for libdispatch on macOS"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "pyobjc-framework-libdispatch-9.2.tar.gz", hash = "sha256:542e7f7c2b041939db5ed6f3119c1d67d73ec14a996278b92485f8513039c168"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5543aea8acd53fb02bcf962b003a2a9c2bdacf28dc290c31a3d2de7543ef8392"},
{file = "pyobjc_framework_libdispatch-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88d4091d4bcb5702783d6e86b4107db973425a17d1de491543f56bd348909b60"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3e0db3138aae333f0b87b42586bc016430a76638af169aab9cef6afee4e5f887"},
{file = "pyobjc_framework_libdispatch-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a67b007113328538b57893cc7829a722270764cdbeae6d5e1460a1d911314df"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b209dbc9338cd87e053ede4d782b8c445bcc0b9a3d0365a6ffa1f9cd5143c301"},
{file = "pyobjc_framework_libdispatch-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6fccea1a57436cf1ac50d9ebc6e3e725bcf77f829ba6b118e62e6ed7866d359d"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a74e62314376dc2d34bc5d4a86cedaf5795786178ebccd0553c58e8fa73400a3"},
{file = "pyobjc_framework_libdispatch-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6eba747b7ad91b0463265a7aee59235bb051fb97687f35ca2233690369b5e4e4"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8e8fb27ac86d48605eb2107ac408ed8de281751df81f5430fe66c8228d7626b8"},
{file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2e835495860d04f63c2d2f73ae3dd79da4222864c107096dc0f99e8382700026"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0a7a19afef70c98b3b527fb2c9adb025444bcb50f65c8d7b949f1efb51bde577"},
{file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1b107e5c3580b09553030961ea6b17abad4a5132101eab1af3ad2cb36d0f08bb"}, {file = "pyobjc_framework_libdispatch-10.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:109044cddecb3332cbb75f14819cd01b98aacfefe91204c776b491eccc58a112"},
{file = "pyobjc_framework_libdispatch-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:83cdb672acf722717b5ecf004768f215f02ac02d7f7f2a9703da6e921ab02222"}, {file = "pyobjc_framework_libdispatch-10.3.1.tar.gz", hash = "sha256:f5c3475498cb32f54d75e21952670e4a32c8517fb2db2e90869f634edc942446"},
] ]
[package.dependencies] [package.dependencies]
pyobjc-core = ">=9.2" pyobjc-core = ">=10.3.1"
pyobjc-framework-Cocoa = ">=10.3.1"
[[package]] [[package]]
name = "pyright" name = "pyright"
version = "1.1.359" version = "1.1.378"
description = "Command line wrapper for pyright" description = "Command line wrapper for pyright"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pyright-1.1.359-py3-none-any.whl", hash = "sha256:5582777be7eab73512277ac7da7b41e15bc0737f488629cb9babd96e0769be61"}, {file = "pyright-1.1.378-py3-none-any.whl", hash = "sha256:8853776138b01bc284da07ac481235be7cc89d3176b073d2dba73636cb95be79"},
{file = "pyright-1.1.359.tar.gz", hash = "sha256:f0eab50f3dafce8a7302caeafd6a733f39901a2bf5170bb23d77fd607c8a8dbc"}, {file = "pyright-1.1.378.tar.gz", hash = "sha256:78a043be2876d12d0af101d667e92c7734f3ebb9db71dccc2c220e7e7eb89ca2"},
] ]
[package.dependencies] [package.dependencies]
@@ -1189,6 +1221,28 @@ nodeenv = ">=1.6.0"
all = ["twine (>=3.4.1)"] all = ["twine (>=3.4.1)"]
dev = ["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]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.1" version = "6.0.1"
@@ -1270,6 +1324,33 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 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]] [[package]]
name = "setuptools" name = "setuptools"
version = "69.5.1" version = "69.5.1"
@@ -1357,17 +1438,16 @@ test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools
[[package]] [[package]]
name = "sphinx-autoapi" name = "sphinx-autoapi"
version = "3.0.0" version = "3.3.1"
description = "Sphinx API documentation generator" description = "Sphinx API documentation generator"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "sphinx-autoapi-3.0.0.tar.gz", hash = "sha256:09ebd674a32b44467222b0fb8a917b97c89523f20dbf05b52cb8a3f0e15714de"}, {file = "sphinx_autoapi-3.3.1-py2.py3-none-any.whl", hash = "sha256:c31a5f41eabc9705d277b75f98e983d653e9af24e294dd576b2afa1719f72c1f"},
{file = "sphinx_autoapi-3.0.0-py2.py3-none-any.whl", hash = "sha256:ea207793cba1feff7b2ded0e29364f2995a4d157303a98603cee0ce94cea2688"}, {file = "sphinx_autoapi-3.3.1.tar.gz", hash = "sha256:e44a225827d0ef7178748225a66f30c95454dfd00ee3c22afbdfb8056f7dffb5"},
] ]
[package.dependencies] [package.dependencies]
anyascii = "*"
astroid = [ astroid = [
{version = ">=2.7", markers = "python_version < \"3.12\""}, {version = ">=2.7", markers = "python_version < \"3.12\""},
{version = ">=3.0.0a1", markers = "python_version >= \"3.12\""}, {version = ">=3.0.0a1", markers = "python_version >= \"3.12\""},
@@ -1375,6 +1455,7 @@ astroid = [
Jinja2 = "*" Jinja2 = "*"
PyYAML = "*" PyYAML = "*"
sphinx = ">=6.1.0" sphinx = ">=6.1.0"
stdlib-list = {version = "*", markers = "python_version < \"3.10\""}
[package.extras] [package.extras]
docs = ["furo", "sphinx", "sphinx-design"] docs = ["furo", "sphinx", "sphinx-design"]
@@ -1492,18 +1573,36 @@ test = ["pytest"]
[[package]] [[package]]
name = "srp" name = "srp"
version = "1.0.20" version = "1.0.21"
description = "Secure Remote Password" description = "Secure Remote Password"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "srp-1.0.20-py3-none-any.whl", hash = "sha256:ad55b94e26e1152db83b57b50d7b365a7a9b6c39d0d1cd762f0642e478b4bdc0"}, {file = "srp-1.0.21-py3-none-any.whl", hash = "sha256:e49ad6e2b8b1189c5879874664d33e4e1e403598c3e0903541a1bde03f7becae"},
{file = "srp-1.0.20.tar.gz", hash = "sha256:2db453bdce26b9eead367a7b5783074ef80e8482bf30c0140a7b89836a054707"}, {file = "srp-1.0.21.tar.gz", hash = "sha256:866813bcf521189a1563e6ca3112b6f54fdf725a410a2dbebb6f0d84b82a1f1d"},
] ]
[package.dependencies] [package.dependencies]
six = "*" 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]] [[package]]
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
@@ -1517,13 +1616,13 @@ files = [
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.11.0" version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+" description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
] ]
[[package]] [[package]]
@@ -1565,221 +1664,245 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[[package]] [[package]]
name = "winrt-runtime" name = "winrt-runtime"
version = "2.0.0b1" version = "2.2.0"
description = "Python projection of Windows Runtime (WinRT) APIs" description = "Python projection of Windows Runtime (WinRT) APIs"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.14,>=3.9"
files = [ files = [
{file = "winrt-runtime-2.0.0b1.tar.gz", hash = "sha256:28db2ebe7bfb347d110224e9f23fe8079cea45af0fcbd643d039524ced07d22c"}, {file = "winrt_runtime-2.2.0-cp310-cp310-win32.whl", hash = "sha256:ab034330d6b64ce93683bdc14d4f3f83dfafbf1f72b45893505f7d684e5e7fe1"},
{file = "winrt_runtime-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:8f812b01e2c8dd3ca68aa51a7aa02e815cc2ac3c8520a883b4ec7a4fc63afb04"}, {file = "winrt_runtime-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad9927a1838dea47ceb2d773c0269242bcee7cb5379ed801547788ab435da502"},
{file = "winrt_runtime-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:f36f6102f9b7a08d917a6809117c085639b66be2c579f4089d3fd47b83e8f87b"}, {file = "winrt_runtime-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:87745ae54d054957a99c70875c1ac3c89cca258ed06836ae308fbbb7dda4ef61"},
{file = "winrt_runtime-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:4a99f267da96edc977623355b816b46c1344c66dc34732857084417d8cf9a96b"}, {file = "winrt_runtime-2.2.0-cp311-cp311-win32.whl", hash = "sha256:7ee2397934c1c4a090f9d889292def90b8f673dc1d320f1f07931ad1cb6e49bf"},
{file = "winrt_runtime-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:ba998e3fc452338c5e2d7bf5174a6206580245066d60079ee4130082d0eb61c2"}, {file = "winrt_runtime-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f110b0f451b514cf09c4fa0e73bab54d4b598c3092df9dd87940403998e81f30"},
{file = "winrt_runtime-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:e7838f0fdf5653ce245888590214177a1f54884cece2c8dfbfe3d01b2780171e"}, {file = "winrt_runtime-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:27606e7a393a26e484f03db699c4d7c206d180a3736a6cd68fba3b3896e364a4"},
{file = "winrt_runtime-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:2afa45b7385e99a63d55ccda29096e6a84fcd4c654479005c147b0e65e274abf"}, {file = "winrt_runtime-2.2.0-cp312-cp312-win32.whl", hash = "sha256:5a769bfb4e264b7fd306027da90c6e4e615667e9afdd8e5d712bc45bdabaf0d2"},
{file = "winrt_runtime-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:edda124ff965cec3a6bfdb26fbe88e004f96975dd84115176e30c1efbcb16f4c"}, {file = "winrt_runtime-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ef30ea7446a1e37660265b76e586fcffc0e83a859b7729141cdf68cbedf808a8"},
{file = "winrt_runtime-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:d8935951efeec6b3d546dce8f48bb203aface57a1ba991c066f0e12e84c8f91e"}, {file = "winrt_runtime-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:d8f6338fb8433b4df900c8f173959a5ae9ac63b0b20faddb338e76a6e9391bc9"},
{file = "winrt_runtime-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:509fb9a03af5e1125433f58522725716ceef040050d33625460b5a5eb98a46ac"}, {file = "winrt_runtime-2.2.0-cp313-cp313-win32.whl", hash = "sha256:6d8c1122158edc96cac956a5ab62bc06a56e088bdf83d0993a455216b3fd1cac"},
{file = "winrt_runtime-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:41138fe4642345d7143e817ce0905d82e60b3832558143e0a17bfea8654c6512"}, {file = "winrt_runtime-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:76b2dc846e6802375113c9ce9e7fcc4292926bd788445f34d404bae72d2b4f4b"},
{file = "winrt_runtime-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:081a429fe85c33cb6610c4a799184b7650b30f15ab1d89866f2bda246d3a5c0a"}, {file = "winrt_runtime-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:faacc05577573702cb135e7da4d619f4990c768063dc869362f13d856a0738e3"},
{file = "winrt_runtime-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:e6984604c6ae1f3258973ba2503d1ea5aa15e536ca41d6a131ad305ebbb6519d"}, {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]] [[package]]
name = "winrt-windows-devices-bluetooth" name = "winrt-windows-devices-bluetooth"
version = "2.0.0b1" version = "2.2.0"
description = "Python projection of Windows Runtime (WinRT) APIs" description = "Python projection of Windows Runtime (WinRT) APIs"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.14,>=3.9"
files = [ files = [
{file = "winrt-Windows.Devices.Bluetooth-2.0.0b1.tar.gz", hash = "sha256:786bd43786b873a083b89debece538974f720584662a2573d6a8a8501a532860"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win32.whl", hash = "sha256:f3ced50ded44f74ac901d05f99cdd0bdf78e3a939a42d3cd80c33e510b4b8569"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:79631bf3f96954da260859df9228a028835ffade0d885ba3942c5a86a853d150"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:241a8f0ab06f6178d2e5757e7bc1f6c37e00e65ab6858ae676a1723a6445fa92"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:cd85337a95065d0d2045c06db1a5edd4a447aad47cf7027818f6fb69f831c56c"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3abefa3d11b4af9d9731d9d1a71083b1ef301fa30f7006a6c1f341426dd6d733"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:6a963869ed003d260e90e9bedc334129303f263f068ea1c0d994df53317db2bc"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4215c45595201f5f43f98b1e8911ff5cb0b303fe3298fa4d91a7bdc6d5523853"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:7c5951943a3911d94a8da190f4355dc70128d7d7f696209316372c834b34d462"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cda69842b30bf56b10ea1a747d01b295abc910d9ccc10e9c97e8f554cd536e0"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:b0bb154ae92235649ed234982f609c490a467d5049c27d63397be9abbb00730e"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7c12a28cd04eb05bacc73d8025ba135a929b9d511d21f20d0072d735853e8a2"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:6688dfb0fc3b7dc517bf8cf40ae00544a50b4dec91470d37be38fc33c4523632"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win32.whl", hash = "sha256:c929ea5215942fb26081b26aae094a2f70551cc0a59499ab2c9ea1f6d6b991f9"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:613c6ff4125df46189b3bef6d3110d94ec725d357ab734f00eedb11c4116c367"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1444e2031f3e69990d412b9edf75413a09280744bbc088a6b0760d94d356d4b"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:59c403b64e9f4e417599c6f6aea6ee6fac960597c21eac6b3fd8a84f64aa387c"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f2d06ce6c43e37ea09ac073805ac6f9f62ae10ce552c90ae6eca978accd3f434"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:b7f6e1b9bb6e33be80045adebd252cf25cd648759fad6e86c61a393ddd709f7f"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win32.whl", hash = "sha256:b44a45c60f1d9fa288a12119991060ef7998793c6b93baa84308cfb090492788"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:eae7a89106eab047e96843e28c3c6ce0886dd7dee60180a1010498925e9503f9"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:fb698a55d06dc34643437b370c35fa064bd28762561e880715a30463c359fa44"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:8dfd1915c894ac19dd0b24aba38ef676c92c3473c0d9826762ba9616ad7df68b"}, {file = "winrt_Windows.Devices.Bluetooth-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:cb350bfe21bab3573c9cd84006efad9c46a395a2943ab474105aed8b21bb88a4"},
{file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:49058587e6d82ba33da0767b97a378ddfea8e3a5991bdeff680faa287bfae57e"}, {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] [package.dependencies]
winrt-runtime = "2.0.0-beta.1" winrt-runtime = "2.2.0"
[package.extras] [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]] [[package]]
name = "winrt-windows-devices-bluetooth-advertisement" name = "winrt-windows-devices-bluetooth-advertisement"
version = "2.0.0b1" version = "2.2.0"
description = "Python projection of Windows Runtime (WinRT) APIs" description = "Python projection of Windows Runtime (WinRT) APIs"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.14,>=3.9"
files = [ files = [
{file = "winrt-Windows.Devices.Bluetooth.Advertisement-2.0.0b1.tar.gz", hash = "sha256:d9050faa4377d410d4f0e9cabb5ec555a267531c9747370555ac9ec93ec9f399"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win32.whl", hash = "sha256:3d5fddffd5f6eeafebe1bcbaa096b8962c28c9236490f6f887ac2ed3ee4ed62c"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:ac9b703d16adc87c3541585525b8fcf6d84391e2fa010c2f001e714c405cc3b7"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1cb5a835dc3574b0c47a613fa49eeeccdd9aa5801d43d7b7606ad5ce3614a54"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:593cade7853a8b0770e8ef30462b5d5f477b82e17e0aa590094b1c26efd3e05a"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:9c2530c4972671ffb8a6e54621490c6c7a8c13b4d57e6474e05b62f211bbaab6"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:574698c08895e2cfee7379bdf34a5f319fe440d7dfcc7bc9858f457c08e9712c"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win32.whl", hash = "sha256:28b36b3be137bdb6bdaad0d7a620c1a8b156e3c2737d08b9827af02b3c9d52bf"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:652a096f8210036bbb539d7f971eaf1f472a3aeb60b7e31278e3d0d30a355292"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:52948f17ecfc70c58b07077191985712172b518b5e3f4874e5708d175b7ace72"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:e5cfb866c44dad644fb44b441f4fdbddafc9564075f1f68f756e20f438105c67"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:338296b76c01840c1dc10799a405b76460346bf677af11e6ab324311fd58e1a9"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:6c2503eaaf5cd988b5510b86347dba45ad6ee52656f9656a1a97abae6d35386e"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win32.whl", hash = "sha256:4c14f48ac1886a3d374ee511467f0a61f26d88a321bf97d47429859730ee9248"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:780c766725a55f4211f921c773c92c2331803e70f65d6ad6676a60f903d39a54"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:89a658e901de88373e6a17a98273b8555e3f80563f2cc362b7f75817a7f9d915"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:39c8633d01039eb2c2f6f20cfc43c045a333b9f3a45229e2ce443f71bb2a562c"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3b2b1b34f37a3329cf72793a089dd13fefd7b582c3e3a53a69a1353fd18940a3"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:eaa0d44b4158b16937eac8102249e792f0299dbb0aefc56cc9adc9552e8f9afe"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win32.whl", hash = "sha256:1b2d42c3d90b3e985954196b9a9e4007e22ff468d3d020c5a4acdee2821018fe"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:d171487e23f7671ad2923544bfa6545d0a29a1a9ae1f5c1d5e5e5f473a5d62b2"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d964c599670ea21b97afe2435e7638ca26e04936aacc0550474b6ec3fea988f"},
{file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:442eecac87653a03617e65bdb2ef79ddc0582dfdacc2be8af841fba541577f8b"}, {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:add4f459f0a02d1da38d579c3af887cfc3fe54f7782d779cf4ffe7f24404f1ff"},
{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-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] [package.dependencies]
winrt-runtime = "2.0.0-beta.1" winrt-runtime = "2.2.0"
[package.extras] [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]] [[package]]
name = "winrt-windows-devices-bluetooth-genericattributeprofile" name = "winrt-windows-devices-bluetooth-genericattributeprofile"
version = "2.0.0b1" version = "2.2.0"
description = "Python projection of Windows Runtime (WinRT) APIs" description = "Python projection of Windows Runtime (WinRT) APIs"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.14,>=3.9"
files = [ files = [
{file = "winrt-Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1.tar.gz", hash = "sha256:93b745d51ecfb3e9d3a21623165cc065735c9e0146cb7a26744182c164e63e14"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win32.whl", hash = "sha256:1472f89b9d6527137e1c58dfb46f22faf2753c477a9d4f85f789b3266ad282a9"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:db740aaedd80cca5b1a390663b26c7733eb08f4c57ade6a04b055d548e9d042b"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:e25702f1aa6d4ecdf335805a50048e70ee2206499cfd7ed4fbe1a92358bdcc16"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:7c81aa6c066cdab58bcc539731f208960e094a6d48b59118898e1e804dbbdf7f"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:d07d27a6f8f7a1f52aa978724d5a09d43053b428c71563892b70df409049a37a"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:92277a6bbcbe2225ad1be92968af597dc77bc37a63cd729690d2d9fb5094ae25"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win32.whl", hash = "sha256:5c6c863daaa99b0bb670730296137b7c718d94726c112ff44ec73c8b27a12ded"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:6b48209669c1e214165530793cf9916ae44a0ae2618a9be7a489e8c94f7e745f"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbee7c90c0a155477eba09eb09297711b2cb32f6ede4c01d0afe58cb3776f06a"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:2f17216e6ce748eaef02fb0658213515d3ff31e2dbb18f070a614876f818c90d"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:655777193fd338e1a8c30ebbb8460c017d08548c54ddec9fc5503f1605c47332"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:db798a0f0762e390da5a9f02f822daff00692bd951a492224bf46782713b2938"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win32.whl", hash = "sha256:45a48ab8da94eee1590f22826c084f4b1f8c32107a023f05d6a03437931a6852"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:b8d9dba04b9cfa53971c35117fc3c68c94bfa5e2ed18ce680f731743598bf246"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:395cb2fecd0835a402c3c4f274395bc689549b2a6b4155d3ad97b29ec87ee4f2"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:e5260b3f33dee8a896604297e05efc04d04298329c205a74ded8e2d6333e84b7"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:25063b43550c5630f188cfb263ab09acc920db97d1625c48e24baa6e7d445b6e"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:822ef539389ecb546004345c4dce8b9b7788e2e99a1d6f0947a4b123dceb7fed"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win32.whl", hash = "sha256:d1d26512fe45c3be0dbeb932dbd75abd580cd46ccfc278fcf51042eff302fa9c"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:11e6863e7a94d2b6dd76ddcd19c01e311895810a4ce6ad08c7b5534294753243"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:21786840502a34958dd5fb137381f9144a6437b49ee90a877beb3148ead6cfe9"},
{file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:20de8d04c301c406362c93e78d41912aea0af23c4b430704aba329420d7c2cdf"}, {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d98852458b639e875bb4895a9ad2d5626059bc99c5f745be0560d235502d648"},
{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-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] [package.dependencies]
winrt-runtime = "2.0.0-beta.1" winrt-runtime = "2.2.0"
[package.extras] [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]] [[package]]
name = "winrt-windows-devices-enumeration" name = "winrt-windows-devices-enumeration"
version = "2.0.0b1" version = "2.2.0"
description = "Python projection of Windows Runtime (WinRT) APIs" description = "Python projection of Windows Runtime (WinRT) APIs"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.14,>=3.9"
files = [ files = [
{file = "winrt-Windows.Devices.Enumeration-2.0.0b1.tar.gz", hash = "sha256:8f214040e4edbe57c4943488887db89f4a00d028c34169aafd2205e228026100"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win32.whl", hash = "sha256:69e87ba0ae5c31f60bc07d0558d91af96213d8b8b2b1be0ccf3e5824cab466ef"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:dcb9e7d230aefec8531a46d393ecb1063b9d4b97c9f3ff2fc537ce22bdfa2444"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6993d5305ff750c5c51f57253935458996fb45c049891f2fb00772cc6ece6b3"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:22a3e1fef40786cc8d51320b6f11ff25de6c674475f3ba608a46915e1dadf0f5"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bb54aa94b17052d65fe4fa5777183cf9bfb697574c3461759114d3ec0c802cec"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:2edcfeb70a71d40622873cad96982a28e92a7ee71f33968212dd3598b2d8d469"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win32.whl", hash = "sha256:fef83263e73c2611d223f06735d2c2a16629d723f74e1964dc882f90b6e1cda1"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:ce4eb88add7f5946d2666761a97a3bb04cac2a061d264f03229c1e15dbd7ce91"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:cf3cec5a6fba069ecbd4f3efa95e9f197aeebdd05a60bcd52b953888169ab7ee"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:a9001f17991572abdddab7ab074e08046e74e05eeeaf3b2b01b8b47d2879b64c"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:d9ce308c492c1e9f2417f91ad02e366f4269cc1c6d271f0be4092b758df4c9bf"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:0440b91ce144111e207f084cec6b1277162ef2df452d321951e989ce87dc9ced"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win32.whl", hash = "sha256:5bea21988749fad21574ea789b4090cfbfbb982a5f9a42b2d6f05b3ad47f68bd"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:e4fae13126f13a8d9420b74fb5a5ff6a6b2f91f7718c4be2d4a8dc1337c58f59"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:c9718d7033550a029e0c2848ff620bf063a519cb22ab9d880d64ceb302763a48"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:e352eebc23dc94fb79e67a056c057fb0e16c20c8cb881dc826094c20ed4791e3"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:69f67f01aa519304e4af04a1a23261bd8b57136395de2e08d56968f9c6daa18e"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:b43f5c1f053a170e6e4b44ba69838ac223f9051adca1a56506d4c46e98d1485f"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win32.whl", hash = "sha256:84447916282773d7b7e5a445eae0ab273c21105f1bbcdfb7d8e21cd41403d5c1"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:ed245fad8de6a134d5c3a630204e7f8238aa944a40388005bce0ce3718c410fa"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:1bb9d97f8d2518bb5b331f825431814277de4341811a1776e79d51767e79700c"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:22a9eefdbfe520778512266d0b48ff239eaa8d272fce6f5cb1ff352bed0619f4"}, {file = "winrt_Windows.Devices.Enumeration-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:2a5408423f680f6b36d7accad7151336ea16ad1eaa2652f60ed88e2cbd14562c"},
{file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:397d43f8fd2621a7719b9eab6a4a8e72a1d6fa2d9c36525a30812f8e7bad3bdf"}, {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] [package.dependencies]
winrt-runtime = "2.0.0-beta.1" winrt-runtime = "2.2.0"
[package.extras] [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]] [[package]]
name = "winrt-windows-foundation" name = "winrt-windows-foundation"
version = "2.0.0b1" version = "2.2.0"
description = "Python projection of Windows Runtime (WinRT) APIs" description = "Python projection of Windows Runtime (WinRT) APIs"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.14,>=3.9"
files = [ files = [
{file = "winrt-Windows.Foundation-2.0.0b1.tar.gz", hash = "sha256:976b6da942747a7ca5a179a35729d8dc163f833e03b085cf940332a5e9070d54"}, {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win32.whl", hash = "sha256:cb86bbf04f72d983e4ae13db0a48784638b36214bb2c44809f39686ef3314354"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:5337ac1ec260132fbff868603e73a3738d4001911226e72669b3d69c8a256d5e"}, {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:2dbd0957216c07db4b91a144a0ffa7c8892cc668b19ca15b78067255445741b2"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:af969e5bb9e2e41e4e86a361802528eafb5eb8fe87ec1dba6048c0702d63caa8"}, {file = "winrt_Windows.Foundation-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:5345f7d0504aa1a605be5b5fe0d1944b322591f7669c2c86b7c45384924c8c9b"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:bbbfa6b3c444a1074a630fd4a1b71171be7a5c9bb07c827ad9259fadaed56cf2"}, {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win32.whl", hash = "sha256:f6711adf8a34e48c94183e792f153de5f3796f8f3c045356544605384bbcb7e1"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:b91bd92b1854c073acd81aa87cf8df571d2151b1dd050b6181aa36f7acc43df4"}, {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:0a5bfe2647659e7ec288d8552e61e577a931914531ccc9cb958469d85f049d6b"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:2f5359f25703347e827dbac982150354069030f1deecd616f7ce37ad90cbcb00"}, {file = "winrt_Windows.Foundation-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9eabbd1b179fd04f167884fa0feaa17ccd67d89f6eac4099b16c6c0dc22e9f32"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:0f1f1978173ddf0ee6262c2edb458f62d628b9fa0df10cd1e8c78c833af3197e"}, {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win32.whl", hash = "sha256:0f0319659f00d04d13fc5db45f574479a396147c955628dc2dda056397a0df28"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:c1d23b737f733104b91c89c507b58d0b3ef5f3234a1b608ef6dfb6dbbb8777ea"}, {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:8bc605242d268cd8ccce68c78ec4a967b8e5431c3a969c9e7a01d454696dfb3f"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:95de6c29e9083fe63f127b965b54dfa52a6424a93a94ce87cfad4c1900a6e887"}, {file = "winrt_Windows.Foundation-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f901b20c3a874a2cf9dcb1e97bbcff329d95fd3859a873be314a5a58073b4690"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:4707063a5a6980e3f71aebeea5ac93101c753ec13a0b47be9ea4dbc0d5ff361e"}, {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win32.whl", hash = "sha256:c5cf43bb1dccf3a302d16572d53f26479d277e02606531782c364056c2323678"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:d0259f1f4a1b8e20d0cbd935a889c0f7234f720645590260f9cf3850fdc1e1fa"}, {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:10c84276ff182a06da6deb1ba9ad375f9b3fbc15c3684a160e775005d915197a"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:15c7b324d0f59839fb4492d84bb1c870881c5c67cb94ac24c664a7c4dce1c475"}, {file = "winrt_Windows.Foundation-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:473cc57664bfd5401ec171c8f55079cdc8a980210f2c82fb2945361ea640bfbf"},
{file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:16ad741f4d38e99f8409ba5760299d0052003255f970f49f4b8ba2e0b609c8b7"}, {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] [package.dependencies]
winrt-runtime = "2.0.0-beta.1" winrt-runtime = "2.2.0"
[package.extras] [package.extras]
all = ["winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)"] all = ["winrt-Windows.Foundation.Collections[all] (==2.2.0)"]
[[package]] [[package]]
name = "winrt-windows-foundation-collections" name = "winrt-windows-foundation-collections"
version = "2.0.0b1" version = "2.2.0"
description = "Python projection of Windows Runtime (WinRT) APIs" description = "Python projection of Windows Runtime (WinRT) APIs"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.14,>=3.9"
files = [ files = [
{file = "winrt-Windows.Foundation.Collections-2.0.0b1.tar.gz", hash = "sha256:185d30f8103934124544a40aac005fa5918a9a7cb3179f45e9863bb86e22ad43"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win32.whl", hash = "sha256:92a031fca53910c8bce683391888ba3427db178fc47653310de16fb7e9131e9d"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:042142e916a170778b7154498aae61254a1a94c552954266b73479479d24f01d"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:a71925d738a443cf27522f34ced84730f1b325f69ccdd0145580e6078d4481c5"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:9f68e66055121fc1e04c4fda627834aceee6fbe922e77d6ccaecf9582e714c57"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:74c9419b26b510e6e95182e02dc55a78094b6f2af5002330467d030ae6d0b765"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:a4609411263cc7f5e93a9a5677b21e2ef130e26f9030bfa960b3e82595324298"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win32.whl", hash = "sha256:8a76d79be0af1840b9c5ac1879dcf5aa65b512accd8278ac6424dcbfdb2a6fe1"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:5296858aa44c53936460a119794b80eedd6bd094016c1bf96822f92cb95ea419"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:b18dcd7bc8cf70758b965397e26da725ac345dd9f16b922b0204e8f21ed4d7e6"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:3db1e1c80c97474e7c88b6052bd8982ca61723fd58ace11dc91a5522662e0b2a"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d6b0b04683e98989dd611940b5fe36c1338f6d91f43c1bdc88f2f2f1956a968"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:c3a594e660c59f9fab04ae2f40bda7c809e8ec4748bada4424dfb02b43d4bfe1"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win32.whl", hash = "sha256:ade4ea4584ba96e39d2b34f1036d8cb40ff2e9609a090562cfd2b8837dc7f828"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:0f355ee943ec5b835e694d97e9e93545a42d6fb984a61f442467789550d62c3f"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:1e896291c5efe0566db84eab13888bee7300392a6811ae85c55ced51bac0b147"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:c4a0cd2eb9f47c7ca3b66d12341cc822250bf26854a93fd58ab77f7a48dfab3a"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:e44e13027597fcc638073459dcc159a21c57f9dbe0e9a2282326e32386c25bd0"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:744dbef50e8b8f34904083cae9ad43ac6e28facb9e166c4f123ce8e758141067"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win32.whl", hash = "sha256:ea7fa3a7ecb754eb09408e7127cd960d316cc1ba60a6440e191a81f14b42265c"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:b7c767184aec3a3d7cba2cd84fadcd68106854efabef1a61092052294d6d6f4f"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:f338860e27a8a67b386273c73ad10c680a9f40a42e0185cc6443d208a7425ece"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:7c1ffe99c12f14fc4ab7027757780e6d850fa2fb23ec404a54311fbd9f1970d3"}, {file = "winrt_Windows.Foundation.Collections-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:dd705d4c62bd8c109f2bc667a0c76dc30ef9a1b2ced3e7bd95253a31e39781df"},
{file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:870fa040ed36066e4c240c35973d8b2e0d7c38cc6050a42d993715ec9e3b748c"}, {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] [package.dependencies]
winrt-runtime = "2.0.0-beta.1" winrt-runtime = "2.2.0"
[package.extras] [package.extras]
all = ["winrt-Windows.Foundation[all] (==2.0.0-beta.1)"] all = ["winrt-Windows.Foundation[all] (==2.2.0)"]
[[package]] [[package]]
name = "winrt-windows-storage-streams" name = "winrt-windows-storage-streams"
version = "2.0.0b1" version = "2.2.0"
description = "Python projection of Windows Runtime (WinRT) APIs" description = "Python projection of Windows Runtime (WinRT) APIs"
optional = false optional = false
python-versions = "<3.13,>=3.9" python-versions = "<3.14,>=3.9"
files = [ files = [
{file = "winrt-Windows.Storage.Streams-2.0.0b1.tar.gz", hash = "sha256:029d67cdc9b092d56c682740fe3c42f267dc5d3346b5c0b12ebc03f38e7d2f1f"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win32.whl", hash = "sha256:e888ae08f1245f8b6d53783487581fc664683bb29778f2acca6bafb6a78bcc22"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:49c90d4bfd539f6676226dfcb4b3574ddd6be528ffc44aa214c55af88c2de89e"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9213576d566398657142372aa34354b9f7b8ce0581cff308c7afbc0d908368a1"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:22cc82779cada84aa2633841e25b33f3357737d912a1d9ecc1ee5a8b799b5171"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:49d2bdd749994fb81c813f02f3c506fff580f358083b65a123308f322c2fe6cf"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:b1750a111be32466f4f0781cbb5df195ac940690571dff4564492b921b162563"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win32.whl", hash = "sha256:db4ebe7ed79a585a1bb78a3f8cea05f7d74a6a8bc913f61b31ddfe3ae10d134d"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:e79b1183ab26d9b95cf3e6dbe3f488a40605174a5a112694dbb7dbfb50899daf"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9f77c5398eb90c58645c62b6f278f701d2636c0007817cc6fc28256adbebdcb"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:3e90a1207eb3076f051a7785132f7b056b37343a68e9481a50c6defb3f660099"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:894c2616eeae887275a1a64a4233964f9466ee1281b8c11ec7c06d64aafec88a"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:4da06522b4fa9cfcc046b604cc4aa1c6a887cc4bb5b8a637ed9bff8028a860bb"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win32.whl", hash = "sha256:85a2eefb2935db92d10b8e9be836c431d47298b566b55da633b11f822c63838d"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:6f74f8ab8ac0d8de61c709043315361d8ac63f8144f3098d428472baadf8246a"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f88cdc6204219c7f1b58d793826ea2eff013a45306fbb340d61c10896c237547"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:5cf7c8d67836c60392d167bfe4f98ac7abcb691bfba2d19e322d0f9181f58347"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:78af200d0db5ebe151b1df194de97f1e71c2d5f5cba4da09798c15402f4ab91d"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:f7f679f2c0f71791eca835856f57942ee5245094c1840a6c34bc7c2176b1bcd6"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win32.whl", hash = "sha256:6408184ba5d17e0d408d7c0b85357a58f13c775521d17a8730f1a680553e0061"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:5beb53429fa9a11ede56b4a7cefe28c774b352dd355f7951f2a4dd7e9ec9b39a"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:ad9cd8e97cf4115ba074ec153ab273c370e690abb010d8b3b970339d20f94321"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:f84233c4b500279d8f5840cb8c47776bc040fcecba05c6c9ab9767053698fc8b"}, {file = "winrt_Windows.Storage.Streams-2.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c467cf04005b72efd769ea99c7c15973db44d5ac6084a7c7714af85e49981abd"},
{file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:cfb163ddbb435906f75ef92a768573b0190e194e1438cea5a4c1d4d32a6b9386"}, {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] [package.dependencies]
winrt-runtime = "2.0.0-beta.1" winrt-runtime = "2.2.0"
[package.extras] [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]] [[package]]
name = "yarl" 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"] 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)"] 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] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.9,<3.13" python-versions = ">=3.9,<3.13"
content-hash = "828fc3307e8314148461691a7ef95572699b2e9597713a118c469a5532c65d61" content-hash = "91a68ea081419a03ce35f7be2401ca292fe077b35bbd38f901a5cb0ead58cbd6"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "FindMy" name = "FindMy"
version = "0.5.0" version = "v0.7.3"
description = "Everything you need to work with Apple's Find My network!" description = "Everything you need to work with Apple's Find My network!"
authors = ["Mike Almeloo <git@mikealmel.ooo>"] authors = ["Mike Almeloo <git@mikealmel.ooo>"]
readme = "README.md" readme = "README.md"
@@ -8,20 +8,35 @@ packages = [{ include = "findmy" }]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.9,<3.13" python = ">=3.9,<3.13"
srp = "^1.0.20" srp = "^1.0.21"
cryptography = "^42.0.5" cryptography = ">=42.0.0,<44.0.0"
beautifulsoup4 = "^4.12.2" beautifulsoup4 = "^4.12.3"
aiohttp = "^3.9.1" aiohttp = "^3.9.5"
bleak = "^0.21.1" bleak = "^0.22.2"
typing-extensions = "^4.12.2"
[tool.poetry.extras] [tool.poetry.group.dev]
scan = ["bleak"] optional = true
[tool.poetry.group.dev.dependencies] [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 = "^7.2.6"
sphinx-autoapi = "^3.0.0" sphinx-autoapi = "3.3.1"
pyright = "^1.1.350"
furo = "^2024.1.29" furo = "^2024.1.29"
myst-parser = "^2.0.0" myst-parser = "^2.0.0"
@@ -33,11 +48,20 @@ venv = ".venv"
typeCheckingMode = "standard" typeCheckingMode = "standard"
reportImplicitOverride = true reportImplicitOverride = true
[tool.ruff] # examples should be run from their own directory
exclude = [ executionEnvironments = [
"docs/", { root = "examples/" }
] ]
[tool.ruff]
line-length = 100
exclude = [
"docs/",
"tests/"
]
[tool.ruff.lint]
select = [ select = [
"ALL", "ALL",
] ]
@@ -50,12 +74,13 @@ ignore = [
"D212", # multi-line docstring start at first line "D212", # multi-line docstring start at first line
"D105", # docstrings in magic methods "D105", # docstrings in magic methods
"S101", # assert statements
"S603", # false-positive subprocess call (https://github.com/astral-sh/ruff/issues/4045)
"PLR2004", # "magic" values >.> "PLR2004", # "magic" values >.>
"FBT", # boolean "traps" "FBT", # boolean "traps"
] ]
line-length = 100
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"examples/*" = [ "examples/*" = [
"T201", # use of "print" "T201", # use of "print"
@@ -63,6 +88,10 @@ line-length = 100
"D", # documentation "D", # documentation
"INP001", # namespacing "INP001", # namespacing
] ]
"scripts/*" = [
"T201", # use of "print"
"D", # documentation
]
[build-system] [build-system]
requires = ["poetry-core"] 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