mirror of
https://github.com/malmeloo/FindMy.py.git
synced 2026-04-17 23:53:57 +02:00
Merge branch 'refs/heads/main' into feat/better-docs
# Conflicts: # poetry.lock
This commit is contained in:
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: [malmeloo]
|
||||
7
.github/renovate.json
vendored
Normal file
7
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
"schedule:monthly"
|
||||
]
|
||||
}
|
||||
3
.github/workflows/docs.yml
vendored
3
.github/workflows/docs.yml
vendored
@@ -9,7 +9,6 @@ on:
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
|
||||
permissions:
|
||||
pages: write
|
||||
@@ -35,7 +34,7 @@ jobs:
|
||||
poetry run make html
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
9
.github/workflows/publish.yml
vendored
9
.github/workflows/publish.yml
vendored
@@ -1,5 +1,7 @@
|
||||
name: Upload Python Package
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -10,7 +12,6 @@ on:
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -32,3 +33,9 @@ jobs:
|
||||
run: |
|
||||
poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }}
|
||||
poetry publish
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
files: dist/*
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -161,3 +161,5 @@ cython_debug/
|
||||
.idea/
|
||||
|
||||
account.json
|
||||
airtag.plist
|
||||
DO_NOT_COMMIT*
|
||||
|
||||
12
README.md
12
README.md
@@ -27,6 +27,7 @@ application wishing to integrate with the Find My network.
|
||||
- [x] Fetch location reports
|
||||
- [x] Apple acount sign-in
|
||||
- [x] SMS 2FA support
|
||||
- [x] Trusted Device 2FA support
|
||||
- [x] Scan for nearby FindMy-devices
|
||||
- [x] Decode their info, such as public keys and status bytes
|
||||
- [x] Import or create your own accessory keys
|
||||
@@ -34,8 +35,6 @@ application wishing to integrate with the Find My network.
|
||||
|
||||
### Roadmap
|
||||
|
||||
- [ ] Trusted device 2FA
|
||||
- Work has been done, but needs testing (I don't own any Apple devices)
|
||||
- [ ] 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.
|
||||
@@ -48,7 +47,7 @@ The package can be installed from [PyPi](https://pypi.org/project/findmy/):
|
||||
pip install findmy
|
||||
```
|
||||
|
||||
For usage examples, see the [examples](examples) directory. Documentation coming soon™.
|
||||
For usage examples, see the [examples](examples) directory. Documentation can be found [here](http://docs.mikealmel.ooo/FindMy.py/).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -68,6 +67,13 @@ pre-commit install
|
||||
After following the above steps, your code will be linted and formatted automatically
|
||||
before committing it.
|
||||
|
||||
## Derivative projects
|
||||
|
||||
There are several other cool projects based on this library! Some of them have been listed below, make sure to check them out as well.
|
||||
|
||||
* [OfflineFindRecovery](https://github.com/hajekj/OfflineFindRecovery) - Set of scripts to be able to precisely locate your lost MacBook via Apple's Offline Find through Bluetooth Low Energy.
|
||||
* [SwiftFindMy](https://github.com/airy10/SwiftFindMy) - Swift port of FindMy.py
|
||||
|
||||
## Credits
|
||||
|
||||
While I designed the library, the vast majority of actual functionality
|
||||
|
||||
101
examples/_login.py
Normal file
101
examples/_login.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from findmy.reports import (
|
||||
AppleAccount,
|
||||
AsyncAppleAccount,
|
||||
BaseAnisetteProvider,
|
||||
LoginState,
|
||||
SmsSecondFactorMethod,
|
||||
TrustedDeviceSecondFactorMethod,
|
||||
)
|
||||
|
||||
ACCOUNT_STORE = "account.json"
|
||||
|
||||
|
||||
def _login_sync(account: AppleAccount) -> None:
|
||||
email = input("email? > ")
|
||||
password = input("passwd? > ")
|
||||
|
||||
state = account.login(email, password)
|
||||
|
||||
if state == LoginState.REQUIRE_2FA: # Account requires 2FA
|
||||
# This only supports SMS methods for now
|
||||
methods = account.get_2fa_methods()
|
||||
|
||||
# Print the (masked) phone numbers
|
||||
for i, method in enumerate(methods):
|
||||
if isinstance(method, TrustedDeviceSecondFactorMethod):
|
||||
print(f"{i} - Trusted Device")
|
||||
elif isinstance(method, SmsSecondFactorMethod):
|
||||
print(f"{i} - SMS ({method.phone_number})")
|
||||
|
||||
ind = int(input("Method? > "))
|
||||
|
||||
method = methods[ind]
|
||||
method.request()
|
||||
code = input("Code? > ")
|
||||
|
||||
# This automatically finishes the post-2FA login flow
|
||||
method.submit(code)
|
||||
|
||||
|
||||
async def _login_async(account: AsyncAppleAccount) -> None:
|
||||
email = input("email? > ")
|
||||
password = input("passwd? > ")
|
||||
|
||||
state = await account.login(email, password)
|
||||
|
||||
if state == LoginState.REQUIRE_2FA: # Account requires 2FA
|
||||
# This only supports SMS methods for now
|
||||
methods = await account.get_2fa_methods()
|
||||
|
||||
# Print the (masked) phone numbers
|
||||
for i, method in enumerate(methods):
|
||||
if isinstance(method, TrustedDeviceSecondFactorMethod):
|
||||
print(f"{i} - Trusted Device")
|
||||
elif isinstance(method, SmsSecondFactorMethod):
|
||||
print(f"{i} - SMS ({method.phone_number})")
|
||||
|
||||
ind = int(input("Method? > "))
|
||||
|
||||
method = methods[ind]
|
||||
await method.request()
|
||||
code = input("Code? > ")
|
||||
|
||||
# This automatically finishes the post-2FA login flow
|
||||
await method.submit(code)
|
||||
|
||||
|
||||
def get_account_sync(anisette: BaseAnisetteProvider) -> AppleAccount:
|
||||
"""Tries to restore a saved Apple account, or prompts the user for login otherwise. (sync)"""
|
||||
acc = AppleAccount(anisette)
|
||||
|
||||
# Save / restore account logic
|
||||
acc_store = Path("account.json")
|
||||
try:
|
||||
with acc_store.open() as f:
|
||||
acc.restore(json.load(f))
|
||||
except FileNotFoundError:
|
||||
_login_sync(acc)
|
||||
with acc_store.open("w+") as f:
|
||||
json.dump(acc.export(), f)
|
||||
|
||||
return acc
|
||||
|
||||
|
||||
async def get_account_async(anisette: BaseAnisetteProvider) -> AsyncAppleAccount:
|
||||
"""Tries to restore a saved Apple account, or prompts the user for login otherwise. (async)"""
|
||||
acc = AsyncAppleAccount(anisette)
|
||||
|
||||
# Save / restore account logic
|
||||
acc_store = Path("account.json")
|
||||
try:
|
||||
with acc_store.open() as f:
|
||||
acc.restore(json.load(f))
|
||||
except FileNotFoundError:
|
||||
await _login_async(acc)
|
||||
with acc_store.open("w+") as f:
|
||||
json.dump(acc.export(), f)
|
||||
|
||||
return acc
|
||||
@@ -1,22 +1,13 @@
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from _login import get_account_sync
|
||||
|
||||
from findmy import KeyPair
|
||||
from findmy.reports import (
|
||||
AppleAccount,
|
||||
LoginState,
|
||||
RemoteAnisetteProvider,
|
||||
SmsSecondFactorMethod,
|
||||
)
|
||||
from findmy.reports import RemoteAnisetteProvider
|
||||
|
||||
# URL to (public or local) anisette server
|
||||
ANISETTE_SERVER = "http://localhost:6969"
|
||||
|
||||
# Apple account details
|
||||
ACCOUNT_EMAIL = "test@test.com"
|
||||
ACCOUNT_PASS = ""
|
||||
|
||||
# Private base64-encoded key to look up
|
||||
KEY_PRIV = ""
|
||||
|
||||
@@ -26,46 +17,16 @@ KEY_ADV = ""
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
def login(account: AppleAccount) -> None:
|
||||
state = account.login(ACCOUNT_EMAIL, ACCOUNT_PASS)
|
||||
|
||||
if state == LoginState.REQUIRE_2FA: # Account requires 2FA
|
||||
# This only supports SMS methods for now
|
||||
methods = account.get_2fa_methods()
|
||||
|
||||
# Print the (masked) phone numbers
|
||||
for method in methods:
|
||||
if isinstance(method, SmsSecondFactorMethod):
|
||||
print(method.phone_number)
|
||||
|
||||
# Just take the first one to keep things simple
|
||||
method = methods[0]
|
||||
method.request()
|
||||
code = input("Code: ")
|
||||
|
||||
# This automatically finishes the post-2FA login flow
|
||||
method.submit(code)
|
||||
|
||||
|
||||
def fetch_reports(lookup_key: KeyPair) -> None:
|
||||
anisette = RemoteAnisetteProvider(ANISETTE_SERVER)
|
||||
acc = AppleAccount(anisette)
|
||||
|
||||
# Save / restore account logic
|
||||
acc_store = Path("account.json")
|
||||
try:
|
||||
with acc_store.open() as f:
|
||||
acc.restore(json.load(f))
|
||||
except FileNotFoundError:
|
||||
login(acc)
|
||||
with acc_store.open("w+") as f:
|
||||
json.dump(acc.export(), f)
|
||||
acc = get_account_sync(anisette)
|
||||
|
||||
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])
|
||||
print(reports)
|
||||
reports = acc.fetch_last_reports([lookup_key])[lookup_key]
|
||||
for report in sorted(reports):
|
||||
print(report)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from _login import get_account_async
|
||||
|
||||
from findmy import KeyPair
|
||||
from findmy.reports import (
|
||||
AsyncAppleAccount,
|
||||
LoginState,
|
||||
RemoteAnisetteProvider,
|
||||
SmsSecondFactorMethod,
|
||||
)
|
||||
from findmy.reports import RemoteAnisetteProvider
|
||||
|
||||
# URL to (public or local) anisette server
|
||||
ANISETTE_SERVER = "http://localhost:6969"
|
||||
|
||||
# Apple account details
|
||||
ACCOUNT_EMAIL = "test@test.com"
|
||||
ACCOUNT_PASS = ""
|
||||
|
||||
# Private base64-encoded key to look up
|
||||
KEY_PRIV = ""
|
||||
|
||||
@@ -27,41 +18,12 @@ KEY_ADV = ""
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
async def login(account: AsyncAppleAccount) -> None:
|
||||
state = await account.login(ACCOUNT_EMAIL, ACCOUNT_PASS)
|
||||
|
||||
if state == LoginState.REQUIRE_2FA: # Account requires 2FA
|
||||
# This only supports SMS methods for now
|
||||
methods = await account.get_2fa_methods()
|
||||
|
||||
# Print the (masked) phone numbers
|
||||
for method in methods:
|
||||
if isinstance(method, SmsSecondFactorMethod):
|
||||
print(method.phone_number)
|
||||
|
||||
# Just take the first one to keep things simple
|
||||
method = methods[0]
|
||||
await method.request()
|
||||
code = input("Code: ")
|
||||
|
||||
# This automatically finishes the post-2FA login flow
|
||||
await method.submit(code)
|
||||
|
||||
|
||||
async def fetch_reports(lookup_key: KeyPair) -> None:
|
||||
anisette = RemoteAnisetteProvider(ANISETTE_SERVER)
|
||||
acc = AsyncAppleAccount(anisette)
|
||||
|
||||
acc = await get_account_async(anisette)
|
||||
|
||||
try:
|
||||
acc_store = Path("account.json")
|
||||
try:
|
||||
with acc_store.open() as f:
|
||||
acc.restore(json.load(f))
|
||||
except FileNotFoundError:
|
||||
await login(acc)
|
||||
with acc_store.open("w+") as f:
|
||||
json.dump(acc.export(), f)
|
||||
|
||||
print(f"Logged in as: {acc.account_name} ({acc.first_name} {acc.last_name})")
|
||||
|
||||
# It's that simple!
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
"""
|
||||
Example showing how to retrieve the primary key of your own AirTag, or any other FindMy-accessory.
|
||||
Example showing how to fetch locations of an AirTag, or any other FindMy accessory.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
This key can be used to retrieve the device's location for a single day.
|
||||
"""
|
||||
import plistlib
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from findmy import FindMyAccessory
|
||||
from _login import get_account_sync
|
||||
|
||||
from findmy import FindMyAccessory, KeyPair
|
||||
from findmy.reports import RemoteAnisetteProvider
|
||||
|
||||
# URL to (public or local) anisette server
|
||||
ANISETTE_SERVER = "http://localhost:6969"
|
||||
|
||||
# PUBLIC key that the accessory is broadcasting or has previously broadcast.
|
||||
# For nearby devices, you can use `device_scanner.py` to find it.
|
||||
PUBLIC_KEY = ""
|
||||
# Path to a .plist dumped from the Find My app.
|
||||
PLIST_PATH = Path("airtag.plist")
|
||||
|
||||
@@ -29,34 +32,50 @@ SKN = device_data["sharedSecret"]["key"]["data"]
|
||||
# "Secondary" shared secret. 32 bytes.
|
||||
SKS = device_data["secondarySharedSecret"]["key"]["data"]
|
||||
|
||||
# "Paired at" timestamp (UTC)
|
||||
PAIRED_AT = device_data["pairingDate"].replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _gen_keys(airtag: FindMyAccessory, _from: datetime, to: datetime) -> set[KeyPair]:
|
||||
keys = set()
|
||||
while _from < to:
|
||||
keys.update(airtag.keys_at(_from))
|
||||
|
||||
_from += timedelta(minutes=15)
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
def main() -> None:
|
||||
paired_at = device_data["pairingDate"].replace(tzinfo=timezone.utc)
|
||||
airtag = FindMyAccessory(MASTER_KEY, SKN, SKS, paired_at)
|
||||
# Step 0: create an accessory key generator
|
||||
airtag = FindMyAccessory(MASTER_KEY, SKN, SKS, PAIRED_AT)
|
||||
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
lookup_time = paired_at.replace(
|
||||
minute=paired_at.minute // 15 * 15,
|
||||
second=0,
|
||||
microsecond=0,
|
||||
) + timedelta(minutes=15)
|
||||
# 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)
|
||||
|
||||
while lookup_time < now:
|
||||
keys = airtag.keys_at(lookup_time)
|
||||
for key in keys:
|
||||
if key.adv_key_b64 != PUBLIC_KEY:
|
||||
continue
|
||||
print(f"Generating keys from {fetch_from} to {fetch_to} ...")
|
||||
lookup_keys = _gen_keys(airtag, fetch_from, fetch_to)
|
||||
|
||||
print("KEY FOUND!!")
|
||||
print("KEEP THE BELOW KEY SECRET! IT CAN BE USED TO RETRIEVE THE DEVICE'S LOCATION!")
|
||||
print(f" - Key: {key.private_key_b64}")
|
||||
print(f" - Approx. Time: {lookup_time}")
|
||||
print(f" - Type: {key.key_type}")
|
||||
return
|
||||
print(f"Generated {len(lookup_keys)} keys")
|
||||
|
||||
lookup_time += timedelta(minutes=15)
|
||||
# Step 2: log into an Apple account
|
||||
print("Logging into account")
|
||||
anisette = RemoteAnisetteProvider(ANISETTE_SERVER)
|
||||
acc = get_account_sync(anisette)
|
||||
|
||||
print("No match found! :(")
|
||||
# step 3: fetch reports!
|
||||
print("Fetching reports")
|
||||
reports = acc.fetch_reports(list(lookup_keys), fetch_from, fetch_to)
|
||||
|
||||
# step 4: print 'em
|
||||
# reports are in {key: [report]} format, but we only really care about the reports
|
||||
print()
|
||||
print("Location reports:")
|
||||
reports = sorted([r for rs in reports.values() for r in rs])
|
||||
for report in reports:
|
||||
print(f" - {report}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"""Code related to fetching location reports."""
|
||||
from .account import AppleAccount, AsyncAppleAccount
|
||||
from .anisette import RemoteAnisetteProvider
|
||||
from .anisette import BaseAnisetteProvider, RemoteAnisetteProvider
|
||||
from .state import LoginState
|
||||
from .twofactor import SmsSecondFactorMethod
|
||||
from .twofactor import SmsSecondFactorMethod, TrustedDeviceSecondFactorMethod
|
||||
|
||||
__all__ = (
|
||||
"AppleAccount",
|
||||
"AsyncAppleAccount",
|
||||
"LoginState",
|
||||
"BaseAnisetteProvider",
|
||||
"RemoteAnisetteProvider",
|
||||
"SmsSecondFactorMethod",
|
||||
"TrustedDeviceSecondFactorMethod",
|
||||
)
|
||||
|
||||
@@ -3,8 +3,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import plistlib
|
||||
@@ -21,16 +19,15 @@ from typing import (
|
||||
Sequence,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
import bs4
|
||||
import srp._pysrp as srp
|
||||
from cryptography.hazmat.primitives import hashes, padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from typing_extensions import override
|
||||
|
||||
from findmy.errors import InvalidCredentialsError, InvalidStateError, UnhandledProtocolError
|
||||
from findmy.util import crypto
|
||||
from findmy.util.closable import Closable
|
||||
from findmy.util.http import HttpSession, decode_plist
|
||||
|
||||
@@ -39,9 +36,11 @@ from .state import LoginState
|
||||
from .twofactor import (
|
||||
AsyncSecondFactorMethod,
|
||||
AsyncSmsSecondFactor,
|
||||
AsyncTrustedDeviceSecondFactor,
|
||||
BaseSecondFactorMethod,
|
||||
SyncSecondFactorMethod,
|
||||
SyncSmsSecondFactor,
|
||||
SyncTrustedDeviceSecondFactor,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -60,6 +59,7 @@ class _AccountInfo(TypedDict):
|
||||
account_name: str
|
||||
first_name: str
|
||||
last_name: str
|
||||
trusted_device_2fa: bool
|
||||
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
@@ -92,32 +92,6 @@ def require_login_state(*states: LoginState) -> Callable[[_F], _F]:
|
||||
return decorator
|
||||
|
||||
|
||||
def _encrypt_password(password: str, salt: bytes, iterations: int) -> bytes:
|
||||
p = hashlib.sha256(password.encode("utf-8")).digest()
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=iterations,
|
||||
)
|
||||
return kdf.derive(p)
|
||||
|
||||
|
||||
def _decrypt_cbc(session_key: bytes, data: bytes) -> bytes:
|
||||
extra_data_key = hmac.new(session_key, b"extra data key:", hashlib.sha256).digest()
|
||||
extra_data_iv = hmac.new(session_key, b"extra data iv:", hashlib.sha256).digest()
|
||||
# Get only the first 16 bytes of the iv
|
||||
extra_data_iv = extra_data_iv[:16]
|
||||
|
||||
# Decrypt with AES CBC
|
||||
cipher = Cipher(algorithms.AES(extra_data_key), modes.CBC(extra_data_iv))
|
||||
decryptor = cipher.decryptor()
|
||||
data = decryptor.update(data) + decryptor.finalize()
|
||||
# Remove PKCS#7 padding
|
||||
padder = padding.PKCS7(128).unpadder()
|
||||
return padder.update(data) + padder.finalize()
|
||||
|
||||
|
||||
def _extract_phone_numbers(html: str) -> list[dict]:
|
||||
soup = bs4.BeautifulSoup(html, features="html.parser")
|
||||
data_elem = soup.find("script", {"class": "boot_args"})
|
||||
@@ -223,6 +197,24 @@ class BaseAppleAccount(Closable, ABC):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def td_2fa_request(self) -> MaybeCoro[None]:
|
||||
"""
|
||||
Request a 2FA code to be sent to a trusted device.
|
||||
|
||||
Consider using `BaseSecondFactorMethod.request` instead.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def td_2fa_submit(self, code: str) -> MaybeCoro[LoginState]:
|
||||
"""
|
||||
Submit a 2FA code that was sent to a trusted device.
|
||||
|
||||
Consider using `BaseSecondFactorMethod.submit` instead.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def fetch_reports(
|
||||
self,
|
||||
@@ -251,7 +243,11 @@ class BaseAppleAccount(Closable, ABC):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_anisette_headers(self, serial: str = "0") -> MaybeCoro[dict[str, str]]:
|
||||
def get_anisette_headers(
|
||||
self,
|
||||
with_client_info: bool = False,
|
||||
serial: str = "0",
|
||||
) -> MaybeCoro[dict[str, str]]:
|
||||
"""
|
||||
Retrieve a complete dictionary of Anisette headers.
|
||||
|
||||
@@ -263,6 +259,20 @@ class BaseAppleAccount(Closable, ABC):
|
||||
class AsyncAppleAccount(BaseAppleAccount):
|
||||
"""An async implementation of `BaseAppleAccount`."""
|
||||
|
||||
# auth endpoints
|
||||
_ENDPOINT_GSA = "https://gsa.apple.com/grandslam/GsService2"
|
||||
_ENDPOINT_LOGIN_MOBILEME = "https://setup.icloud.com/setup/iosbuddy/loginDelegates"
|
||||
|
||||
# 2fa auth endpoints
|
||||
_ENDPOINT_2FA_METHODS = "https://gsa.apple.com/auth"
|
||||
_ENDPOINT_2FA_SMS_REQUEST = "https://gsa.apple.com/auth/verify/phone"
|
||||
_ENDPOINT_2FA_SMS_SUBMIT = "https://gsa.apple.com/auth/verify/phone/securitycode"
|
||||
_ENDPOINT_2FA_TD_REQUEST = "https://gsa.apple.com/auth/verify/trusteddevice"
|
||||
_ENDPOINT_2FA_TD_SUBMIT = "https://gsa.apple.com/grandslam/GsService2/validate"
|
||||
|
||||
# reports endpoints
|
||||
_ENDPOINT_REPORTS_FETCH = "https://gateway.icloud.com/acsnservice/fetch"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
anisette: BaseAnisetteProvider,
|
||||
@@ -409,8 +419,14 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
"""See `BaseAppleAccount.get_2fa_methods`."""
|
||||
methods: list[AsyncSecondFactorMethod] = []
|
||||
|
||||
if self._account_info is None:
|
||||
return []
|
||||
|
||||
if self._account_info["trusted_device_2fa"]:
|
||||
methods.append(AsyncTrustedDeviceSecondFactor(self))
|
||||
|
||||
# sms
|
||||
auth_page = await self._sms_2fa_request("GET", "https://gsa.apple.com/auth")
|
||||
auth_page = await self._sms_2fa_request("GET", self._ENDPOINT_2FA_METHODS)
|
||||
try:
|
||||
phone_numbers = _extract_phone_numbers(auth_page)
|
||||
methods.extend(
|
||||
@@ -434,7 +450,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
|
||||
await self._sms_2fa_request(
|
||||
"PUT",
|
||||
"https://gsa.apple.com/auth/verify/phone",
|
||||
self._ENDPOINT_2FA_SMS_REQUEST,
|
||||
data,
|
||||
)
|
||||
|
||||
@@ -450,7 +466,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
|
||||
await self._sms_2fa_request(
|
||||
"POST",
|
||||
"https://gsa.apple.com/auth/verify/phone/securitycode",
|
||||
self._ENDPOINT_2FA_SMS_SUBMIT,
|
||||
data,
|
||||
)
|
||||
|
||||
@@ -463,6 +479,44 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
# AUTHENTICATED -> LOGGED_IN
|
||||
return await self._login_mobileme()
|
||||
|
||||
@require_login_state(LoginState.REQUIRE_2FA)
|
||||
@override
|
||||
async def td_2fa_request(self) -> None:
|
||||
"""See `BaseAppleAccount.td_2fa_request`."""
|
||||
headers = {
|
||||
"Content-Type": "text/x-xml-plist",
|
||||
"Accept": "text/x-xml-plist",
|
||||
}
|
||||
await self._sms_2fa_request(
|
||||
"GET",
|
||||
self._ENDPOINT_2FA_TD_REQUEST,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
@require_login_state(LoginState.REQUIRE_2FA)
|
||||
@override
|
||||
async def td_2fa_submit(self, code: str) -> LoginState:
|
||||
"""See `BaseAppleAccount.td_2fa_submit`."""
|
||||
headers = {
|
||||
"security-code": code,
|
||||
"Content-Type": "text/x-xml-plist",
|
||||
"Accept": "text/x-xml-plist",
|
||||
}
|
||||
await self._sms_2fa_request(
|
||||
"GET",
|
||||
self._ENDPOINT_2FA_TD_SUBMIT,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# REQUIRE_2FA -> AUTHENTICATED
|
||||
new_state = await self._gsa_authenticate()
|
||||
if new_state != LoginState.AUTHENTICATED:
|
||||
msg = f"Unexpected state after submitting 2FA: {new_state}"
|
||||
raise UnhandledProtocolError(msg)
|
||||
|
||||
# AUTHENTICATED -> LOGGED_IN
|
||||
return await self._login_mobileme()
|
||||
|
||||
@require_login_state(LoginState.LOGGED_IN)
|
||||
async def fetch_raw_reports(self, start: int, end: int, ids: list[str]) -> dict[str, Any]:
|
||||
"""Make a request for location reports, returning raw data."""
|
||||
@@ -471,8 +525,9 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
self._login_state_data["mobileme_data"]["tokens"]["searchPartyToken"],
|
||||
)
|
||||
data = {"search": [{"startDate": start, "endDate": end, "ids": ids}]}
|
||||
|
||||
r = await self._http.post(
|
||||
"https://gateway.icloud.com/acsnservice/fetch",
|
||||
self._ENDPOINT_REPORTS_FETCH,
|
||||
auth=auth,
|
||||
headers=await self.get_anisette_headers(),
|
||||
json=data,
|
||||
@@ -550,7 +605,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
|
||||
logging.debug("Attempting password challenge")
|
||||
|
||||
usr.p = _encrypt_password(self._password, r["s"], r["i"])
|
||||
usr.p = crypto.encrypt_password(self._password, r["s"], r["i"])
|
||||
m1 = usr.process_challenge(r["s"], r["B"])
|
||||
if m1 is None:
|
||||
msg = "Failed to process challenge"
|
||||
@@ -571,37 +626,45 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
|
||||
logging.debug("Decrypting SPD data in response")
|
||||
|
||||
spd = _decrypt_cbc(usr.get_session_key() or b"", r["spd"])
|
||||
spd = decode_plist(spd)
|
||||
spd = decode_plist(
|
||||
crypto.decrypt_spd_aes_cbc(
|
||||
usr.get_session_key() or b"",
|
||||
r["spd"],
|
||||
),
|
||||
)
|
||||
|
||||
logging.debug("Received account information")
|
||||
self._account_info = {
|
||||
"account_name": spd.get("acname"),
|
||||
"first_name": spd.get("fn"),
|
||||
"last_name": spd.get("ln"),
|
||||
}
|
||||
self._account_info = cast(
|
||||
_AccountInfo,
|
||||
{
|
||||
"account_name": spd.get("acname"),
|
||||
"first_name": spd.get("fn"),
|
||||
"last_name": spd.get("ln"),
|
||||
"trusted_device_2fa": False,
|
||||
},
|
||||
)
|
||||
|
||||
# TODO(malmeloo): support trusted device auth (need account to test)
|
||||
# https://github.com/malmeloo/FindMy.py/issues/1
|
||||
au = r["Status"].get("au")
|
||||
if au in ("secondaryAuth",):
|
||||
if au in ("secondaryAuth", "trustedDeviceSecondaryAuth"):
|
||||
logging.info("Detected 2FA requirement: %s", au)
|
||||
|
||||
self._account_info["trusted_device_2fa"] = au == "trustedDeviceSecondaryAuth"
|
||||
|
||||
return self._set_login_state(
|
||||
LoginState.REQUIRE_2FA,
|
||||
{"adsid": spd["adsid"], "idms_token": spd["GsIdmsToken"]},
|
||||
)
|
||||
if au is not None:
|
||||
msg = f"Unknown auth value: {au}"
|
||||
raise UnhandledProtocolError(msg)
|
||||
if au is None:
|
||||
logging.info("GSA authentication successful")
|
||||
|
||||
logging.info("GSA authentication successful")
|
||||
idms_pet = spd.get("t", {}).get("com.apple.gs.idms.pet", {}).get("token", "")
|
||||
return self._set_login_state(
|
||||
LoginState.AUTHENTICATED,
|
||||
{"idms_pet": idms_pet, "adsid": spd["adsid"]},
|
||||
)
|
||||
|
||||
idms_pet = spd.get("t", {}).get("com.apple.gs.idms.pet", {}).get("token", "")
|
||||
return self._set_login_state(
|
||||
LoginState.AUTHENTICATED,
|
||||
{"idms_pet": idms_pet, "adsid": spd["adsid"]},
|
||||
)
|
||||
msg = f"Unknown auth value: {au}"
|
||||
raise UnhandledProtocolError(msg)
|
||||
|
||||
@require_login_state(LoginState.AUTHENTICATED)
|
||||
async def _login_mobileme(self) -> LoginState:
|
||||
@@ -624,7 +687,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
headers.update(await self.get_anisette_headers())
|
||||
|
||||
resp = await self._http.post(
|
||||
"https://setup.icloud.com/setup/iosbuddy/loginDelegates",
|
||||
self._ENDPOINT_LOGIN_MOBILEME,
|
||||
auth=(self._username or "", self._login_state_data["idms_pet"]),
|
||||
data=data,
|
||||
headers=headers,
|
||||
@@ -648,26 +711,26 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
method: str,
|
||||
url: str,
|
||||
data: dict[str, Any] | None = None,
|
||||
headers: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
adsid = self._login_state_data["adsid"]
|
||||
idms_token = self._login_state_data["idms_token"]
|
||||
identity_token = base64.b64encode((adsid + ":" + idms_token).encode()).decode()
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Xcode",
|
||||
"Accept-Language": "en-us",
|
||||
"X-Apple-Identity-Token": identity_token,
|
||||
"X-Apple-App-Info": "com.apple.gs.xcode.auth",
|
||||
"X-Xcode-Version": "11.2 (11B41)",
|
||||
"X-Mme-Client-Info": "<MacBookPro18,3> <Mac OS X;13.4.1;22F8>"
|
||||
" <com.apple.AOSKit/282 (com.apple.dt.Xcode/3594.4.19)>",
|
||||
}
|
||||
headers.update(await self.get_anisette_headers())
|
||||
headers = headers or {}
|
||||
headers.update(
|
||||
{
|
||||
"User-Agent": "Xcode",
|
||||
"Accept-Language": "en-us",
|
||||
"X-Apple-Identity-Token": identity_token,
|
||||
},
|
||||
)
|
||||
headers.update(await self.get_anisette_headers(with_client_info=True))
|
||||
|
||||
r = await self._http.request(
|
||||
method,
|
||||
url,
|
||||
json=data or {},
|
||||
json=data,
|
||||
headers=headers,
|
||||
)
|
||||
if not r.ok:
|
||||
@@ -676,34 +739,29 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
|
||||
return r.text()
|
||||
|
||||
async def _gsa_request(self, params: dict[str, Any]) -> dict[Any, Any]:
|
||||
request_data = {
|
||||
"cpd": {
|
||||
"bootstrap": True,
|
||||
"icscrec": True,
|
||||
"pbe": False,
|
||||
"prkgen": True,
|
||||
"svct": "iCloud",
|
||||
},
|
||||
}
|
||||
request_data["cpd"].update(await self.get_anisette_headers())
|
||||
request_data.update(params)
|
||||
|
||||
async def _gsa_request(self, parameters: dict[str, Any]) -> dict[str, Any]:
|
||||
body = {
|
||||
"Header": {"Version": "1.0.1"},
|
||||
"Request": request_data,
|
||||
"Header": {
|
||||
"Version": "1.0.1",
|
||||
},
|
||||
"Request": {
|
||||
"cpd": await self._anisette.get_cpd(
|
||||
self._uid,
|
||||
self._devid,
|
||||
),
|
||||
**parameters,
|
||||
},
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "text/x-xml-plist",
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0",
|
||||
"X-MMe-Client-Info": "<MacBookPro18,3> <Mac OS X;13.4.1;22F8> "
|
||||
"<com.apple.AOSKit/282 (com.apple.dt.Xcode/3594.4.19)>",
|
||||
"X-MMe-Client-Info": self._anisette.client,
|
||||
}
|
||||
|
||||
resp = await self._http.post(
|
||||
"https://gsa.apple.com/grandslam/GsService2",
|
||||
self._ENDPOINT_GSA,
|
||||
headers=headers,
|
||||
data=plistlib.dumps(body),
|
||||
)
|
||||
@@ -713,9 +771,13 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
return resp.plist()["Response"]
|
||||
|
||||
@override
|
||||
async def get_anisette_headers(self, serial: str = "0") -> dict[str, str]:
|
||||
async def get_anisette_headers(
|
||||
self,
|
||||
with_client_info: bool = False,
|
||||
serial: str = "0",
|
||||
) -> dict[str, str]:
|
||||
"""See `BaseAppleAccount.get_anisette_headers`."""
|
||||
return await self._anisette.get_headers(self._uid, self._devid, serial)
|
||||
return await self._anisette.get_headers(self._uid, self._devid, serial, with_client_info)
|
||||
|
||||
|
||||
class AppleAccount(BaseAppleAccount):
|
||||
@@ -797,6 +859,8 @@ class AppleAccount(BaseAppleAccount):
|
||||
for m in methods:
|
||||
if isinstance(m, AsyncSmsSecondFactor):
|
||||
res.append(SyncSmsSecondFactor(self, m.phone_number_id, m.phone_number))
|
||||
elif isinstance(m, AsyncTrustedDeviceSecondFactor):
|
||||
res.append(SyncTrustedDeviceSecondFactor(self))
|
||||
else:
|
||||
msg = (
|
||||
f"Failed to cast 2FA object to sync alternative: {m}."
|
||||
@@ -818,6 +882,18 @@ class AppleAccount(BaseAppleAccount):
|
||||
coro = self._asyncacc.sms_2fa_submit(phone_number_id, code)
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@override
|
||||
def td_2fa_request(self) -> None:
|
||||
"""See `AsyncAppleAccount.td_2fa_request`."""
|
||||
coro = self._asyncacc.td_2fa_request()
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@override
|
||||
def td_2fa_submit(self, code: str) -> LoginState:
|
||||
"""See `AsyncAppleAccount.td_2fa_submit`."""
|
||||
coro = self._asyncacc.td_2fa_submit(code)
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@override
|
||||
def fetch_reports(
|
||||
self,
|
||||
@@ -840,7 +916,11 @@ class AppleAccount(BaseAppleAccount):
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@override
|
||||
def get_anisette_headers(self, serial: str = "0") -> dict[str, str]:
|
||||
def get_anisette_headers(
|
||||
self,
|
||||
with_client_info: bool = False,
|
||||
serial: str = "0",
|
||||
) -> dict[str, str]:
|
||||
"""See `AsyncAppleAccount.get_anisette_headers`."""
|
||||
coro = self._asyncacc.get_anisette_headers(serial)
|
||||
coro = self._asyncacc.get_anisette_headers(with_client_info, serial)
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@@ -13,48 +13,148 @@ from findmy.util.closable import Closable
|
||||
from findmy.util.http import HttpSession
|
||||
|
||||
|
||||
def _gen_meta_headers(
|
||||
user_id: str,
|
||||
device_id: str,
|
||||
serial: str = "0",
|
||||
) -> dict[str, str]:
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
locale_str = locale.getdefaultlocale()[0] or "en_US"
|
||||
|
||||
return {
|
||||
"X-Apple-I-Client-Time": now.replace(microsecond=0).isoformat() + "Z",
|
||||
"X-Apple-I-TimeZone": str(now.astimezone().tzinfo),
|
||||
"loc": locale_str,
|
||||
"X-Apple-Locale": locale_str,
|
||||
"X-Apple-I-MD-RINFO": "17106176",
|
||||
"X-Apple-I-MD-LU": base64.b64encode(str(user_id).upper().encode()).decode(),
|
||||
"X-Mme-Device-Id": str(device_id).upper(),
|
||||
"X-Apple-I-SRL-NO": serial,
|
||||
}
|
||||
|
||||
|
||||
class BaseAnisetteProvider(Closable, ABC):
|
||||
"""Abstract base class for Anisette providers."""
|
||||
"""
|
||||
Abstract base class for Anisette providers.
|
||||
|
||||
Generously derived from https://github.com/nythepegasus/grandslam/blob/main/src/grandslam/gsa.py#L41.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
async def _get_base_headers(self) -> dict[str, str]:
|
||||
def otp(self) -> str:
|
||||
"""
|
||||
A seemingly random base64 string containing 28 bytes.
|
||||
|
||||
TODO: Figure out how to generate this.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def machine(self) -> str:
|
||||
"""
|
||||
A base64 encoded string of 60 'random' bytes.
|
||||
|
||||
We're not sure how this is generated, we have to rely on the server.
|
||||
TODO: Figure out how to generate this.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def timestamp(self) -> str:
|
||||
"""Current timestamp in ISO 8601 format."""
|
||||
return datetime.now(tz=timezone.utc).replace(microsecond=0).isoformat() + "Z"
|
||||
|
||||
@property
|
||||
def timezone(self) -> str:
|
||||
"""Abbreviation of the timezone of the device."""
|
||||
return str(datetime.now().astimezone().tzinfo)
|
||||
|
||||
@property
|
||||
def locale(self) -> str:
|
||||
"""Locale of the device (e.g. en_US)."""
|
||||
return locale.getdefaultlocale()[0] or "en_US"
|
||||
|
||||
@property
|
||||
def router(self) -> str:
|
||||
"""
|
||||
A number, either 17106176 or 50660608.
|
||||
|
||||
It doesn't seem to matter which one we use.
|
||||
- 17106176 is used by Sideloadly and Provision (android) based servers.
|
||||
- 50660608 is used by Windows iCloud based servers.
|
||||
"""
|
||||
return "17106176"
|
||||
|
||||
@property
|
||||
def client(self) -> str:
|
||||
"""
|
||||
Client string.
|
||||
|
||||
The format is as follows:
|
||||
<%MODEL%> <%OS%;%MAJOR%.%MINOR%(%SPMAJOR%,%SPMINOR%);%BUILD%>
|
||||
<%AUTHKIT_BUNDLE_ID%/%AUTHKIT_VERSION% (%APP_BUNDLE_ID%/%APP_VERSION%)>
|
||||
|
||||
Where:
|
||||
MODEL: The model of the device (e.g. MacBookPro15,1 or 'PC'
|
||||
OS: The OS of the device (e.g. Mac OS X or Windows)
|
||||
MAJOR: The major version of the OS (e.g. 10)
|
||||
MINOR: The minor version of the OS (e.g. 15)
|
||||
SPMAJOR: The major version of the service pack (e.g. 0) (Windows only)
|
||||
SPMINOR: The minor version of the service pack (e.g. 0) (Windows only)
|
||||
BUILD: The build number of the OS (e.g. 19C57)
|
||||
AUTHKIT_BUNDLE_ID: The bundle ID of the AuthKit framework (e.g. com.apple.AuthKit)
|
||||
AUTHKIT_VERSION: The version of the AuthKit framework (e.g. 1)
|
||||
APP_BUNDLE_ID: The bundle ID of the app (e.g. com.apple.dt.Xcode)
|
||||
APP_VERSION: The version of the app (e.g. 3594.4.19)
|
||||
"""
|
||||
return (
|
||||
"<MacBookPro18,3> <Mac OS X;13.4.1;22F8> "
|
||||
"<com.apple.AOSKit/282 (com.apple.dt.Xcode/3594.4.19)>"
|
||||
)
|
||||
|
||||
async def get_headers(
|
||||
self,
|
||||
user_id: str,
|
||||
device_id: str,
|
||||
serial: str = "0",
|
||||
with_client_info: bool = False,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Retrieve a complete dictionary of Anisette headers.
|
||||
Generate a complete dictionary of Anisette headers.
|
||||
|
||||
Consider using `BaseAppleAccount.get_anisette_headers` instead.
|
||||
"""
|
||||
base_headers = await self._get_base_headers()
|
||||
base_headers.update(_gen_meta_headers(user_id, device_id, serial))
|
||||
headers = {
|
||||
# Current Time
|
||||
"X-Apple-I-Client-Time": self.timestamp,
|
||||
"X-Apple-I-TimeZone": self.timezone,
|
||||
# Locale
|
||||
"loc": self.locale,
|
||||
"X-Apple-Locale": self.locale,
|
||||
# 'One Time Password'
|
||||
"X-Apple-I-MD": self.otp,
|
||||
# 'Local User ID'
|
||||
"X-Apple-I-MD-LU": base64.b64encode(str(user_id).encode()).decode(),
|
||||
# 'Machine ID'
|
||||
"X-Apple-I-MD-M": self.machine,
|
||||
# 'Routing Info', some implementations convert this to an integer
|
||||
"X-Apple-I-MD-RINFO": self.router,
|
||||
# 'Device Unique Identifier'
|
||||
"X-Mme-Device-Id": str(device_id).upper(),
|
||||
# 'Device Serial Number'
|
||||
"X-Apple-I-SRL-NO": serial,
|
||||
}
|
||||
|
||||
return base_headers
|
||||
if with_client_info:
|
||||
headers["X-Mme-Client-Info"] = self.client
|
||||
headers["X-Apple-App-Info"] = "com.apple.gs.xcode.auth"
|
||||
headers["X-Xcode-Version"] = "11.2 (11B41)"
|
||||
|
||||
return headers
|
||||
|
||||
async def get_cpd(
|
||||
self,
|
||||
user_id: str,
|
||||
device_id: str,
|
||||
serial: str = "0",
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Generate a complete dictionary of CPD data.
|
||||
|
||||
Intended for internal use.
|
||||
"""
|
||||
cpd = {
|
||||
"bootstrap": True,
|
||||
"icscrec": True,
|
||||
"pbe": False,
|
||||
"prkgen": True,
|
||||
"svct": "iCloud",
|
||||
}
|
||||
cpd.update(await self.get_headers(user_id, device_id, serial))
|
||||
|
||||
return cpd
|
||||
|
||||
|
||||
class RemoteAnisetteProvider(BaseAnisetteProvider):
|
||||
@@ -68,17 +168,42 @@ class RemoteAnisetteProvider(BaseAnisetteProvider):
|
||||
|
||||
self._http = HttpSession()
|
||||
|
||||
logging.info("Using remote anisette server: %s", self._server_url)
|
||||
self._anisette_data: dict[str, str] | None = None
|
||||
|
||||
@property
|
||||
@override
|
||||
def otp(self) -> str:
|
||||
"""See `BaseAnisetteProvider.otp`_."""
|
||||
otp = (self._anisette_data or {}).get("X-Apple-I-MD")
|
||||
if otp is None:
|
||||
logging.warning("X-Apple-I-MD header not found! Returning fallback...")
|
||||
return otp or ""
|
||||
|
||||
@property
|
||||
@override
|
||||
def machine(self) -> str:
|
||||
"""See `BaseAnisetteProvider.machine`_."""
|
||||
machine = (self._anisette_data or {}).get("X-Apple-I-MD-M")
|
||||
if machine is None:
|
||||
logging.warning("X-Apple-I-MD-M header not found! Returning fallback...")
|
||||
return machine or ""
|
||||
|
||||
@override
|
||||
async def _get_base_headers(self) -> dict[str, str]:
|
||||
r = await self._http.get(self._server_url)
|
||||
headers = r.json()
|
||||
async def get_headers(
|
||||
self,
|
||||
user_id: str,
|
||||
device_id: str,
|
||||
serial: str = "0",
|
||||
with_client_info: bool = False,
|
||||
) -> dict[str, str]:
|
||||
"""See `BaseAnisetteProvider.get_headers`_."""
|
||||
if self._anisette_data is None:
|
||||
logging.info("Fetching anisette data from %s", self._server_url)
|
||||
|
||||
return {
|
||||
"X-Apple-I-MD": headers["X-Apple-I-MD"],
|
||||
"X-Apple-I-MD-M": headers["X-Apple-I-MD-M"],
|
||||
}
|
||||
r = await self._http.get(self._server_url)
|
||||
self._anisette_data = r.json()
|
||||
|
||||
return await super().get_headers(user_id, device_id, serial, with_client_info)
|
||||
|
||||
@override
|
||||
async def close(self) -> None:
|
||||
@@ -91,14 +216,18 @@ class RemoteAnisetteProvider(BaseAnisetteProvider):
|
||||
class LocalAnisetteProvider(BaseAnisetteProvider):
|
||||
"""Anisette provider. Generates headers without a remote server using pyprovision."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the provider."""
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
@override
|
||||
async def _get_base_headers(self) -> dict[str, str]:
|
||||
return NotImplemented
|
||||
def otp(self) -> str:
|
||||
"""See `BaseAnisetteProvider.otp`_."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@override
|
||||
def machine(self) -> str:
|
||||
"""See `BaseAnisetteProvider.machine`_."""
|
||||
raise NotImplementedError
|
||||
|
||||
@override
|
||||
async def close(self) -> None:
|
||||
"""See `AnisetteProvider.close`."""
|
||||
"""See `BaseAnisetteProvider.close`_."""
|
||||
|
||||
@@ -3,29 +3,22 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Sequence, TypedDict, overload
|
||||
from typing import TYPE_CHECKING, Sequence, overload
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from typing_extensions import Unpack, override
|
||||
from typing_extensions import override
|
||||
|
||||
from findmy.keys import KeyPair
|
||||
from findmy.util.http import HttpSession
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .account import AsyncAppleAccount
|
||||
|
||||
_session = HttpSession()
|
||||
|
||||
|
||||
class _FetcherConfig(TypedDict):
|
||||
user_id: str
|
||||
device_id: str
|
||||
dsid: str
|
||||
search_party_token: str
|
||||
logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _decrypt_payload(payload: bytes, key: KeyPair) -> bytes:
|
||||
@@ -131,7 +124,7 @@ class LocationReport:
|
||||
Requires a `KeyPair` to decrypt the report's payload.
|
||||
"""
|
||||
timestamp_int = int.from_bytes(payload[0:4], "big") + (60 * 60 * 24 * 11323)
|
||||
timestamp = datetime.fromtimestamp(timestamp_int, tz=timezone.utc)
|
||||
timestamp = datetime.fromtimestamp(timestamp_int, tz=timezone.utc).astimezone()
|
||||
|
||||
data = _decrypt_payload(payload, key)
|
||||
latitude = struct.unpack(">i", data[0:4])[0] / 10000000
|
||||
@@ -181,14 +174,6 @@ class LocationReportsFetcher:
|
||||
"""
|
||||
self._account: AsyncAppleAccount = account
|
||||
|
||||
self._http: HttpSession = HttpSession()
|
||||
|
||||
self._config: _FetcherConfig | None = None
|
||||
|
||||
def apply_config(self, **conf: Unpack[_FetcherConfig]) -> None:
|
||||
"""Configure internal variables necessary to make reports fetching calls."""
|
||||
self._config = conf
|
||||
|
||||
@overload
|
||||
async def fetch_reports(
|
||||
self,
|
||||
@@ -225,8 +210,12 @@ class LocationReportsFetcher:
|
||||
if isinstance(device, KeyPair):
|
||||
return await self._fetch_reports(date_from, date_to, [device])
|
||||
|
||||
# sequence of KeyPairs
|
||||
reports = await self._fetch_reports(date_from, date_to, device)
|
||||
# sequence of KeyPairs (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]
|
||||
reports.extend(await self._fetch_reports(date_from, date_to, chunk))
|
||||
|
||||
res: dict[KeyPair, list[LocationReport]] = {key: [] for key in device}
|
||||
for report in reports:
|
||||
res[report.key].append(report)
|
||||
@@ -238,6 +227,8 @@ class LocationReportsFetcher:
|
||||
date_to: datetime,
|
||||
keys: Sequence[KeyPair],
|
||||
) -> list[LocationReport]:
|
||||
logging.debug("Fetching reports for %s keys", len(keys))
|
||||
|
||||
start_date = int(date_from.timestamp() * 1000)
|
||||
end_date = int(date_to.timestamp() * 1000)
|
||||
ids = [key.hashed_adv_key_b64 for key in keys]
|
||||
@@ -250,7 +241,7 @@ class LocationReportsFetcher:
|
||||
date_published = datetime.fromtimestamp(
|
||||
report.get("datePublished", 0) / 1000,
|
||||
tz=timezone.utc,
|
||||
)
|
||||
).astimezone()
|
||||
description = report.get("description", "")
|
||||
payload = base64.b64decode(report["payload"])
|
||||
|
||||
|
||||
@@ -122,8 +122,12 @@ class SmsSecondFactorMethod(BaseSecondFactorMethod, ABC):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TrustedDeviceSecondFactorMethod(BaseSecondFactorMethod, ABC):
|
||||
"""Base class for trusted device-based two-factor authentication."""
|
||||
|
||||
|
||||
class AsyncSmsSecondFactor(AsyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||
"""An async implementation of a second-factor method."""
|
||||
"""An async implementation of `SmsSecondFactorMethod`."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -164,16 +168,12 @@ class AsyncSmsSecondFactor(AsyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||
|
||||
@override
|
||||
async def submit(self, code: str) -> LoginState:
|
||||
"""See `BaseSecondFactorMethod.submit`."""
|
||||
"""Submit the 2FA code as received over SMS."""
|
||||
return await self.account.sms_2fa_submit(self._phone_number_id, code)
|
||||
|
||||
|
||||
class SyncSmsSecondFactor(SyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||
"""
|
||||
A sync implementation of `BaseSecondFactorMethod`.
|
||||
|
||||
Uses `AsyncSmsSecondFactor` internally.
|
||||
"""
|
||||
"""A sync implementation of `SmsSecondFactorMethod`."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -208,3 +208,29 @@ class SyncSmsSecondFactor(SyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||
def submit(self, code: str) -> LoginState:
|
||||
"""See `AsyncSmsSecondFactor.submit`."""
|
||||
return self.account.sms_2fa_submit(self._phone_number_id, code)
|
||||
|
||||
|
||||
class AsyncTrustedDeviceSecondFactor(AsyncSecondFactorMethod, TrustedDeviceSecondFactorMethod):
|
||||
"""An async implementation of `TrustedDeviceSecondFactorMethod`."""
|
||||
|
||||
@override
|
||||
async def request(self) -> None:
|
||||
return await self.account.td_2fa_request()
|
||||
|
||||
@override
|
||||
async def submit(self, code: str) -> LoginState:
|
||||
return await self.account.td_2fa_submit(code)
|
||||
|
||||
|
||||
class SyncTrustedDeviceSecondFactor(SyncSecondFactorMethod, TrustedDeviceSecondFactorMethod):
|
||||
"""A sync implementation of `TrustedDeviceSecondFactorMethod`."""
|
||||
|
||||
@override
|
||||
def request(self) -> None:
|
||||
"""See `AsyncTrustedDeviceSecondFactor.request`."""
|
||||
return self.account.td_2fa_request()
|
||||
|
||||
@override
|
||||
def submit(self, code: str) -> LoginState:
|
||||
"""See `AsyncTrustedDeviceSecondFactor.submit`."""
|
||||
return self.account.td_2fa_submit(code)
|
||||
|
||||
@@ -131,7 +131,7 @@ class OfflineFindingScanner:
|
||||
You most likely do not want to use this yourself;
|
||||
check out `OfflineFindingScanner.create` instead.
|
||||
"""
|
||||
self._scanner: BleakScanner = BleakScanner(self._scan_callback)
|
||||
self._scanner: BleakScanner = BleakScanner(self._scan_callback, cb={"use_bdaddr": True})
|
||||
|
||||
self._loop = loop
|
||||
self._device_fut: asyncio.Future[tuple[BLEDevice, AdvertisementData]] = loop.create_future()
|
||||
@@ -173,7 +173,12 @@ class OfflineFindingScanner:
|
||||
if not apple_data:
|
||||
return None
|
||||
|
||||
additional_data = device.details.get("props", {})
|
||||
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)
|
||||
|
||||
async def scan_for(
|
||||
|
||||
@@ -1,13 +1,44 @@
|
||||
"""Pure-python NIST P-224 Elliptic Curve cryptography. Used for some Apple algorithms."""
|
||||
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives.kdf.x963kdf import X963KDF
|
||||
|
||||
ECPoint = tuple[float, float]
|
||||
|
||||
P224_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D
|
||||
|
||||
|
||||
def encrypt_password(password: str, salt: bytes, iterations: int) -> bytes:
|
||||
"""Encrypt password using PBKDF2-HMAC."""
|
||||
p = hashlib.sha256(password.encode("utf-8")).digest()
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=iterations,
|
||||
)
|
||||
return kdf.derive(p)
|
||||
|
||||
|
||||
def decrypt_spd_aes_cbc(session_key: bytes, data: bytes) -> bytes:
|
||||
"""Decrypt SPD data using SRP session key."""
|
||||
extra_data_key = hmac.new(session_key, b"extra data key:", hashlib.sha256).digest()
|
||||
extra_data_iv = hmac.new(session_key, b"extra data iv:", hashlib.sha256).digest()
|
||||
# Get only the first 16 bytes of the iv
|
||||
extra_data_iv = extra_data_iv[:16]
|
||||
|
||||
# Decrypt with AES CBC
|
||||
cipher = Cipher(algorithms.AES(extra_data_key), modes.CBC(extra_data_iv))
|
||||
decryptor = cipher.decryptor()
|
||||
data = decryptor.update(data) + decryptor.finalize()
|
||||
# Remove PKCS#7 padding
|
||||
padder = padding.PKCS7(128).unpadder()
|
||||
return padder.update(data) + padder.finalize()
|
||||
|
||||
|
||||
def x963_kdf(value: bytes, si: bytes, length: int) -> bytes:
|
||||
"""Single pass of X9.63 KDF with SHA1."""
|
||||
return X963KDF(
|
||||
|
||||
@@ -15,7 +15,7 @@ logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _HttpRequestOptions(TypedDict, total=False):
|
||||
json: dict[str, Any]
|
||||
json: dict[str, Any] | None
|
||||
headers: dict[str, str]
|
||||
auth: tuple[str, str] | BasicAuth
|
||||
data: bytes
|
||||
|
||||
363
poetry.lock
generated
363
poetry.lock
generated
@@ -2,87 +2,87 @@
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.9.3"
|
||||
version = "3.9.5"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"},
|
||||
{file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"},
|
||||
{file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"},
|
||||
{file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"},
|
||||
{file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"},
|
||||
{file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"},
|
||||
{file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"},
|
||||
{file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -134,13 +134,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "astroid"
|
||||
version = "3.0.3"
|
||||
version = "3.1.0"
|
||||
description = "An abstract syntax tree for Python with inference support."
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "astroid-3.0.3-py3-none-any.whl", hash = "sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17"},
|
||||
{file = "astroid-3.0.3.tar.gz", hash = "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93"},
|
||||
{file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"},
|
||||
{file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -456,43 +456,43 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "42.0.2"
|
||||
version = "42.0.5"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"},
|
||||
{file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"},
|
||||
{file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"},
|
||||
{file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"},
|
||||
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"},
|
||||
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"},
|
||||
{file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"},
|
||||
{file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"},
|
||||
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"},
|
||||
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"},
|
||||
{file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"},
|
||||
{file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"},
|
||||
{file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"},
|
||||
{file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"},
|
||||
{file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"},
|
||||
{file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"},
|
||||
{file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"},
|
||||
{file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"},
|
||||
{file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"},
|
||||
{file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"},
|
||||
{file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"},
|
||||
{file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"},
|
||||
{file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -575,18 +575,18 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.13.1"
|
||||
version = "3.13.4"
|
||||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
|
||||
{file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
|
||||
{file = "filelock-3.13.4-py3-none-any.whl", hash = "sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f"},
|
||||
{file = "filelock-3.13.4.tar.gz", hash = "sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
|
||||
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
|
||||
typing = ["typing-extensions (>=4.8)"]
|
||||
|
||||
[[package]]
|
||||
@@ -694,13 +694,13 @@ sphinx-basic-ng = "*"
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.5.33"
|
||||
version = "2.5.36"
|
||||
description = "File identification library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"},
|
||||
{file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"},
|
||||
{file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"},
|
||||
{file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -708,13 +708,13 @@ license = ["ukkonen"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
version = "3.7"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
|
||||
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
|
||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -730,22 +730,22 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "7.0.1"
|
||||
version = "7.1.0"
|
||||
description = "Read metadata from Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"},
|
||||
{file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"},
|
||||
{file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"},
|
||||
{file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
perf = ["ipython"]
|
||||
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
|
||||
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 = "jinja2"
|
||||
@@ -1028,39 +1028,40 @@ setuptools = "*"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.2"
|
||||
version = "24.0"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
|
||||
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
||||
{file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
|
||||
{file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.2.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
version = "4.2.1"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
|
||||
{file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
|
||||
{file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"},
|
||||
{file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
|
||||
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 = "pre-commit"
|
||||
version = "3.6.0"
|
||||
version = "3.7.0"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"},
|
||||
{file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"},
|
||||
{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"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1072,13 +1073,13 @@ virtualenv = ">=20.10.0"
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.21"
|
||||
version = "2.22"
|
||||
description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1172,13 +1173,13 @@ pyobjc-core = ">=9.2"
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.350"
|
||||
version = "1.1.359"
|
||||
description = "Command line wrapper for pyright"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyright-1.1.350-py3-none-any.whl", hash = "sha256:f1dde6bcefd3c90aedbe9dd1c573e4c1ddbca8c74bf4fa664dd3b1a599ac9a66"},
|
||||
{file = "pyright-1.1.350.tar.gz", hash = "sha256:a8ba676de3a3737ea4d8590604da548d4498cc5ee9ee00b1a403c6db987916c6"},
|
||||
{file = "pyright-1.1.359-py3-none-any.whl", hash = "sha256:5582777be7eab73512277ac7da7b41e15bc0737f488629cb9babd96e0769be61"},
|
||||
{file = "pyright-1.1.359.tar.gz", hash = "sha256:f0eab50f3dafce8a7302caeafd6a733f39901a2bf5170bb23d77fd607c8a8dbc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1271,19 +1272,19 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "69.0.3"
|
||||
version = "69.5.1"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"},
|
||||
{file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"},
|
||||
{file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"},
|
||||
{file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
@@ -1320,20 +1321,20 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "sphinx"
|
||||
version = "7.2.6"
|
||||
version = "7.3.7"
|
||||
description = "Python documentation generator"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "sphinx-7.2.6-py3-none-any.whl", hash = "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560"},
|
||||
{file = "sphinx-7.2.6.tar.gz", hash = "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5"},
|
||||
{file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"},
|
||||
{file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
alabaster = ">=0.7,<0.8"
|
||||
alabaster = ">=0.7.14,<0.8.0"
|
||||
babel = ">=2.9"
|
||||
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||
docutils = ">=0.18.1,<0.21"
|
||||
docutils = ">=0.18.1,<0.22"
|
||||
imagesize = ">=1.3"
|
||||
importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""}
|
||||
Jinja2 = ">=3.0"
|
||||
@@ -1347,11 +1348,12 @@ sphinxcontrib-htmlhelp = ">=2.0.0"
|
||||
sphinxcontrib-jsmath = "*"
|
||||
sphinxcontrib-qthelp = "*"
|
||||
sphinxcontrib-serializinghtml = ">=1.1.9"
|
||||
tomli = {version = ">=2", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinxcontrib-websupport"]
|
||||
lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"]
|
||||
test = ["cython (>=3.0)", "filelock", "html5lib", "pytest (>=4.6)", "setuptools (>=67.0)"]
|
||||
lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"]
|
||||
test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx-autoapi"
|
||||
@@ -1502,26 +1504,37 @@ files = [
|
||||
[package.dependencies]
|
||||
six = "*"
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.9.0"
|
||||
version = "4.11.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
|
||||
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
|
||||
{file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
|
||||
{file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.2.0"
|
||||
version = "2.2.1"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"},
|
||||
{file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"},
|
||||
{file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
|
||||
{file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -1532,13 +1545,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.25.0"
|
||||
version = "20.26.0"
|
||||
description = "Virtual Python Environment builder"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"},
|
||||
{file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"},
|
||||
{file = "virtualenv-20.26.0-py3-none-any.whl", hash = "sha256:0846377ea76e818daaa3e00a4365c018bc3ac9760cbb3544de542885aad61fb3"},
|
||||
{file = "virtualenv-20.26.0.tar.gz", hash = "sha256:ec25a9671a5102c8d2657f62792a27b48f016664c6873f6beed3800008577210"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1547,7 +1560,7 @@ filelock = ">=3.12.2,<4"
|
||||
platformdirs = ">=3.9.1,<5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
|
||||
|
||||
[[package]]
|
||||
@@ -1873,18 +1886,18 @@ multidict = ">=4.0"
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.17.0"
|
||||
version = "3.18.1"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
|
||||
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
|
||||
{file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"},
|
||||
{file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
|
||||
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"]
|
||||
@@ -1892,4 +1905,4 @@ scan = ["bleak"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9,<3.13"
|
||||
content-hash = "ac0ef4ca30b86c4ef5b45db550b69e6ef85203e9c667cc5002ae8998cd05edd6"
|
||||
content-hash = "828fc3307e8314148461691a7ef95572699b2e9597713a118c469a5532c65d61"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "FindMy"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
description = "Everything you need to work with Apple's Find My network!"
|
||||
authors = ["Mike Almeloo <git@mikealmel.ooo>"]
|
||||
readme = "README.md"
|
||||
@@ -9,7 +9,7 @@ packages = [{ include = "findmy" }]
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<3.13"
|
||||
srp = "^1.0.20"
|
||||
cryptography = "^42.0.2"
|
||||
cryptography = "^42.0.5"
|
||||
beautifulsoup4 = "^4.12.2"
|
||||
aiohttp = "^3.9.1"
|
||||
bleak = "^0.21.1"
|
||||
@@ -51,6 +51,7 @@ ignore = [
|
||||
"D105", # docstrings in magic methods
|
||||
|
||||
"PLR2004", # "magic" values >.>
|
||||
"FBT", # boolean "traps"
|
||||
]
|
||||
|
||||
line-length = 100
|
||||
|
||||
Reference in New Issue
Block a user