From f87ac3b2b29e385d9af5a76bbdab925407748657 Mon Sep 17 00:00:00 2001 From: Simon Wilkinson Date: Tue, 17 Nov 2020 14:39:17 +0000 Subject: [PATCH 1/5] Add support for the new encryption protocol This adds support for the new TP-Link discovery and encryption protocols. It is currently incomplete - only devices without username and password are current supported, and single device discovery is not implemented. Discovery should find both old and new devices. When accessing a device by IP the --klap option can be specified on the command line to active the new connection protocol. sdb9696 - This commit also contains 16 later commits from Simon Wilkinson squashed into the original --- kasa/auth.py | 21 ++++++ kasa/klapprotocol.py | 155 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 kasa/auth.py create mode 100755 kasa/klapprotocol.py diff --git a/kasa/auth.py b/kasa/auth.py new file mode 100644 index 000000000..efd431a82 --- /dev/null +++ b/kasa/auth.py @@ -0,0 +1,21 @@ +"""Authentication class for KASA username / passwords.""" +from hashlib import md5 + + +class Auth: + """Authentication for Kasa KLAP authentication.""" + + def __init__(self, user: str = "", password: str = ""): + self.user = user + self.password = password + self.md5user = md5(user.encode()).digest() + self.md5password = md5(password.encode()).digest() + self.md5auth = md5(self.md5user + self.md5password).digest() + + def authenticator(self): + """Return the KLAP authenticator for these credentials.""" + return self.md5auth + + def owner(self): + """Return the MD5 hash of the username in this object.""" + return self.md5user diff --git a/kasa/klapprotocol.py b/kasa/klapprotocol.py new file mode 100755 index 000000000..7b901ce4c --- /dev/null +++ b/kasa/klapprotocol.py @@ -0,0 +1,155 @@ +"""Implementation of the TP-Link Smart Home Protocol. + +Encryption/Decryption methods based on the works of +Lubomir Stroetmann and Tobias Esser + +https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/ +https://github.com/softScheck/tplink-smartplug/ + +which are licensed under the Apache License, Version 2.0 +http://www.apache.org/licenses/LICENSE-2.0 +""" +import asyncio +import hashlib +import logging +import secrets + +import aiohttp +from Crypto.Cipher import AES +from Crypto.Util import Padding +from yarl import URL + +from .auth import Auth +from .exceptions import SmartDeviceException +from .protocol import TPLinkProtocol + +_LOGGER = logging.getLogger(__name__) + + +class TPLinkKLAP(TPLinkProtocol): + """Implementation of the KLAP encryption protocol. + + KLAP is the name used in device discovery for TP-Link's new encryption + protocol, used by newer firmware versions. + """ + + def __init__(self, host: str, authentication: Auth = Auth()) -> None: + self.jar = aiohttp.CookieJar(unsafe=True, quote_cookie=False) + self.client_challenge = secrets.token_bytes(16) + self.authenticator = authentication.authenticator() + self.handshake_lock = asyncio.Lock() + self.handshake_done = False + + super().__init__(host=host) + + _LOGGER.debug("[KLAP] Created KLAP object for %s", self.host) + + @staticmethod + def _sha256(payload: bytes) -> bytes: + return hashlib.sha256(payload).digest() + + async def _handshake(self, session) -> None: + _LOGGER.debug("[KLAP] Starting handshake with %s", self.host) + + # Handshake 1 has a payload of client_challenge + # and a response of 16 bytes, followed by sha256(clientBytes | authenticator) + + url = f"http://{self.host}/app/handshake1" + resp = await session.post(url, data=self.client_challenge) + _LOGGER.debug("Got response of %d to handshake1", resp.status) + if resp.status != 200: + raise SmartDeviceException( + "Device responded with %d to handshake1" % resp.status + ) + + response = await resp.read() + self.server_challenge = response[0:16] + server_hash = response[16:] + + _LOGGER.debug("Server bytes are: %s", self.server_challenge.hex()) + _LOGGER.debug("Server hash is: %s", server_hash.hex()) + + # Check the response from the device + local_hash = self._sha256(self.client_challenge + self.authenticator) + + if local_hash != server_hash: + _LOGGER.debug( + "Expected %s got %s in handshake1", + local_hash.hex(), + server_hash.hex(), + ) + raise SmartDeviceException("Server response doesn't match our challenge") + else: + _LOGGER.debug("handshake1 hashes match") + + # We need to include only the TP_SESSIONID cookie - aiohttp's cookie handling + # adds a bogus TIMEOUT cookie + cookie = session.cookie_jar.filter_cookies(url).get("TP_SESSIONID") + session.cookie_jar.clear() + session.cookie_jar.update_cookies({"TP_SESSIONID": cookie}, URL(url)) + _LOGGER.debug("Cookie is %s", cookie) + + # Handshake 2 has the following payload: + # sha256(serverBytes | authenticator) + url = f"http://{self.host}/app/handshake2" + payload = self._sha256(self.server_challenge + self.authenticator) + resp = await session.post(url, data=payload) + _LOGGER.debug("Got response of %d to handshake2", resp.status) + if resp.status != 200: + raise SmartDeviceException( + "Device responded with %d to handshake2" % resp.status + ) + + # Done handshaking, now we need to compute the encryption keys + agreed = self.client_challenge + self.server_challenge + self.authenticator + self.encrypt_key = self._sha256(b"lsk" + agreed)[:16] + self.hmac_key = self._sha256(b"ldk" + agreed)[:28] + fulliv = self._sha256(b"iv" + agreed) + self.iv = fulliv[:12] + self.seq = int.from_bytes(fulliv[-4:], "big", signed=True) + self.handshake_done = True + + def _encrypt(self, plaintext: bytes, iv: bytes, seq: int) -> bytes: + cipher = AES.new(self.encrypt_key, AES.MODE_CBC, iv) + ciphertext = cipher.encrypt(Padding.pad(plaintext, AES.block_size)) + signature = self._sha256( + self.hmac_key + seq.to_bytes(4, "big", signed=True) + ciphertext + ) + return signature + ciphertext + + def _decrypt(self, payload: bytes, iv: bytes, seq: int) -> bytes: + cipher = AES.new(self.encrypt_key, AES.MODE_CBC, iv) + # In theory we should verify the hmac here too + return Padding.unpad(cipher.decrypt(payload[32:]), AES.block_size) + + async def _ask(self, request: str) -> str: + + try: + timeout = aiohttp.ClientTimeout(total=self.timeout) + session = aiohttp.ClientSession(cookie_jar=self.jar, timeout=timeout) + + async with self.handshake_lock: + if not self.handshake_done: + await self._handshake(session) + + msg_seq = self.seq + msg_iv = self.iv + msg_seq.to_bytes(4, "big", signed=True) + payload = self._encrypt(request.encode("utf-8"), msg_iv, msg_seq) + + url = f"http://{self.host}/app/request" + resp = await session.post(url, params={"seq": msg_seq}, data=payload) + _LOGGER.debug("Got response of %d to request", resp.status) + + # If we failed with a security error, force a new handshake next time + if resp.status == 403: + self.handshake_done = False + + if resp.status != 200: + raise SmartDeviceException( + "Device responded with %d to request with seq %d" + % (resp.status, msg_seq) + ) + response = await resp.read() + return self._decrypt(response, msg_iv, msg_seq).decode("utf-8") + finally: + await session.close() From 72ef18a9130d8195b59b514f63fcbb16dfc35b31 Mon Sep 17 00:00:00 2001 From: sdb9696 Date: Tue, 22 Aug 2023 12:38:30 +0100 Subject: [PATCH 2/5] Update klap changes 2023 to fix encryption, deal with kasa credential switching and work with new discovery changes --- .github/workflows/ci.yml | 7 +- .github/workflows/codeql-analysis.yml | 16 + kasa/__init__.py | 5 +- kasa/auth.py | 22 +- kasa/cli.py | 11 +- kasa/discover.py | 96 ++++- kasa/klapprotocol.py | 520 +++++++++++++++++++++----- kasa/protocol.py | 26 +- kasa/smartdevice.py | 7 +- kasa/tests/test_discovery.py | 4 +- poetry.lock | 514 ++++++++++++++++++++++++- pyproject.toml | 4 + 12 files changed, 1093 insertions(+), 139 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0dce418d..c2a9a500b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,9 +87,10 @@ jobs: # exclude pypy on windows, as the poetry install seems to be very flaky: # PermissionError(13, 'The process cannot access the file because it is being used by another process')) # at C:\hostedtoolcache\windows\PyPy\3.7.10\x86\site-packages\requests\models.py:761 in generate - # exclude: - # - python-version: pypy-3.8 - # os: windows-latest + # and with pypy3.8 trying to use setuptools to build frozenlist which is a dependency of aiohttp + # ChefBuildError: Backend 'setuptools.build_meta' is not available. + - os: windows-latest + python-version: pypy-3.8 steps: - uses: "actions/checkout@v2" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b8d5f3968..14f8f5b8c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -33,3 +33,19 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 + with: + upload: False + output: sarif-results + + - name: filter-sarif + uses: advanced-security/filter-sarif@v1 + with: + patterns: | + -**/kasa/klapprotocol.py:py/weak-sensitive-data-hashing + input: sarif-results/python.sarif + output: sarif-results/python.sarif + + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: sarif-results/python.sarif diff --git a/kasa/__init__.py b/kasa/__init__.py index 4ccf6286b..989e507f2 100755 --- a/kasa/__init__.py +++ b/kasa/__init__.py @@ -21,7 +21,8 @@ SmartDeviceException, UnsupportedDeviceException, ) -from kasa.protocol import TPLinkSmartHomeProtocol +from kasa.klapprotocol import TPLinkKlap +from kasa.protocol import TPLinkProtocol, TPLinkSmartHomeProtocol from kasa.smartbulb import SmartBulb, SmartBulbPreset, TurnOnBehavior, TurnOnBehaviors from kasa.smartdevice import DeviceType, SmartDevice from kasa.smartdimmer import SmartDimmer @@ -35,6 +36,8 @@ __all__ = [ "Discover", "TPLinkSmartHomeProtocol", + "TPLinkProtocol", + "TPLinkKlap", "SmartBulb", "SmartBulbPreset", "TurnOnBehaviors", diff --git a/kasa/auth.py b/kasa/auth.py index efd431a82..4f00609e0 100644 --- a/kasa/auth.py +++ b/kasa/auth.py @@ -1,21 +1,9 @@ -"""Authentication class for KASA username / passwords.""" -from hashlib import md5 +"""Authentication class for username / passwords.""" -class Auth: - """Authentication for Kasa KLAP authentication.""" +class AuthCredentials: + """Authentication credentials for Kasa authentication.""" - def __init__(self, user: str = "", password: str = ""): - self.user = user + def __init__(self, username: str = "", password: str = ""): + self.username = username self.password = password - self.md5user = md5(user.encode()).digest() - self.md5password = md5(password.encode()).digest() - self.md5auth = md5(self.md5user + self.md5password).digest() - - def authenticator(self): - """Return the KLAP authenticator for these credentials.""" - return self.md5auth - - def owner(self): - """Return the MD5 hash of the username in this object.""" - return self.md5user diff --git a/kasa/cli.py b/kasa/cli.py index f0c180c57..91cf309f5 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -6,7 +6,7 @@ import sys from functools import singledispatch, wraps from pprint import pformat as pf -from typing import Any, Dict, cast +from typing import Any, Dict, Optional, cast import asyncclick as click @@ -302,6 +302,7 @@ async def discover(ctx, timeout, show_unsupported): sem = asyncio.Semaphore() discovered = dict() unsupported = [] + auth_failed = [] async def print_unsupported(data: Dict): unsupported.append(data) @@ -309,6 +310,11 @@ async def print_unsupported(data: Dict): echo(f"Found unsupported device (tapo/unknown encryption): {data}") echo() + async def print_auth_failed(data: Dict): + auth_failed.append(data) + echo(f"Authentication failed for device: {data}") + echo() + echo(f"Discovering devices on {target} for {timeout} seconds") async def print_discovered(dev: SmartDevice): @@ -325,6 +331,7 @@ async def print_discovered(dev: SmartDevice): on_discovered=print_discovered, on_unsupported=print_unsupported, credentials=credentials, + on_auth_failed=print_auth_failed, ) echo(f"Found {len(discovered)} devices") @@ -337,6 +344,8 @@ async def print_discovered(dev: SmartDevice): else ", to show them use: kasa discover --show-unsupported" ) ) + if auth_failed: + echo(f"Found {len(auth_failed)} devices that failed to authenticate") return discovered diff --git a/kasa/discover.py b/kasa/discover.py index f8e11a62b..68c47c40c 100755 --- a/kasa/discover.py +++ b/kasa/discover.py @@ -3,17 +3,18 @@ import binascii import logging import socket -from typing import Awaitable, Callable, Dict, Optional, Type, cast +from typing import Awaitable, Callable, Dict, Optional, Set, Type, cast # When support for cpython older than 3.11 is dropped # async_timeout can be replaced with asyncio.timeout from async_timeout import timeout as asyncio_timeout from kasa.credentials import Credentials -from kasa.exceptions import UnsupportedDeviceException +from kasa.exceptions import UnsupportedDeviceException, AuthenticationException from kasa.json import dumps as json_dumps from kasa.json import loads as json_loads -from kasa.protocol import TPLinkSmartHomeProtocol +from kasa.klapprotocol import TPLinkKlap +from kasa.protocol import TPLinkProtocol, TPLinkSmartHomeProtocol from kasa.smartbulb import SmartBulb from kasa.smartdevice import SmartDevice, SmartDeviceException from kasa.smartdimmer import SmartDimmer @@ -47,6 +48,7 @@ def __init__( port: Optional[int] = None, discovered_event: Optional[asyncio.Event] = None, credentials: Optional[Credentials] = None, + on_auth_failed: Optional[Callable[[Dict], Awaitable[None]]] = None, ): self.transport = None self.discovery_packets = discovery_packets @@ -58,9 +60,13 @@ def __init__( self.discovered_devices = {} self.unsupported_devices: Dict = {} self.invalid_device_exceptions: Dict = {} + self.auth_failed_devices: Dict = {} self.on_unsupported = on_unsupported self.discovered_event = discovered_event self.credentials = credentials + self.on_auth_failed = on_auth_failed + self.ip_sem = asyncio.Semaphore() + self.ip_set: Set[str] = set() def connection_made(self, transport) -> None: """Set socket options for broadcasting.""" @@ -91,27 +97,61 @@ def do_discover(self) -> None: def datagram_received(self, data, addr) -> None: """Handle discovery responses.""" - ip, port = addr - if ( - ip in self.discovered_devices - or ip in self.unsupported_devices - or ip in self.invalid_device_exceptions - ): - return + asyncio.create_task(self.datagram_received_async(data, addr)) + async def datagram_received_async(self, data, addr) -> None: + """Handle discovery responses.""" + ip, port = addr + # Prevent multiple entries due multiple broadcasts + async with self.ip_sem: + if ip in self.ip_set: + return + self.ip_set.add(ip) + + protocol: Optional[TPLinkProtocol] = None + info: Dict if port == self.discovery_port: info = json_loads(TPLinkSmartHomeProtocol.decrypt(data)) _LOGGER.debug("[DISCOVERY] %s << %s", ip, info) - elif port == Discover.DISCOVERY_PORT_2: info = json_loads(data[16:]) - self.unsupported_devices[ip] = info - if self.on_unsupported is not None: - asyncio.ensure_future(self.on_unsupported(info)) - _LOGGER.debug("[DISCOVERY] Unsupported device found at %s << %s", ip, info) - if self.discovered_event is not None: - self.discovered_event.set() - return + if not ( + self.credentials is not None + and Discover._is_klap_discovery_info(info) + ): + _LOGGER.debug("Unsupported device found at %s << %s", ip, info) + self.unsupported_devices[ip] = info + if self.on_unsupported is not None: + asyncio.ensure_future(self.on_unsupported(info)) + if self.discovered_event is not None and "255" not in self.target[ + 0 + ].split("."): + self.discovered_event.set() + return + else: + _LOGGER.debug( + "[DISCOVERY] Initialising klap protocol for device %s (%s) with owner %s", + ip, + info["result"]["mac"], + info["result"]["owner"], + ) + protocol = TPLinkKlap(ip, credentials=self.credentials) + try: + info = await protocol.get_sysinfo_info() + except AuthenticationException: + _LOGGER.debug( + "Authentication failed for device found at %s << %s", ip, info + ) + self.auth_failed_devices[ip] = info + if self.on_auth_failed is not None: + asyncio.ensure_future(self.on_auth_failed(info)) + if self.discovered_event is not None and "255" not in self.target[ + 0 + ].split("."): + self.discovered_event.set() + return + + _LOGGER.debug("[DISCOVERY] %s << %s", ip, info) try: device_class = Discover._get_device_class(info) @@ -127,6 +167,10 @@ def datagram_received(self, data, addr) -> None: device = device_class(ip, port=port, credentials=self.credentials) device.update_from_discover_info(info) + # Override protocol if klap created and used succesfully + if protocol: + device.override_protocol(protocol) + self.discovered_devices[ip] = device if self.on_discovered is not None: @@ -197,6 +241,7 @@ async def discover( interface=None, on_unsupported=None, credentials=None, + on_auth_failed=None, ) -> DeviceDict: """Discover supported devices. @@ -226,6 +271,7 @@ async def discover( interface=interface, on_unsupported=on_unsupported, credentials=credentials, + on_auth_failed=on_auth_failed, ), local_addr=("0.0.0.0", 0), ) @@ -287,6 +333,10 @@ async def discover_single( ) elif host in protocol.invalid_device_exceptions: raise protocol.invalid_device_exceptions[host] + elif host in protocol.auth_failed_devices: + raise AuthenticationException( + f"Authentication failed for {host}" + ) else: raise SmartDeviceException(f"Unable to get discovery response for {host}") @@ -317,3 +367,13 @@ def _get_device_class(info: dict) -> Type[SmartDevice]: return SmartBulb raise SmartDeviceException("Unknown device type: %s" % type_) + + @staticmethod + def _is_klap_discovery_info(info: dict) -> bool: + return ( + "result" in info + and "mgt_encrypt_schm" in info["result"] + and "encrypt_type" in info["result"]["mgt_encrypt_schm"] + and info["result"]["mgt_encrypt_schm"]["encrypt_type"] == "KLAP" + and "lv" not in info["result"]["mgt_encrypt_schm"] + ) diff --git a/kasa/klapprotocol.py b/kasa/klapprotocol.py index 7b901ce4c..18ec640a5 100755 --- a/kasa/klapprotocol.py +++ b/kasa/klapprotocol.py @@ -1,155 +1,493 @@ """Implementation of the TP-Link Smart Home Protocol. +Comment by sdb - 4-Jul-2023 + Encryption/Decryption methods based on the works of -Lubomir Stroetmann and Tobias Esser +Simon Wilkinson and Chris Weeldon + +While working on these changes I discovered my HS100 devices would periodically change their device owner to something that produces the following +md5 owner hash: 994661e5222b8e5e3e1d90e73a322315. It seems to be after an update to the on/off state that was scheduled via the app. Switching the device on and off manually via the +Kasa app would revert to the correct owner. + +For devices that have not been connected to the kasa cloud the theory is that blank username and password md5 hashes will succesfully authenticate but +at this point I have been unable to verify. -https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/ -https://github.com/softScheck/tplink-smartplug/ +https://gist.github.com/chriswheeldon/3b17d974db3817613c69191c0480fe55 +https://github.com/python-kasa/python-kasa/pull/117 + +N.B. chrisweeldon implementation had a bug in the encryption logic for determining the initial seq number and Simon Wilkinson's implementation did not seem to support +incrementing the sequence number for subsequent encryption requests -which are licensed under the Apache License, Version 2.0 -http://www.apache.org/licenses/LICENSE-2.0 """ import asyncio +import datetime import hashlib import logging import secrets +from pprint import pformat as pf +from typing import Any, Dict, Optional, Tuple, Union import aiohttp -from Crypto.Cipher import AES -from Crypto.Util import Padding +from cryptography.hazmat.primitives import hashes, padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from yarl import URL -from .auth import Auth -from .exceptions import SmartDeviceException +from .credentials import Credentials +from .exceptions import AuthenticationException, SmartDeviceException +from .json import dumps as json_dumps +from .json import loads as json_loads from .protocol import TPLinkProtocol _LOGGER = logging.getLogger(__name__) -class TPLinkKLAP(TPLinkProtocol): +class TPLinkKlap(TPLinkProtocol): """Implementation of the KLAP encryption protocol. KLAP is the name used in device discovery for TP-Link's new encryption protocol, used by newer firmware versions. """ - def __init__(self, host: str, authentication: Auth = Auth()) -> None: + DEFAULT_PORT = 80 + DISCOVERY_PORT = 20002 + DEFAULT_TIMEOUT = 5 + DISCOVERY_QUERY = {"system": {"get_sysinfo": None}} + TP_SESSION_COOKIE_NAME = "TP_SESSIONID" + KASA_SETUP_EMAIL = "kasa@tp-link.net" + KASA_SETUP_PASSWORD = "kasaSetup" + + def __init__( + self, host: str, credentials: Credentials = Credentials() + ) -> None: + super().__init__(host=host, port=self.DEFAULT_PORT) + + self.credentials = credentials if credentials.username is not None and credentials.password is not None else Credentials(username="",password="") self.jar = aiohttp.CookieJar(unsafe=True, quote_cookie=False) - self.client_challenge = secrets.token_bytes(16) - self.authenticator = authentication.authenticator() + + self._local_seed: Optional[bytes] = None + self.local_auth_hash = self.generate_auth_hash(self.credentials) + self.local_auth_owner = self.generate_owner_hash(self.credentials).hex() self.handshake_lock = asyncio.Lock() + self.query_lock = asyncio.Lock() self.handshake_done = False - super().__init__(host=host) + self.encryption_session: Optional[KlapEncryptionSession] = None + + self.timeout = self.DEFAULT_TIMEOUT _LOGGER.debug("[KLAP] Created KLAP object for %s", self.host) + async def get_sysinfo_info(self): + """Return discovery info from host or None if unable to.""" + return await self.query(TPLinkKlap.DISCOVERY_QUERY) + @staticmethod def _sha256(payload: bytes) -> bytes: return hashlib.sha256(payload).digest() - async def _handshake(self, session) -> None: - _LOGGER.debug("[KLAP] Starting handshake with %s", self.host) + @staticmethod + def _md5(payload: bytes) -> bytes: + digest = hashes.Hash(hashes.MD5()) + digest.update(payload) + hash = digest.finalize() + return hash + + @staticmethod + async def session_post(session, url, params=None, data=None): + """Send an http post request to the device.""" + response_data = None + + resp = await session.post(url, params=params, data=data) + async with resp: + if resp.status == 200: + response_data = await resp.read() + else: + try: + response_data = await resp.read() + except Exception: + pass + + return resp.status, response_data + + def get_local_seed(self): + """Get the local seed. Can be mocked for testing.""" + return secrets.token_bytes(16) + + @staticmethod + def handle_cookies(session, url): + """Strip out any cookies other than TP_SESSION.""" + # We need to include only the TP_SESSIONID cookie - the klap device sends a + # TIMEOUT cookie after handshake1 that it doesn't like getting back again + cookie = session.cookie_jar.filter_cookies(url).get( + TPLinkKlap.TP_SESSION_COOKIE_NAME + ) + session.cookie_jar.clear() + session.cookie_jar.update_cookies( + {TPLinkKlap.TP_SESSION_COOKIE_NAME: cookie}, URL(url) + ) + + def clear_cookies(self, session): + """Clear out all cookies for new handshake.""" + session.cookie_jar.clear() + self.jar.clear() + + async def perform_handshake1( + self, session, new_local_seed: Optional[bytes] = None + ) -> Tuple[bytes, bytes]: + """Perform handshake1. Resets authentication_failed to False at the start.""" + self.authentication_failed = False + + self.clear_cookies(session) - # Handshake 1 has a payload of client_challenge + if new_local_seed is not None: + self._local_seed = new_local_seed + else: + self._local_seed = self.get_local_seed() + + # Handshake 1 has a payload of local_seed # and a response of 16 bytes, followed by sha256(clientBytes | authenticator) + self.handshake_done = False + + payload = self._local_seed url = f"http://{self.host}/app/handshake1" - resp = await session.post(url, data=self.client_challenge) - _LOGGER.debug("Got response of %d to handshake1", resp.status) - if resp.status != 200: - raise SmartDeviceException( - "Device responded with %d to handshake1" % resp.status + + response_status, response_data = await self.session_post( + session, url, data=payload + ) + + cookie = self.jar.filter_cookies(url).get(self.TP_SESSION_COOKIE_NAME) + + if response_status != 200: + raise AuthenticationException( + "Device %s responded with %d to handshake1, this is probably not a klap device" + % (self.host, response_status) ) + self.handle_cookies(session, url) - response = await resp.read() - self.server_challenge = response[0:16] - server_hash = response[16:] + remote_seed = response_data[0:16] + server_hash = response_data[16:] - _LOGGER.debug("Server bytes are: %s", self.server_challenge.hex()) - _LOGGER.debug("Server hash is: %s", server_hash.hex()) + cookie = self.jar.filter_cookies(url).get(self.TP_SESSION_COOKIE_NAME) + _LOGGER.debug( + f"Handshake1 posted at {datetime.datetime.now()}. Host is {self.host}, Session cookie is {cookie}, Response status is {response_status}, Request was {self.local_auth_hash.hex()}" + ) - # Check the response from the device - local_hash = self._sha256(self.client_challenge + self.authenticator) + _LOGGER.debug( + "Server remote_seed is: %s, server hash is: %s", + remote_seed.hex(), + server_hash.hex(), + ) + + local_seed_auth_hash = self._sha256(self._local_seed + self.local_auth_hash) - if local_hash != server_hash: + # Check the response from the device + if local_seed_auth_hash == server_hash: + _LOGGER.debug("handshake1 hashes match") + return remote_seed, self.local_auth_hash + else: _LOGGER.debug( - "Expected %s got %s in handshake1", - local_hash.hex(), + "Expected %s got %s in handshake1. Checking if blank auth is a match", + local_seed_auth_hash.hex(), server_hash.hex(), ) - raise SmartDeviceException("Server response doesn't match our challenge") - else: - _LOGGER.debug("handshake1 hashes match") - - # We need to include only the TP_SESSIONID cookie - aiohttp's cookie handling - # adds a bogus TIMEOUT cookie - cookie = session.cookie_jar.filter_cookies(url).get("TP_SESSIONID") - session.cookie_jar.clear() - session.cookie_jar.update_cookies({"TP_SESSIONID": cookie}, URL(url)) - _LOGGER.debug("Cookie is %s", cookie) + blank_auth = Credentials(username="", password="") + blank_auth_hash = self.generate_auth_hash(blank_auth) + blank_seed_auth_hash = self._sha256(self._local_seed + blank_auth_hash) + if blank_seed_auth_hash == server_hash: + _LOGGER.debug( + "Server response doesn't match our expected hash on ip %s but an authentication with blank credentials matched", + self.host, + ) + return remote_seed, blank_auth_hash + else: + kasa_setup_auth = Credentials( + username=self.KASA_SETUP_EMAIL, password=self.KASA_SETUP_PASSWORD + ) + kasa_setup_auth_hash = self.generate_auth_hash(kasa_setup_auth) + kasa_setup_seed_auth_hash = self._sha256( + self._local_seed + kasa_setup_auth_hash + ) + if kasa_setup_seed_auth_hash == server_hash: + self.local_auth_hash = kasa_setup_auth_hash + _LOGGER.debug( + "Server response doesn't match our expected hash on ip %s but an authentication with kasa setup credentials matched", + self.host, + ) + return remote_seed, kasa_setup_auth_hash + else: + self.authentication_failed = True + msg = "Server response doesn't match our challenge on ip {}".format( + self.host + ) + _LOGGER.debug(msg) + raise AuthenticationException(msg) + + async def perform_handshake2(self, session, remote_seed, auth_hash) -> None: + """Perform handshake2. Sets authentication_failed based on success/failure.""" # Handshake 2 has the following payload: # sha256(serverBytes | authenticator) + url = f"http://{self.host}/app/handshake2" - payload = self._sha256(self.server_challenge + self.authenticator) - resp = await session.post(url, data=payload) - _LOGGER.debug("Got response of %d to handshake2", resp.status) - if resp.status != 200: - raise SmartDeviceException( - "Device responded with %d to handshake2" % resp.status - ) - # Done handshaking, now we need to compute the encryption keys - agreed = self.client_challenge + self.server_challenge + self.authenticator - self.encrypt_key = self._sha256(b"lsk" + agreed)[:16] - self.hmac_key = self._sha256(b"ldk" + agreed)[:28] - fulliv = self._sha256(b"iv" + agreed) - self.iv = fulliv[:12] - self.seq = int.from_bytes(fulliv[-4:], "big", signed=True) - self.handshake_done = True - - def _encrypt(self, plaintext: bytes, iv: bytes, seq: int) -> bytes: - cipher = AES.new(self.encrypt_key, AES.MODE_CBC, iv) - ciphertext = cipher.encrypt(Padding.pad(plaintext, AES.block_size)) - signature = self._sha256( - self.hmac_key + seq.to_bytes(4, "big", signed=True) + ciphertext + payload = self._sha256(remote_seed + auth_hash) + + response_status, response_data = await self.session_post( + session, url, data=payload ) - return signature + ciphertext - def _decrypt(self, payload: bytes, iv: bytes, seq: int) -> bytes: - cipher = AES.new(self.encrypt_key, AES.MODE_CBC, iv) - # In theory we should verify the hmac here too - return Padding.unpad(cipher.decrypt(payload[32:]), AES.block_size) + cookie = self.jar.filter_cookies(url).get(self.TP_SESSION_COOKIE_NAME) + _LOGGER.debug( + f"Handshake2 posted {datetime.datetime.now()}. Host is {self.host}, Session cookie is {cookie}, Response status is {response_status}, Request was {payload!r}" + ) - async def _ask(self, request: str) -> str: + if response_status != 200: + self.authentication_failed = True + self.handshake_done = False + raise AuthenticationException( + "Device responded with %d to handshake2" % response_status + ) + else: + self.authentication_failed = False + self.handshake_done = True + self.handle_cookies(session, url) + + async def perform_handshake( + self, session, new_local_seed: Optional[bytes] = None + ) -> Any: + """Perform handshake1 and handshake2 and set the encryption_session if successful.""" + _LOGGER.debug("[KLAP] Starting handshake with %s", self.host) - try: - timeout = aiohttp.ClientTimeout(total=self.timeout) - session = aiohttp.ClientSession(cookie_jar=self.jar, timeout=timeout) + remote_seed, auth_hash = await self.perform_handshake1(session, new_local_seed) - async with self.handshake_lock: - if not self.handshake_done: - await self._handshake(session) + await self.perform_handshake2(session, remote_seed, auth_hash) - msg_seq = self.seq - msg_iv = self.iv + msg_seq.to_bytes(4, "big", signed=True) - payload = self._encrypt(request.encode("utf-8"), msg_iv, msg_seq) + self.encryption_session = KlapEncryptionSession( + self._local_seed, remote_seed, auth_hash + ) - url = f"http://{self.host}/app/request" - resp = await session.post(url, params={"seq": msg_seq}, data=payload) - _LOGGER.debug("Got response of %d to request", resp.status) + _LOGGER.debug("[KLAP] Handshake with %s complete", self.host) - # If we failed with a security error, force a new handshake next time - if resp.status == 403: - self.handshake_done = False + @staticmethod + def generate_auth_hash(auth: Credentials): + """Generate an md5 auth hash for the protocol on the supplied credentials.""" + return TPLinkKlap._md5( + TPLinkKlap._md5(auth.username.encode()) + + TPLinkKlap._md5(auth.password.encode()) + ) - if resp.status != 200: + @staticmethod + def generate_owner_hash(auth: Credentials): + """Return the MD5 hash of the username in this object.""" + return TPLinkKlap._md5(auth.username.encode()) + + async def query(self, request: Union[str, Dict], retry_count: int = 3) -> Dict: + """Query the device retrying for retry_count on failure.""" + if isinstance(request, dict): + request = json_dumps(request) + assert isinstance(request, str) + + async with self.query_lock: + return await self._query(request, retry_count) + + async def _query(self, request: str, retry_count: int = 3) -> Dict: + for retry in range(retry_count + 1): + try: + return await self._execute_query(request, retry) + except aiohttp.ClientConnectionError as ex: + raise SmartDeviceException( + f"Unable to connect to the device: {self.host}: {ex}" + ) + except TimeoutError as ex: raise SmartDeviceException( - "Device responded with %d to request with seq %d" - % (resp.status, msg_seq) + f"Unable to connect to the device, timed out: {self.host}: {ex}" ) - response = await resp.read() - return self._decrypt(response, msg_iv, msg_seq).decode("utf-8") - finally: - await session.close() + except AuthenticationException as auex: + _LOGGER.debug("Unable to authenticate with %s, not retrying", self.host) + raise auex + except Exception as ex: + if retry >= retry_count: + _LOGGER.debug("Giving up on %s after %s retries", self.host, retry) + raise SmartDeviceException( + f"Unable to connect to the device: {self.host}: {ex}" + ) + continue + + # make mypy happy, this should never be reached.. + raise SmartDeviceException("Query reached somehow to unreachable") + + async def _execute_query(self, request: str, retry_count: int) -> Dict: + timeout = aiohttp.ClientTimeout(total=self.timeout) + + async with aiohttp.ClientSession( + cookie_jar=self.jar, timeout=timeout + ) as session: + if not self.handshake_done: + try: + await self.perform_handshake(session) + + except AuthenticationException as auex: + _LOGGER.debug( + f"Unable to complete handshake for device {self.host}, authentication failed" + ) + raise auex + + # Check for mypy + if self.encryption_session is not None: + payload, seq = self.encryption_session.encrypt(request.encode()) + + url = f"http://{self.host}/app/request" + + cookie = self.jar.filter_cookies(url).get(self.TP_SESSION_COOKIE_NAME) + + response_status, response_data = await self.session_post( + session, url, params={"seq": seq}, data=payload + ) + + if response_status != 200: + _LOGGER.error( + f"Query failed after succesful authentication at {datetime.datetime.now()}. Host is {self.host}, Session cookie is {cookie}, Retry count is {retry_count}, Sequence is {seq}, Response status is {response_status}, Request was {request}" + ) + # If we failed with a security error, force a new handshake next time. + if response_status == 403: + self.handshake_done = False + self.authentication_failed = True + raise AuthenticationException( + "Got a security error after handshake completed" + ) + else: + raise SmartDeviceException( + "Device %s responded with %d to request with seq %d" + % (self.host, response_status, seq) + ) + else: + _LOGGER.debug( + f"Query posted at {datetime.datetime.now()}. Host is {self.host}, Session cookie is {cookie}, Retry count is {retry_count}, Sequence is {seq}, Response status is {response_status}, Request was {request}" + ) + + self.handle_cookies(session, url) + + self.authentication_failed = False + + # Check for mypy + if self.encryption_session is not None: + decrypted_response = self.encryption_session.decrypt(response_data) + + json_payload = json_loads(decrypted_response) + + _LOGGER.debug("%s << %s", self.host, pf(json_payload)) + + return json_payload + + async def close(self) -> None: + """Close the protocol. Does nothing for this implementation.""" + pass + + def parse_unauthenticated_info(self, unauthenticated_info) -> Dict[str, str]: + """Parse raw unauthenticated info based on the data the protocol expects.""" + if "result" not in unauthenticated_info: + raise SmartDeviceException( + f"Received unexpected unauthenticated_info for {self.host}" + ) + + result = unauthenticated_info["result"] + + if unauthenticated_info["result"]["owner"] != self.local_auth_owner: + pad = 8 + len("python-kasa.tplinkklap.auth_message") + 2 + msg = "The owner hashes do not match, if you expected authentication\n" + msg += f"{' ':>{pad}}to work try switching the device on and off via the Kasa app\n" + msg += f"{' ':>{pad}}to see if the device owner gets corrected." + else: + msg = "The owner hashed match, do you have the wrong password?" + + def _get_value(thedict, value): + return "" if thedict == "" or value not in thedict else thedict[value] + + return { + "ip": _get_value(result, "ip"), + "mac": _get_value(result, "mac"), + "device_id": _get_value(result, "device_id"), + "owner": _get_value(result, "owner"), + "device_type": _get_value(result, "device_type"), + "device_model": _get_value(result, "device_model"), + "hw_ver": _get_value(result, "hw_ver"), + "factory_default": _get_value(result, "factory_default"), + "mgt_encrypt_schm.is_support_https": _get_value( + _get_value(result, "mgt_encrypt_schm"), "is_support_https" + ), + "mgt_encrypt_schm.encrypt_type": _get_value( + _get_value(result, "mgt_encrypt_schm"), "encrypt_type" + ), + "mgt_encrypt_schm.http_port": _get_value( + _get_value(result, "mgt_encrypt_schm"), "http_port" + ), + "error_code": _get_value(unauthenticated_info, "error_code"), + "python-kasa.tplinkklap.auth_owner_hash": self.local_auth_owner, + "python-kasa.tplinkklap.auth_message": msg, + } + + +class KlapEncryptionSession: + """Class to represent an encryption session and it's internal state, i.e. sequence number.""" + + def __init__(self, local_seed, remote_seed, user_hash): + self._key = self._key_derive(local_seed, remote_seed, user_hash) + (self._iv, self._seq) = self._iv_derive(local_seed, remote_seed, user_hash) + self._sig = self._sig_derive(local_seed, remote_seed, user_hash) + + def _key_derive(self, local_seed, remote_seed, user_hash): + payload = b"lsk" + local_seed + remote_seed + user_hash + return hashlib.sha256(payload).digest()[:16] + + def _iv_derive(self, local_seed, remote_seed, user_hash): + # iv is first 16 bytes of sha256, where the last 4 bytes forms the + # sequence number used in requests and is incremented on each request + payload = b"iv" + local_seed + remote_seed + user_hash + fulliv = hashlib.sha256(payload).digest() + seq = int.from_bytes(fulliv[-4:], "big", signed=True) + return (fulliv[:12], seq) + + def _sig_derive(self, local_seed, remote_seed, user_hash): + # used to create a hash with which to prefix each request + payload = b"ldk" + local_seed + remote_seed + user_hash + return hashlib.sha256(payload).digest()[:28] + + def _iv_seq(self): + seq = self._seq.to_bytes(4, "big", signed=True) + iv = self._iv + seq + assert len(iv) == 16 + return iv + + def encrypt(self, msg): + """Encrypt the data and increment the sequence number.""" + self._seq = self._seq + 1 + if type(msg) == str: + msg = msg.encode("utf-8") + assert type(msg) == bytes + + cipher = Cipher(algorithms.AES(self._key), modes.CBC(self._iv_seq())) + encryptor = cipher.encryptor() + padder = padding.PKCS7(128).padder() + padded_data = padder.update(msg) + padder.finalize() + ciphertext = encryptor.update(padded_data) + encryptor.finalize() + + digest = hashes.Hash(hashes.SHA256()) + digest.update( + self._sig + self._seq.to_bytes(4, "big", signed=True) + ciphertext + ) + signature = digest.finalize() + + return (signature + ciphertext, self._seq) + + def decrypt(self, msg): + """Decrypt the data.""" + assert type(msg) == bytes + + cipher = Cipher(algorithms.AES(self._key), modes.CBC(self._iv_seq())) + decryptor = cipher.decryptor() + dp = decryptor.update(msg[32:]) + decryptor.finalize() + unpadder = padding.PKCS7(128).unpadder() + plaintextbytes = unpadder.update(dp) + unpadder.finalize() + + return plaintextbytes.decode() diff --git a/kasa/protocol.py b/kasa/protocol.py index 461dd85ad..62a8c924a 100755 --- a/kasa/protocol.py +++ b/kasa/protocol.py @@ -14,6 +14,7 @@ import errno import logging import struct +from abc import ABC, abstractmethod from pprint import pformat as pf from typing import Dict, Generator, Optional, Union @@ -29,7 +30,26 @@ _NO_RETRY_ERRORS = {errno.EHOSTDOWN, errno.EHOSTUNREACH, errno.ECONNREFUSED} -class TPLinkSmartHomeProtocol: +class TPLinkProtocol(ABC): + """Base class for all TP-Link Smart Home communication.""" + + def __init__(self, host: str, *, port: Optional[int] = None) -> None: + """Create a protocol object.""" + self.host = host + self.port = port + + @abstractmethod + async def query(self, request: Union[str, Dict], retry_count: int = 3) -> Dict: + """Query the device for the protocol. Abstract method to be overriden.""" + pass + + @abstractmethod + async def close(self) -> None: + """Close the protocol. Abstract method to be overriden.""" + pass + + +class TPLinkSmartHomeProtocol(TPLinkProtocol): """Implementation of the TP-Link Smart Home protocol.""" INITIALIZATION_VECTOR = 171 @@ -39,8 +59,8 @@ class TPLinkSmartHomeProtocol: def __init__(self, host: str, *, port: Optional[int] = None) -> None: """Create a protocol object.""" - self.host = host - self.port = port or TPLinkSmartHomeProtocol.DEFAULT_PORT + super().__init__(host=host, port=port or self.DEFAULT_PORT) + self.reader: Optional[asyncio.StreamReader] = None self.writer: Optional[asyncio.StreamWriter] = None self.query_lock: Optional[asyncio.Lock] = None diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index 5c24c943e..0fb4e60a9 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -24,7 +24,7 @@ from .emeterstatus import EmeterStatus from .exceptions import SmartDeviceException from .modules import Emeter, Module -from .protocol import TPLinkSmartHomeProtocol +from .protocol import TPLinkProtocol, TPLinkSmartHomeProtocol _LOGGER = logging.getLogger(__name__) @@ -205,7 +205,6 @@ def __init__( """ self.host = host self.port = port - self.protocol = TPLinkSmartHomeProtocol(host, port=port) self.credentials = credentials _LOGGER.debug("Initializing %s of type %s", self.host, type(self)) @@ -219,6 +218,10 @@ def __init__( self.children: List["SmartDevice"] = [] + def override_protocol(self, protocol: TPLinkProtocol): + """Override the default protocol for device communication.""" + self.protocol = protocol + def add_module(self, name: str, module: Module): """Register a module.""" if name in self.modules: diff --git a/kasa/tests/test_discovery.py b/kasa/tests/test_discovery.py index 41578a2ce..f6a871f59 100644 --- a/kasa/tests/test_discovery.py +++ b/kasa/tests/test_discovery.py @@ -170,9 +170,9 @@ async def test_discover_datagram_received(mocker, discovery_data): mocker.patch.object(protocol.TPLinkSmartHomeProtocol, "decrypt") addr = "127.0.0.1" - proto.datagram_received("", (addr, 9999)) addr2 = "127.0.0.2" - proto.datagram_received("", (addr2, 20002)) + await proto.datagram_received_async("", (addr, 9999)) + await proto.datagram_received_async("", (addr2, 20002)) # Check that device in discovered_devices is initialized correctly assert len(proto.discovered_devices) == 1 diff --git a/poetry.lock b/poetry.lock index 180b6cd08..8915c1d08 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,127 @@ # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +[[package]] +name = "aiohttp" +version = "3.8.5" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"}, + {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"}, + {file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825"}, + {file = "aiohttp-3.8.5-cp310-cp310-win32.whl", hash = "sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802"}, + {file = "aiohttp-3.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df"}, + {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9"}, + {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975"}, + {file = "aiohttp-3.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c"}, + {file = "aiohttp-3.8.5-cp311-cp311-win32.whl", hash = "sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945"}, + {file = "aiohttp-3.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755"}, + {file = "aiohttp-3.8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"}, + {file = "aiohttp-3.8.5-cp36-cp36m-win32.whl", hash = "sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e"}, + {file = "aiohttp-3.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-win32.whl", hash = "sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22"}, + {file = "aiohttp-3.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d"}, + {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced"}, + {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690"}, + {file = "aiohttp-3.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35"}, + {file = "aiohttp-3.8.5-cp38-cp38-win32.whl", hash = "sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c"}, + {file = "aiohttp-3.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e"}, + {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3"}, + {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51"}, + {file = "aiohttp-3.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91"}, + {file = "aiohttp-3.8.5-cp39-cp39-win32.whl", hash = "sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67"}, + {file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"}, + {file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<4.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "cchardet"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + [[package]] name = "alabaster" version = "0.7.13" @@ -71,6 +193,24 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + [[package]] name = "babel" version = "2.12.1" @@ -107,6 +247,82 @@ files = [ {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.4.0" @@ -306,6 +522,51 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "41.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, + {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, + {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, + {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +nox = ["nox"] +pep8test = ["black", "check-sdist", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "distlib" version = "0.3.7" @@ -357,6 +618,76 @@ files = [ docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +[[package]] +name = "frozenlist" +version = "1.4.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62"}, + {file = "frozenlist-1.4.0-cp310-cp310-win32.whl", hash = "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0"}, + {file = "frozenlist-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb"}, + {file = "frozenlist-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431"}, + {file = "frozenlist-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8"}, + {file = "frozenlist-1.4.0-cp38-cp38-win32.whl", hash = "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc"}, + {file = "frozenlist-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3"}, + {file = "frozenlist-1.4.0-cp39-cp39-win32.whl", hash = "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f"}, + {file = "frozenlist-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167"}, + {file = "frozenlist-1.4.0.tar.gz", hash = "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251"}, +] + [[package]] name = "identify" version = "2.5.27" @@ -578,6 +909,89 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + [[package]] name = "myst-parser" version = "0.18.1" @@ -746,6 +1160,17 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pydantic" version = "2.3.0" @@ -1444,6 +1869,93 @@ tests-binary = ["cmake", "cmake", "ninja", "ninja", "pybind11", "pybind11", "sci tests-binary-strict = ["cmake (==3.21.2)", "cmake (==3.25.0)", "ninja (==1.10.2)", "ninja (==1.11.1)", "pybind11 (==2.10.3)", "pybind11 (==2.7.1)", "scikit-build (==0.11.1)", "scikit-build (==0.16.1)"] tests-strict = ["codecov (==2.0.15)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "typing (==3.7.4)"] +[[package]] +name = "yarl" +version = "1.9.2" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, + {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, + {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, + {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, + {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, + {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, + {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, + {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, + {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, + {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, + {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, + {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, + {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + [[package]] name = "zipp" version = "3.16.2" @@ -1466,4 +1978,4 @@ speedups = ["kasa-crypt", "orjson"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "888a000414d6140156c0f878af06470505ed6edaab936af8a607d396c6252bf9" +content-hash = "431f6f655f63b75ed3cc57fa00bb4102acdd57b81ded44a53ff6c2df0e46053e" diff --git a/pyproject.toml b/pyproject.toml index f8adeeed4..c017573d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,10 @@ pydantic = ">=1" orjson = { "version" = ">=3.9.1", optional = true } kasa-crypt = { "version" = ">=0.2.0", optional = true } +# Klap +aiohttp = "^3" +cryptography = ">=1.9" + # required only for docs sphinx = { version = "^4", optional = true } sphinx_rtd_theme = { version = "^0", optional = true } From 0aff8ffc735fff663a21068a011f31462b7e3913 Mon Sep 17 00:00:00 2001 From: sdb9696 Date: Tue, 5 Sep 2023 11:53:45 +0100 Subject: [PATCH 3/5] Remove codeql changes as it doesn't seem to trigger since moving to cryptography --- .github/workflows/codeql-analysis.yml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 14f8f5b8c..53c64f020 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,7 +2,7 @@ name: "CodeQL checks" on: push: - branches: [ master ] + branches: [ master, add_klap_protocol ] pull_request: branches: [ master ] schedule: @@ -33,19 +33,3 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 - with: - upload: False - output: sarif-results - - - name: filter-sarif - uses: advanced-security/filter-sarif@v1 - with: - patterns: | - -**/kasa/klapprotocol.py:py/weak-sensitive-data-hashing - input: sarif-results/python.sarif - output: sarif-results/python.sarif - - - name: Upload SARIF - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: sarif-results/python.sarif From 48fd8aadaa8277daba735a2f8a59edd75ecde21a Mon Sep 17 00:00:00 2001 From: sdb9696 Date: Tue, 5 Sep 2023 14:24:35 +0100 Subject: [PATCH 4/5] Changes to datagram_received following review --- .github/workflows/codeql-analysis.yml | 2 +- kasa/auth.py | 9 -- kasa/cli.py | 29 ++--- kasa/discover.py | 152 ++++++++++++-------------- kasa/klapprotocol.py | 35 +++--- kasa/smartdevice.py | 6 +- kasa/tests/test_discovery.py | 6 +- 7 files changed, 112 insertions(+), 127 deletions(-) delete mode 100644 kasa/auth.py diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 53c64f020..b8d5f3968 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,7 +2,7 @@ name: "CodeQL checks" on: push: - branches: [ master, add_klap_protocol ] + branches: [ master ] pull_request: branches: [ master ] schedule: diff --git a/kasa/auth.py b/kasa/auth.py deleted file mode 100644 index 4f00609e0..000000000 --- a/kasa/auth.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Authentication class for username / passwords.""" - - -class AuthCredentials: - """Authentication credentials for Kasa authentication.""" - - def __init__(self, username: str = "", password: str = ""): - self.username = username - self.password = password diff --git a/kasa/cli.py b/kasa/cli.py index 91cf309f5..29494818c 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -6,7 +6,7 @@ import sys from functools import singledispatch, wraps from pprint import pformat as pf -from typing import Any, Dict, Optional, cast +from typing import Any, Dict, cast import asyncclick as click @@ -21,6 +21,8 @@ SmartStrip, ) +from .exceptions import AuthenticationException + try: from rich import print as _do_echo except ImportError: @@ -304,26 +306,24 @@ async def discover(ctx, timeout, show_unsupported): unsupported = [] auth_failed = [] - async def print_unsupported(data: Dict): + async def print_unsupported(data: str): unsupported.append(data) if show_unsupported: echo(f"Found unsupported device (tapo/unknown encryption): {data}") echo() - async def print_auth_failed(data: Dict): - auth_failed.append(data) - echo(f"Authentication failed for device: {data}") - echo() - echo(f"Discovering devices on {target} for {timeout} seconds") async def print_discovered(dev: SmartDevice): - await dev.update() - async with sem: - discovered[dev.host] = dev.internal_state - ctx.obj = dev - await ctx.invoke(state) - echo() + try: + await dev.update() + async with sem: + discovered[dev.host] = dev.internal_state + ctx.obj = dev + await ctx.invoke(state) + echo() + except AuthenticationException as aex: + auth_failed.append(str(aex)) await Discover.discover( target=target, @@ -331,7 +331,6 @@ async def print_discovered(dev: SmartDevice): on_discovered=print_discovered, on_unsupported=print_unsupported, credentials=credentials, - on_auth_failed=print_auth_failed, ) echo(f"Found {len(discovered)} devices") @@ -346,6 +345,8 @@ async def print_discovered(dev: SmartDevice): ) if auth_failed: echo(f"Found {len(auth_failed)} devices that failed to authenticate") + for fail in auth_failed: + echo(fail) return discovered diff --git a/kasa/discover.py b/kasa/discover.py index 68c47c40c..8a3925c2b 100755 --- a/kasa/discover.py +++ b/kasa/discover.py @@ -10,11 +10,11 @@ from async_timeout import timeout as asyncio_timeout from kasa.credentials import Credentials -from kasa.exceptions import UnsupportedDeviceException, AuthenticationException +from kasa.exceptions import UnsupportedDeviceException from kasa.json import dumps as json_dumps from kasa.json import loads as json_loads from kasa.klapprotocol import TPLinkKlap -from kasa.protocol import TPLinkProtocol, TPLinkSmartHomeProtocol +from kasa.protocol import TPLinkSmartHomeProtocol from kasa.smartbulb import SmartBulb from kasa.smartdevice import SmartDevice, SmartDeviceException from kasa.smartdimmer import SmartDimmer @@ -44,11 +44,10 @@ def __init__( target: str = "255.255.255.255", discovery_packets: int = 3, interface: Optional[str] = None, - on_unsupported: Optional[Callable[[Dict], Awaitable[None]]] = None, + on_unsupported: Optional[Callable[[str], Awaitable[None]]] = None, port: Optional[int] = None, discovered_event: Optional[asyncio.Event] = None, credentials: Optional[Credentials] = None, - on_auth_failed: Optional[Callable[[Dict], Awaitable[None]]] = None, ): self.transport = None self.discovery_packets = discovery_packets @@ -60,12 +59,9 @@ def __init__( self.discovered_devices = {} self.unsupported_devices: Dict = {} self.invalid_device_exceptions: Dict = {} - self.auth_failed_devices: Dict = {} self.on_unsupported = on_unsupported self.discovered_event = discovered_event self.credentials = credentials - self.on_auth_failed = on_auth_failed - self.ip_sem = asyncio.Semaphore() self.ip_set: Set[str] = set() def connection_made(self, transport) -> None: @@ -96,81 +92,38 @@ def do_discover(self) -> None: self.transport.sendto(Discover.DISCOVERY_QUERY_2, self.target_2) # type: ignore def datagram_received(self, data, addr) -> None: - """Handle discovery responses.""" - asyncio.create_task(self.datagram_received_async(data, addr)) - - async def datagram_received_async(self, data, addr) -> None: """Handle discovery responses.""" ip, port = addr # Prevent multiple entries due multiple broadcasts - async with self.ip_sem: - if ip in self.ip_set: - return - self.ip_set.add(ip) - - protocol: Optional[TPLinkProtocol] = None - info: Dict - if port == self.discovery_port: - info = json_loads(TPLinkSmartHomeProtocol.decrypt(data)) - _LOGGER.debug("[DISCOVERY] %s << %s", ip, info) - elif port == Discover.DISCOVERY_PORT_2: - info = json_loads(data[16:]) - if not ( - self.credentials is not None - and Discover._is_klap_discovery_info(info) - ): - _LOGGER.debug("Unsupported device found at %s << %s", ip, info) - self.unsupported_devices[ip] = info - if self.on_unsupported is not None: - asyncio.ensure_future(self.on_unsupported(info)) - if self.discovered_event is not None and "255" not in self.target[ - 0 - ].split("."): - self.discovered_event.set() - return - else: - _LOGGER.debug( - "[DISCOVERY] Initialising klap protocol for device %s (%s) with owner %s", - ip, - info["result"]["mac"], - info["result"]["owner"], - ) - protocol = TPLinkKlap(ip, credentials=self.credentials) - try: - info = await protocol.get_sysinfo_info() - except AuthenticationException: - _LOGGER.debug( - "Authentication failed for device found at %s << %s", ip, info - ) - self.auth_failed_devices[ip] = info - if self.on_auth_failed is not None: - asyncio.ensure_future(self.on_auth_failed(info)) - if self.discovered_event is not None and "255" not in self.target[ - 0 - ].split("."): - self.discovered_event.set() - return - - _LOGGER.debug("[DISCOVERY] %s << %s", ip, info) + if ip in self.ip_set: + return + self.ip_set.add(ip) + device = None try: - device_class = Discover._get_device_class(info) + if port == self.discovery_port: + device = Discover._get_device_instance_legacy(data, ip, port) + elif port == Discover.DISCOVERY_PORT_2: + device = Discover._get_device_instance( + data, ip, port, self.credentials or Credentials() + ) + else: + return + except UnsupportedDeviceException as udex: + _LOGGER.debug("Unsupported device found at %s << %s", ip, udex) + self.unsupported_devices[ip] = str(udex) + if self.on_unsupported is not None: + asyncio.ensure_future(self.on_unsupported(str(udex))) + if self.discovered_event is not None: + self.discovered_event.set() + return except SmartDeviceException as ex: - _LOGGER.debug( - "[DISCOVERY] Unable to find device type from %s: %s", info, ex - ) + _LOGGER.debug(f"[DISCOVERY] Unable to find device type for {ip}: {ex}") self.invalid_device_exceptions[ip] = ex if self.discovered_event is not None: self.discovered_event.set() return - device = device_class(ip, port=port, credentials=self.credentials) - device.update_from_discover_info(info) - - # Override protocol if klap created and used succesfully - if protocol: - device.override_protocol(protocol) - self.discovered_devices[ip] = device if self.on_discovered is not None: @@ -241,7 +194,6 @@ async def discover( interface=None, on_unsupported=None, credentials=None, - on_auth_failed=None, ) -> DeviceDict: """Discover supported devices. @@ -271,7 +223,6 @@ async def discover( interface=interface, on_unsupported=on_unsupported, credentials=credentials, - on_auth_failed=on_auth_failed, ), local_addr=("0.0.0.0", 0), ) @@ -333,10 +284,6 @@ async def discover_single( ) elif host in protocol.invalid_device_exceptions: raise protocol.invalid_device_exceptions[host] - elif host in protocol.auth_failed_devices: - raise AuthenticationException( - f"Authentication failed for {host}" - ) else: raise SmartDeviceException(f"Unable to get discovery response for {host}") @@ -365,15 +312,58 @@ def _get_device_class(info: dict) -> Type[SmartDevice]: return SmartLightStrip return SmartBulb + raise UnsupportedDeviceException("Unknown device type: %s" % type_) - raise SmartDeviceException("Unknown device type: %s" % type_) + @staticmethod + def _get_device_instance_legacy(data: bytes, ip: str, port: int) -> SmartDevice: + """Get SmartDevice for device described by passed data from legacy 9999 response.""" + try: + info = json_loads(TPLinkSmartHomeProtocol.decrypt(data)) + except Exception as ex: + raise SmartDeviceException( + f"Unable to read response from device: {ip}: {ex}" + ) + + _LOGGER.debug("[DISCOVERY] %s << %s", ip, info) + + device_class = Discover._get_device_class(info) + device = device_class(ip, port=port) + device.update_from_discover_info(info) + return device @staticmethod - def _is_klap_discovery_info(info: dict) -> bool: - return ( + def _get_device_instance( + data: bytes, ip: str, port: int, credentials: Credentials + ) -> SmartDevice: + """Get SmartDevice for device described by passed data from new 20002 response.""" + try: + info = json_loads(data[16:]) + except Exception as ex: + raise SmartDeviceException( + f"Unable to read response from device: {ip}: {ex}" + ) + + if ( "result" in info and "mgt_encrypt_schm" in info["result"] and "encrypt_type" in info["result"]["mgt_encrypt_schm"] and info["result"]["mgt_encrypt_schm"]["encrypt_type"] == "KLAP" and "lv" not in info["result"]["mgt_encrypt_schm"] - ) + and "device_type" in info["result"] + ): + type_ = info["result"]["device_type"] + device_class = None + if type_.upper() == "IOT.SMARTPLUGSWITCH": + device_class = SmartPlug + + if device_class: + _LOGGER.debug("[DISCOVERY] %s << %s", ip, info) + device = device_class(ip, port=port, credentials=credentials) + device.protocol = TPLinkKlap(ip, credentials, info) + return device + else: + raise UnsupportedDeviceException( + f"Unsupported device {ip} of type {type_}: {info}" + ) + else: + raise UnsupportedDeviceException(f"Unsupported device {ip}: {info}") diff --git a/kasa/klapprotocol.py b/kasa/klapprotocol.py index 18ec640a5..2d8bdac48 100755 --- a/kasa/klapprotocol.py +++ b/kasa/klapprotocol.py @@ -57,11 +57,19 @@ class TPLinkKlap(TPLinkProtocol): KASA_SETUP_PASSWORD = "kasaSetup" def __init__( - self, host: str, credentials: Credentials = Credentials() + self, + host: str, + credentials: Credentials = Credentials(), + discovery_data: Dict = {}, ) -> None: super().__init__(host=host, port=self.DEFAULT_PORT) - self.credentials = credentials if credentials.username is not None and credentials.password is not None else Credentials(username="",password="") + self.credentials = ( + credentials + if credentials.username is not None and credentials.password is not None + else Credentials(username="", password="") + ) + self.discovery_data = discovery_data self.jar = aiohttp.CookieJar(unsafe=True, quote_cookie=False) self._local_seed: Optional[bytes] = None @@ -160,8 +168,7 @@ async def perform_handshake1( if response_status != 200: raise AuthenticationException( - "Device %s responded with %d to handshake1, this is probably not a klap device" - % (self.host, response_status) + f"Device {self.host} responded with {response_status} to handshake1 {self.discovery_data}" ) self.handle_cookies(session, url) @@ -218,9 +225,7 @@ async def perform_handshake1( return remote_seed, kasa_setup_auth_hash else: self.authentication_failed = True - msg = "Server response doesn't match our challenge on ip {}".format( - self.host - ) + msg = f"Server response doesn't match our challenge on ip {self.host} {self.discovery_data}" _LOGGER.debug(msg) raise AuthenticationException(msg) @@ -246,7 +251,7 @@ async def perform_handshake2(self, session, remote_seed, auth_hash) -> None: self.authentication_failed = True self.handshake_done = False raise AuthenticationException( - "Device responded with %d to handshake2" % response_status + f"Device {self.host} responded with {response_status} to handshake2 {self.discovery_data}" ) else: self.authentication_failed = False @@ -270,17 +275,19 @@ async def perform_handshake( _LOGGER.debug("[KLAP] Handshake with %s complete", self.host) @staticmethod - def generate_auth_hash(auth: Credentials): + def generate_auth_hash(creds: Credentials): """Generate an md5 auth hash for the protocol on the supplied credentials.""" + un = creds.username or "" + pw = creds.password or "" return TPLinkKlap._md5( - TPLinkKlap._md5(auth.username.encode()) - + TPLinkKlap._md5(auth.password.encode()) + TPLinkKlap._md5(un.encode()) + TPLinkKlap._md5(pw.encode()) ) @staticmethod - def generate_owner_hash(auth: Credentials): + def generate_owner_hash(creds: Credentials): """Return the MD5 hash of the username in this object.""" - return TPLinkKlap._md5(auth.username.encode()) + un = creds.username or "" + return TPLinkKlap._md5(un.encode()) async def query(self, request: Union[str, Dict], retry_count: int = 3) -> Dict: """Query the device retrying for retry_count on failure.""" @@ -354,7 +361,7 @@ async def _execute_query(self, request: str, retry_count: int) -> Dict: self.handshake_done = False self.authentication_failed = True raise AuthenticationException( - "Got a security error after handshake completed" + f"Got a security error from {self.host} after handshake completed {self.discovery_data}" ) else: raise SmartDeviceException( diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index 0fb4e60a9..3dbc1fbac 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -205,7 +205,7 @@ def __init__( """ self.host = host self.port = port - self.protocol = TPLinkSmartHomeProtocol(host, port=port) + self.protocol: TPLinkProtocol = TPLinkSmartHomeProtocol(host, port=port) self.credentials = credentials _LOGGER.debug("Initializing %s of type %s", self.host, type(self)) self._device_type = DeviceType.Unknown @@ -218,10 +218,6 @@ def __init__( self.children: List["SmartDevice"] = [] - def override_protocol(self, protocol: TPLinkProtocol): - """Override the default protocol for device communication.""" - self.protocol = protocol - def add_module(self, name: str, module: Module): """Register a module.""" if name in self.modules: diff --git a/kasa/tests/test_discovery.py b/kasa/tests/test_discovery.py index f6a871f59..1ff86e919 100644 --- a/kasa/tests/test_discovery.py +++ b/kasa/tests/test_discovery.py @@ -50,7 +50,7 @@ async def test_type_detection_lightstrip(dev: SmartDevice): async def test_type_unknown(): invalid_info = {"system": {"get_sysinfo": {"type": "nosuchtype"}}} - with pytest.raises(SmartDeviceException): + with pytest.raises(UnsupportedDeviceException): Discover._get_device_class(invalid_info) @@ -170,9 +170,9 @@ async def test_discover_datagram_received(mocker, discovery_data): mocker.patch.object(protocol.TPLinkSmartHomeProtocol, "decrypt") addr = "127.0.0.1" + proto.datagram_received("", (addr, 9999)) addr2 = "127.0.0.2" - await proto.datagram_received_async("", (addr, 9999)) - await proto.datagram_received_async("", (addr2, 20002)) + proto.datagram_received("", (addr2, 20002)) # Check that device in discovered_devices is initialized correctly assert len(proto.discovered_devices) == 1 From d759c8217286defc2fb41d887d42ca1ac2efca34 Mon Sep 17 00:00:00 2001 From: sdb9696 Date: Wed, 6 Sep 2023 17:50:00 +0100 Subject: [PATCH 5/5] Set basic info without update() for better HA integration --- kasa/discover.py | 6 ++++++ kasa/smartdevice.py | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/kasa/discover.py b/kasa/discover.py index 8a3925c2b..0943c2915 100755 --- a/kasa/discover.py +++ b/kasa/discover.py @@ -359,6 +359,12 @@ def _get_device_instance( if device_class: _LOGGER.debug("[DISCOVERY] %s << %s", ip, info) device = device_class(ip, port=port, credentials=credentials) + # Set the MAC so HA can add the device and try authentication later + device._requires_update_overrides = { + "mac": info["result"].get("mac"), + "alias": ip, + "model": info["result"].get("device_model"), + } device.protocol = TPLinkKlap(ip, credentials, info) return device else: diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index 3dbc1fbac..0773254a6 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -74,6 +74,8 @@ def requires_update(f): @functools.wraps(f) async def wrapped(*args, **kwargs): self = args[0] + if f.__name__ in self._requires_update_overrides: + return self._requires_update_overrides.get(f.__name__) if self._last_update is None: raise SmartDeviceException( "You need to await update() to access the data" @@ -85,6 +87,8 @@ async def wrapped(*args, **kwargs): @functools.wraps(f) def wrapped(*args, **kwargs): self = args[0] + if f.__name__ in self._requires_update_overrides: + return self._requires_update_overrides.get(f.__name__) if self._last_update is None: raise SmartDeviceException( "You need to await update() to access the data" @@ -213,6 +217,11 @@ def __init__( # accessors. the @updated_required decorator does not ensure mypy that these # are not accessed incorrectly. self._last_update: Any = None + + # Homeassistant uses the mac for storing devices and alias for display so this property allows updating of + # some properties from the new discovery info even if the device can't authenticate + self._requires_update_overrides: dict = {} + self._sys_info: Any = None # TODO: this is here to avoid changing tests self.modules: Dict[str, Any] = {} @@ -323,6 +332,7 @@ async def update(self, update_children: bool = True): if self._last_update is None: _LOGGER.debug("Performing the initial update to obtain sysinfo") self._last_update = await self.protocol.query(req) + self._requires_update_overrides = {} self._sys_info = self._last_update["system"]["get_sysinfo"] await self._modular_update(req) @@ -461,7 +471,6 @@ def mac(self) -> str: :return: mac address in hexadecimal with colons, e.g. 01:23:45:67:89:ab """ sys_info = self.sys_info - mac = sys_info.get("mac", sys_info.get("mic_mac")) if not mac: raise SmartDeviceException(