Add mypy checks

This commit is contained in:
Andre Basche 2023-04-15 15:55:22 +02:00
parent b6ca12ebff
commit f54b7b2dbf
6 changed files with 73 additions and 63 deletions

View File

@ -25,12 +25,16 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install -r requirements.txt python -m pip install -r requirements.txt
python -m pip install flake8 pylint black python -m pip install flake8 pylint black mypy
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
- name: Type check with mypy
run: |
# stop the build if there are Python syntax errors or undefined names
mypy pyhon/
# - name: Analysing the code with pylint # - name: Analysing the code with pylint
# run: | # run: |
# pylint --max-line-length 88 $(git ls-files '*.py') # pylint --max-line-length 88 $(git ls-files '*.py')

View File

@ -1,23 +1,29 @@
import importlib import importlib
from contextlib import suppress from contextlib import suppress
from typing import Optional, Dict from typing import Optional, Dict, Any
from typing import TYPE_CHECKING
from pyhon import helper from pyhon import helper
from pyhon.commands import HonCommand from pyhon.commands import HonCommand
from pyhon.parameter import HonParameterFixed from pyhon.parameter import HonParameterFixed
if TYPE_CHECKING:
from pyhon import HonAPI
class HonAppliance: class HonAppliance:
def __init__(self, api, info: Dict, zone: int = 0) -> None: def __init__(
self, api: Optional["HonAPI"], info: Dict[str, Any], zone: int = 0
) -> None:
if attributes := info.get("attributes"): if attributes := info.get("attributes"):
info["attributes"] = {v["parName"]: v["parValue"] for v in attributes} info["attributes"] = {v["parName"]: v["parValue"] for v in attributes}
self._info = info self._info: Dict = info
self._api = api self._api: Optional[HonAPI] = api
self._appliance_model = {} self._appliance_model: Dict = {}
self._commands = {} self._commands: Dict = {}
self._statistics = {} self._statistics: Dict = {}
self._attributes = {} self._attributes: Dict = {}
self._zone = zone self._zone = zone
try: try:
@ -58,11 +64,11 @@ class HonAppliance:
@property @property
def appliance_model_id(self) -> str: def appliance_model_id(self) -> str:
return self._info.get("applianceModelId") return self._info.get("applianceModelId", "")
@property @property
def appliance_type(self) -> str: def appliance_type(self) -> str:
return self._info.get("applianceTypeName") return self._info.get("applianceTypeName", "")
@property @property
def mac_address(self) -> str: def mac_address(self) -> str:

View File

@ -3,10 +3,11 @@ import logging
import re import re
import secrets import secrets
import urllib import urllib
from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pprint import pformat from pprint import pformat
from typing import Dict, Optional from typing import Dict, Optional, List
from urllib import parse from urllib import parse
from urllib.parse import quote from urllib.parse import quote
@ -82,14 +83,15 @@ class HonAuth:
if fail: if fail:
raise exceptions.HonAuthenticationError("Can't login") raise exceptions.HonAuthenticationError("Can't login")
def _generate_nonce(self) -> str: @staticmethod
def _generate_nonce() -> str:
nonce = secrets.token_hex(16) nonce = secrets.token_hex(16)
return f"{nonce[:8]}-{nonce[8:12]}-{nonce[12:16]}-{nonce[16:20]}-{nonce[20:]}" return f"{nonce[:8]}-{nonce[8:12]}-{nonce[12:16]}-{nonce[16:20]}-{nonce[20:]}"
async def _load_login(self): async def _load_login(self) -> bool:
login_url = await self._introduce() login_url = await self._introduce()
login_url = await self._handle_redirects(login_url) login_url = await self._handle_redirects(login_url)
await self._login_url(login_url) return await self._login_url(login_url)
async def _introduce(self) -> str: async def _introduce(self) -> str:
redirect_uri = urllib.parse.quote(f"{const.APP}://mobilesdk/detect/oauth/done") redirect_uri = urllib.parse.quote(f"{const.APP}://mobilesdk/detect/oauth/done")
@ -101,8 +103,8 @@ class HonAuth:
"scope": "api openid refresh_token web", "scope": "api openid refresh_token web",
"nonce": self._generate_nonce(), "nonce": self._generate_nonce(),
} }
params = "&".join([f"{k}={v}" for k, v in params.items()]) params_encode = "&".join([f"{k}={v}" for k, v in params.items()])
url = f"{const.AUTH_API}/services/oauth2/authorize/expid_Login?{params}" url = f"{const.AUTH_API}/services/oauth2/authorize/expid_Login?{params_encode}"
async with self._request.get(url) as response: async with self._request.get(url) as response:
text = await response.text() text = await response.text()
self._expires = datetime.utcnow() self._expires = datetime.utcnow()
@ -115,7 +117,7 @@ class HonAuth:
async def _manual_redirect(self, url: str) -> str: async def _manual_redirect(self, url: str) -> str:
async with self._request.get(url, allow_redirects=False) as response: async with self._request.get(url, allow_redirects=False) as response:
if not (new_location := response.headers.get("Location")): if not (new_location := response.headers.get("Location", "")):
await self._error_logger(response) await self._error_logger(response)
return new_location return new_location
@ -138,11 +140,11 @@ class HonAuth:
) )
return True return True
await self._error_logger(response) await self._error_logger(response)
return False
async def _login(self): async def _login(self) -> str:
start_url = parse.unquote(self._login_data.url.split("startURL=")[-1]).split( start_url = self._login_data.url.rsplit("startURL=", maxsplit=1)[-1]
"%3D" start_url = parse.unquote(start_url).split("%3D")[0]
)[0]
action = { action = {
"id": "79;a", "id": "79;a",
"descriptor": "apex://LightningLoginCustomController/ACTION$login", "descriptor": "apex://LightningLoginCustomController/ACTION$login",
@ -175,19 +177,13 @@ class HonAuth:
params=params, params=params,
) as response: ) as response:
if response.status == 200: if response.status == 200:
try: with suppress(json.JSONDecodeError, KeyError):
data = await response.json() result = await response.json()
return data["events"][0]["attributes"]["values"]["url"] return result["events"][0]["attributes"]["values"]["url"]
except json.JSONDecodeError:
pass
except KeyError:
_LOGGER.error(
"Can't get login url - %s", pformat(await response.json())
)
await self._error_logger(response) await self._error_logger(response)
return "" return ""
def _parse_token_data(self, text): def _parse_token_data(self, text: str) -> None:
if access_token := re.findall("access_token=(.*?)&", text): if access_token := re.findall("access_token=(.*?)&", text):
self._access_token = access_token[0] self._access_token = access_token[0]
if refresh_token := re.findall("refresh_token=(.*?)&", text): if refresh_token := re.findall("refresh_token=(.*?)&", text):
@ -195,22 +191,26 @@ class HonAuth:
if id_token := re.findall("id_token=(.*?)&", text): if id_token := re.findall("id_token=(.*?)&", text):
self._id_token = id_token[0] self._id_token = id_token[0]
async def _get_token(self, url): async def _get_token(self, url: str) -> bool:
async with self._request.get(url) as response: async with self._request.get(url) as response:
if response.status != 200: if response.status != 200:
await self._error_logger(response) await self._error_logger(response)
return False return False
url = re.findall("href\\s*=\\s*[\"'](.+?)[\"']", await response.text()) url_search = re.findall(
if not url: "href\\s*=\\s*[\"'](.+?)[\"']", await response.text()
)
if not url_search:
await self._error_logger(response) await self._error_logger(response)
return False return False
if "ProgressiveLogin" in url[0]: if "ProgressiveLogin" in url_search[0]:
async with self._request.get(url[0]) as response: async with self._request.get(url_search[0]) as response:
if response.status != 200: if response.status != 200:
await self._error_logger(response) await self._error_logger(response)
return False return False
url = re.findall("href\\s*=\\s*[\"'](.*?)[\"']", await response.text()) url_search = re.findall(
url = "/".join(const.AUTH_API.split("/")[:-1]) + url[0] "href\\s*=\\s*[\"'](.*?)[\"']", await response.text()
)
url = "/".join(const.AUTH_API.split("/")[:-1]) + url_search[0]
async with self._request.get(url) as response: async with self._request.get(url) as response:
if response.status != 200: if response.status != 200:
await self._error_logger(response) await self._error_logger(response)
@ -218,7 +218,7 @@ class HonAuth:
self._parse_token_data(await response.text()) self._parse_token_data(await response.text())
return True return True
async def _api_auth(self): async def _api_auth(self) -> bool:
post_headers = {"id-token": self._id_token} post_headers = {"id-token": self._id_token}
data = self._device.get() data = self._device.get()
async with self._request.post( async with self._request.post(
@ -232,7 +232,7 @@ class HonAuth:
self._cognito_token = json_data["cognitoUser"]["Token"] self._cognito_token = json_data["cognitoUser"]["Token"]
return True return True
async def authenticate(self): async def authenticate(self) -> None:
self.clear() self.clear()
try: try:
if not await self._load_login(): if not await self._load_login():
@ -246,7 +246,7 @@ class HonAuth:
except exceptions.HonNoAuthenticationNeeded: except exceptions.HonNoAuthenticationNeeded:
return return
async def refresh(self): async def refresh(self) -> bool:
params = { params = {
"client_id": const.CLIENT_ID, "client_id": const.CLIENT_ID,
"refresh_token": self._refresh_token, "refresh_token": self._refresh_token,
@ -264,7 +264,7 @@ class HonAuth:
self._access_token = data["access_token"] self._access_token = data["access_token"]
return await self._api_auth() return await self._api_auth()
def clear(self): def clear(self) -> None:
self._session.cookie_jar.clear_domain(const.AUTH_API.split("/")[-2]) self._session.cookie_jar.clear_domain(const.AUTH_API.split("/")[-2])
self._request.called_urls = [] self._request.called_urls = []
self._cognito_token = "" self._cognito_token = ""

View File

@ -1,7 +1,7 @@
import logging import logging
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Optional, Callable, Dict from typing import Optional, Callable, Dict, Any
import aiohttp import aiohttp
from typing_extensions import Self from typing_extensions import Self
@ -37,18 +37,18 @@ class ConnectionHandler:
raise NotImplementedError raise NotImplementedError
@asynccontextmanager @asynccontextmanager
async def get(self, *args, **kwargs) -> AsyncIterator[Callable]: async def get(self, *args, **kwargs) -> AsyncIterator[aiohttp.ClientResponse]:
if self._session is None: if self._session is None:
raise exceptions.NoSessionException() raise exceptions.NoSessionException()
response: Callable response: aiohttp.ClientResponse
async with self._intercept(self._session.get, *args, **kwargs) as response: async with self._intercept(self._session.get, *args, **kwargs) as response:
yield response yield response
@asynccontextmanager @asynccontextmanager
async def post(self, *args, **kwargs) -> AsyncIterator[Callable]: async def post(self, *args, **kwargs) -> AsyncIterator[aiohttp.ClientResponse]:
if self._session is None: if self._session is None:
raise exceptions.NoSessionException() raise exceptions.NoSessionException()
response: Callable response: aiohttp.ClientResponse
async with self._intercept(self._session.post, *args, **kwargs) as response: async with self._intercept(self._session.post, *args, **kwargs) as response:
yield response yield response

View File

@ -10,7 +10,7 @@ from typing_extensions import Self
from pyhon.connection.auth import HonAuth from pyhon.connection.auth import HonAuth
from pyhon.connection.device import HonDevice from pyhon.connection.device import HonDevice
from pyhon.connection.handler.base import ConnectionHandler from pyhon.connection.handler.base import ConnectionHandler
from pyhon.exceptions import HonAuthenticationError from pyhon.exceptions import HonAuthenticationError, NoAuthenticationException
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,7 +30,9 @@ class HonConnectionHandler(ConnectionHandler):
self._auth: Optional[HonAuth] = None self._auth: Optional[HonAuth] = None
@property @property
def auth(self) -> Optional[HonAuth]: def auth(self) -> HonAuth:
if self._auth is None:
raise NoAuthenticationException()
return self._auth return self._auth
@property @property
@ -39,16 +41,14 @@ class HonConnectionHandler(ConnectionHandler):
async def create(self) -> Self: async def create(self) -> Self:
await super().create() await super().create()
self._auth: HonAuth = HonAuth( self._auth = HonAuth(self._session, self._email, self._password, self._device)
self._session, self._email, self._password, self._device
)
return self return self
async def _check_headers(self, headers: Dict) -> Dict: async def _check_headers(self, headers: Dict) -> Dict:
if not (self._auth.cognito_token and self._auth.id_token): if not (self.auth.cognito_token and self.auth.id_token):
await self._auth.authenticate() await self.auth.authenticate()
headers["cognito-token"] = self._auth.cognito_token headers["cognito-token"] = self.auth.cognito_token
headers["id-token"] = self._auth.id_token headers["id-token"] = self.auth.id_token
return self._HEADERS | headers return self._HEADERS | headers
@asynccontextmanager @asynccontextmanager
@ -58,16 +58,16 @@ class HonConnectionHandler(ConnectionHandler):
kwargs["headers"] = await self._check_headers(kwargs.get("headers", {})) kwargs["headers"] = await self._check_headers(kwargs.get("headers", {}))
async with method(*args, **kwargs) as response: async with method(*args, **kwargs) as response:
if ( if (
self._auth.token_expires_soon or response.status in [401, 403] self.auth.token_expires_soon or response.status in [401, 403]
) and loop == 0: ) and loop == 0:
_LOGGER.info("Try refreshing token...") _LOGGER.info("Try refreshing token...")
await self._auth.refresh() await self.auth.refresh()
async with self._intercept( async with self._intercept(
method, *args, loop=loop + 1, **kwargs method, *args, loop=loop + 1, **kwargs
) as result: ) as result:
yield result yield result
elif ( elif (
self._auth.token_is_expired or response.status in [401, 403] self.auth.token_is_expired or response.status in [401, 403]
) and loop == 1: ) and loop == 1:
_LOGGER.warning( _LOGGER.warning(
"%s - Error %s - %s", "%s - Error %s - %s",

View File

@ -1,5 +1,5 @@
import asyncio import asyncio
from typing import List, Optional, Dict from typing import List, Optional, Dict, Any
from typing_extensions import Self from typing_extensions import Self
from aiohttp import ClientSession from aiohttp import ClientSession
@ -39,8 +39,8 @@ class Hon:
def appliances(self) -> List[HonAppliance]: def appliances(self) -> List[HonAppliance]:
return self._appliances return self._appliances
async def _create_appliance(self, appliance: Dict, zone=0) -> None: async def _create_appliance(self, appliance_data: Dict[str, Any], zone=0) -> None:
appliance = HonAppliance(self._api, appliance, zone=zone) appliance = HonAppliance(self._api, appliance_data, zone=zone)
if appliance.mac_address is None: if appliance.mac_address is None:
return return
await asyncio.gather( await asyncio.gather(