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