Merge branch 'refs/heads/main' into feat/better-docs

# Conflicts:
#	poetry.lock
This commit is contained in:
Mike A
2024-04-24 11:38:12 +02:00
20 changed files with 812 additions and 469 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: [malmeloo]

7
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"schedule:monthly"
]
}

View File

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

View File

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

@@ -161,3 +161,5 @@ cython_debug/
.idea/
account.json
airtag.plist
DO_NOT_COMMIT*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`_."""

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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