Add mypy checks
This commit is contained in:
parent
b6ca12ebff
commit
f54b7b2dbf
6
.github/workflows/python-check.yml
vendored
6
.github/workflows/python-check.yml
vendored
@ -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')
|
||||||
|
@ -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:
|
||||||
|
@ -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 = ""
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user