diff --git a/hass_nabucasa/__init__.py b/hass_nabucasa/__init__.py index c844bf45b..bbc224315 100644 --- a/hass_nabucasa/__init__.py +++ b/hass_nabucasa/__init__.py @@ -3,16 +3,15 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Callable, Mapping from datetime import datetime, timedelta import json import logging -import os from pathlib import Path import shutil from typing import Any, Generic, Literal, TypeVar -from collections.abc import Awaitable, Callable, Mapping -import aiohttp +from aiohttp import ClientSession from atomicwrites import atomic_write from jose import jwt @@ -21,9 +20,9 @@ from .cloudhooks import Cloudhooks from .const import ( CONFIG_DIR, - MODE_DEV, DEFAULT_SERVERS, DEFAULT_VALUES, + MODE_DEV, STATE_CONNECTED, ) from .google_report_state import GoogleReportState @@ -127,7 +126,7 @@ def is_connected(self) -> bool: return self.iot.state == STATE_CONNECTED @property - def websession(self) -> aiohttp.ClientSession: + def websession(self) -> ClientSession: """Return websession for connections.""" return self.client.websession @@ -141,7 +140,7 @@ def expiration_date(self) -> datetime: """Return the subscription expiration as a UTC datetime object.""" if (parsed_date := parse_date(self.claims["custom:sub-exp"])) is None: raise ValueError( - f"Invalid expiration date ({self.claims['custom:sub-exp']})" + f"Invalid expiration date ({self.claims['custom:sub-exp']})", ) return datetime.combine(parsed_date, datetime.min.time()).replace(tzinfo=UTC) @@ -161,7 +160,10 @@ def user_info_path(self) -> Path: return self.path(f"{self.mode}_auth.json") async def update_token( - self, id_token: str, access_token: str, refresh_token: str | None = None + self, + id_token: str, + access_token: str, + refresh_token: str | None = None, ) -> asyncio.Task | None: """Update the id and access token.""" self.id_token = id_token @@ -185,7 +187,8 @@ async def update_token( return None def register_on_initialized( - self, on_initialized_cb: Callable[[], Awaitable[None]] + self, + on_initialized_cb: Callable[[], Awaitable[None]], ) -> None: """Register an async on_initialized callback. @@ -249,7 +252,7 @@ def _remove_data(self) -> None: base_path = self.path() # Recursively remove .cloud - if os.path.isdir(base_path): + if base_path.is_dir(): shutil.rmtree(base_path) # Guard against .cloud not being a directory @@ -271,7 +274,7 @@ def _write_user_info(self) -> None: "refresh_token": self.refresh_token, }, indent=4, - ) + ), ) self.user_info_path.chmod(0o600) @@ -287,11 +290,11 @@ def load_config() -> None | dict[str, Any]: if not self.user_info_path.exists(): return None + try: content: dict[str, Any] = json.loads( - self.user_info_path.read_text(encoding="utf-8") + self.user_info_path.read_text(encoding="utf-8"), ) - return content except (ValueError, OSError) as err: path = self.user_info_path.relative_to(self.client.base_path) self.client.user_message( @@ -300,10 +303,14 @@ def load_config() -> None | dict[str, Any]: f"Unable to load authentication from {path}. [Please login again](/config/cloud)", ) _LOGGER.warning( - "Error loading cloud authentication info from %s: %s", path, err + "Error loading cloud authentication info from %s: %s", + path, + err, ) return None + return content + info = await self.run_executor(load_config) if info is None: # No previous token data diff --git a/hass_nabucasa/account_link.py b/hass_nabucasa/account_link.py index 751ca98ee..637d4e10d 100644 --- a/hass_nabucasa/account_link.py +++ b/hass_nabucasa/account_link.py @@ -63,7 +63,7 @@ async def async_get_authorize_url(self) -> str: _LOGGER.debug("Opening connection for %s", self.service) self._client = await self.cloud.client.websession.ws_connect( - f"https://{self.cloud.account_link_server}/v1" + f"https://{self.cloud.account_link_server}/v1", ) await self._client.send_json({"service": self.service}) @@ -101,7 +101,7 @@ async def _get_response(self) -> dict[str, Any]: if "error" in response: if response["error"] == ERR_TIMEOUT: - raise TimeoutError() + raise TimeoutError raise AccountLinkException(response["error"]) @@ -131,7 +131,7 @@ async def async_fetch_available_services( """Fetch available services.""" resp = await cloud.client.websession.get( - f"https://{cloud.account_link_server}/services" + f"https://{cloud.account_link_server}/services", ) resp.raise_for_status() content: list[dict[str, Any]] = await resp.json() diff --git a/hass_nabucasa/acme.py b/hass_nabucasa/acme.py index e787549f8..d03b18e09 100644 --- a/hass_nabucasa/acme.py +++ b/hass_nabucasa/acme.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING import urllib -import OpenSSL from acme import challenges, client, crypto_util, errors, messages import async_timeout from atomicwrites import atomic_write @@ -18,9 +17,10 @@ from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.x509.oid import NameOID from cryptography.x509.extensions import SubjectAlternativeName +from cryptography.x509.oid import NameOID import josepy as jose +import OpenSSL from . import cloud_api @@ -134,7 +134,7 @@ def common_name(self) -> str | None: if not self._x509: return None return str( - self._x509.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value + self._x509.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value, ) @property @@ -144,7 +144,7 @@ def alternative_names(self) -> list[str] | None: return None alternative_names = self._x509.extensions.get_extension_for_class( - SubjectAlternativeName + SubjectAlternativeName, ).value return [str(entry.value) for entry in alternative_names] @@ -182,7 +182,8 @@ def _load_account_key(self) -> None: else: _LOGGER.debug("Create new RSA keyfile: %s", self.path_account_key) key = rsa.generate_private_key( - public_exponent=65537, key_size=ACCOUNT_KEY_SIZE + public_exponent=65537, + key_size=ACCOUNT_KEY_SIZE, ) # Store it to file @@ -203,7 +204,7 @@ def _create_client(self) -> None: if self.path_registration_info.exists(): _LOGGER.info("Load exists ACME registration") regr = messages.RegistrationResource.json_loads( - self.path_registration_info.read_text(encoding="utf-8") + self.path_registration_info.read_text(encoding="utf-8"), ) acme_url = urllib.parse.urlparse(self._acme_server) @@ -255,15 +256,17 @@ def _create_client(self) -> None: ) regr = self._acme_client.new_account( messages.NewRegistration.from_data( - email=self._email, terms_of_service_agreed=True - ) + email=self._email, + terms_of_service_agreed=True, + ), ) except errors.Error as err: raise AcmeClientError(f"Can't register to ACME server: {err}") from err # Store registration info self.path_registration_info.write_text( - regr.json_dumps_pretty(), encoding="utf-8" + regr.json_dumps_pretty(), + encoding="utf-8", ) self.path_registration_info.chmod(0o600) @@ -280,10 +283,10 @@ def _create_order(self, csr_pem: bytes) -> messages.OrderResource: and err.detail == "JWS verification error" ): raise AcmeJWSVerificationError( - f"JWS verification failed: {err}" + f"JWS verification failed: {err}", ) from None raise AcmeChallengeError( - f"Can't order a new ACME challenge: {err}" + f"Can't order a new ACME challenge: {err}", ) from None def _start_challenge(self, order: messages.OrderResource) -> list[ChallengeHandler]: @@ -307,14 +310,14 @@ def _start_challenge(self, order: messages.OrderResource) -> list[ChallengeHandl for dns_challenge in dns_challenges: try: response, validation = dns_challenge.response_and_validation( - self._account_jwk + self._account_jwk, ) except errors.Error as err: raise AcmeChallengeError( - f"Can't validate the new ACME challenge: {err}" + f"Can't validate the new ACME challenge: {err}", ) from None handlers.append( - ChallengeHandler(dns_challenge, order, response, validation) + ChallengeHandler(dns_challenge, order, response, validation), ) return handlers @@ -338,14 +341,16 @@ def _finish_challenge(self, order: messages.OrderResource) -> None: try: order = self._acme_client.poll_authorizations(order, deadline) order = self._acme_client.finalize_order( - order, deadline, fetch_alternative_chains=True + order, + deadline, + fetch_alternative_chains=True, ) except errors.Error as err: raise AcmeChallengeError(f"Wait of ACME challenge fails: {err}") from err except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception while finalizing order") raise AcmeChallengeError( - "Unexpected exception while finalizing order" + "Unexpected exception while finalizing order", ) from None # Cleanup the old stuff @@ -385,8 +390,9 @@ def _revoke_certificate(self) -> None: fullchain = jose.ComparableX509( OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, self.path_fullchain.read_bytes() - ) + OpenSSL.crypto.FILETYPE_PEM, + self.path_fullchain.read_bytes(), + ), ) _LOGGER.info("Revoke certificate") @@ -397,10 +403,10 @@ def _revoke_certificate(self) -> None: pass except errors.Error as err: # Ignore errors where certificate did not exist - if "No such certificate" in str(err): + if "No such certificate" in str(err): # noqa: SIM114 pass # Ignore errors where certificate has expired - elif "Certificate is expired" in str(err): + elif "Certificate is expired" in str(err): # noqa: SIM114 pass # Ignore errors where unrecognized issuer (happens dev/prod switch) elif "Certificate from unrecognized issuer" in str(err): @@ -415,7 +421,7 @@ def _deactivate_account(self) -> None: _LOGGER.info("Load exists ACME registration") regr = messages.RegistrationResource.json_loads( - self.path_registration_info.read_text(encoding="utf-8") + self.path_registration_info.read_text(encoding="utf-8"), ) try: @@ -446,7 +452,8 @@ async def issue_certificate(self) -> None: csr = await self.cloud.run_executor(self._generate_csr) order = await self.cloud.run_executor(self._create_order, csr) dns_challenges: list[ChallengeHandler] = await self.cloud.run_executor( - self._start_challenge, order + self._start_challenge, + order, ) try: @@ -455,18 +462,19 @@ async def issue_certificate(self) -> None: try: async with async_timeout.timeout(30): resp = await cloud_api.async_remote_challenge_txt( - self.cloud, challenge.validation + self.cloud, + challenge.validation, ) assert resp.status in (200, 201) except (TimeoutError, AssertionError): raise AcmeNabuCasaError( - "Can't set challenge token to NabuCasa DNS!" + "Can't set challenge token to NabuCasa DNS!", ) from None # Answer challenge try: _LOGGER.info( - "Waiting 60 seconds for publishing DNS to ACME provider" + "Waiting 60 seconds for publishing DNS to ACME provider", ) await asyncio.sleep(60) await self.cloud.run_executor(self._answer_challenge, challenge) @@ -483,7 +491,8 @@ async def issue_certificate(self) -> None: async with async_timeout.timeout(30): # We only need to cleanup for the last entry await cloud_api.async_remote_challenge_cleanup( - self.cloud, dns_challenges[-1].validation + self.cloud, + dns_challenges[-1].validation, ) except TimeoutError: _LOGGER.error("Failed to clean up challenge from NabuCasa DNS!") diff --git a/hass_nabucasa/auth.py b/hass_nabucasa/auth.py index fc820a285..780e239cd 100644 --- a/hass_nabucasa/auth.py +++ b/hass_nabucasa/auth.py @@ -120,13 +120,13 @@ async def async_register( email, password, client_metadata=client_metadata, - ) + ), ) except ClientError as err: raise _map_aws_exception(err) from err except BotoCoreError as err: - raise UnknownError() from err + raise UnknownError from err async def async_resend_email_confirm(self, email: str) -> None: """Resend email confirmation.""" @@ -139,12 +139,12 @@ async def async_resend_email_confirm(self, email: str) -> None: cognito.client.resend_confirmation_code, Username=email, ClientId=cognito.client_id, - ) + ), ) except ClientError as err: raise _map_aws_exception(err) from err except BotoCoreError as err: - raise UnknownError() from err + raise UnknownError from err async def async_forgot_password(self, email: str) -> None: """Initialize forgotten password flow.""" @@ -157,7 +157,7 @@ async def async_forgot_password(self, email: str) -> None: except ClientError as err: raise _map_aws_exception(err) from err except BotoCoreError as err: - raise UnknownError() from err + raise UnknownError from err async def async_login(self, email: str, password: str) -> None: """Log user in and fetch certificate.""" @@ -170,24 +170,26 @@ async def async_login(self, email: str, password: str) -> None: async with async_timeout.timeout(30): await self.cloud.run_executor( - partial(cognito.authenticate, password=password) + partial(cognito.authenticate, password=password), ) task = await self.cloud.update_token( - cognito.id_token, cognito.access_token, cognito.refresh_token + cognito.id_token, + cognito.access_token, + cognito.refresh_token, ) if task: await task except ForceChangePasswordException as err: - raise PasswordChangeRequired() from err + raise PasswordChangeRequired from err except ClientError as err: raise _map_aws_exception(err) from err except BotoCoreError as err: - raise UnknownError() from err + raise UnknownError from err async def async_check_token(self) -> None: """Check that the token is valid and renew if necessary.""" @@ -230,7 +232,7 @@ async def _async_renew_access_token(self) -> None: raise _map_aws_exception(err) from err except BotoCoreError as err: - raise UnknownError() from err + raise UnknownError from err @property def _authenticated_cognito(self) -> pycognito.Cognito: @@ -239,7 +241,8 @@ def _authenticated_cognito(self) -> pycognito.Cognito: raise Unauthenticated("No authentication found") return self._cognito( - access_token=self.cloud.access_token, refresh_token=self.cloud.refresh_token + access_token=self.cloud.access_token, + refresh_token=self.cloud.refresh_token, ) def _cognito(self, **kwargs: Any) -> pycognito.Cognito: diff --git a/hass_nabucasa/client.py b/hass_nabucasa/client.py index 6d058d76b..b658b3117 100644 --- a/hass_nabucasa/client.py +++ b/hass_nabucasa/client.py @@ -3,12 +3,12 @@ from __future__ import annotations from abc import ABC, abstractmethod -import asyncio +from asyncio import AbstractEventLoop from pathlib import Path from typing import TYPE_CHECKING, Any, Literal -import aiohttp -from aiohttp import web +from aiohttp import ClientSession +from aiohttp.web import AppRunner from .iot import HandlerError @@ -36,12 +36,12 @@ def base_path(self) -> Path: @property @abstractmethod - def loop(self) -> asyncio.AbstractEventLoop: + def loop(self) -> AbstractEventLoop: """Return client loop.""" @property @abstractmethod - def websession(self) -> aiohttp.ClientSession: + def websession(self) -> ClientSession: """Return client session for aiohttp.""" @property @@ -51,7 +51,7 @@ def client_name(self) -> str: @property @abstractmethod - def aiohttp_runner(self) -> web.AppRunner | None: + def aiohttp_runner(self) -> AppRunner | None: """Return client webinterface aiohttp application.""" @property @@ -94,7 +94,8 @@ async def async_cloud_connect_update(self, connect: bool) -> None: @abstractmethod async def async_cloud_connection_info( - self, payload: dict[str, Any] + self, + payload: dict[str, Any], ) -> dict[str, Any]: """Process cloud connection info message to client.""" diff --git a/hass_nabucasa/cloud_api.py b/hass_nabucasa/cloud_api.py index fb9011d83..cd93ae598 100644 --- a/hass_nabucasa/cloud_api.py +++ b/hass_nabucasa/cloud_api.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine from functools import wraps import logging from typing import ( @@ -11,9 +12,8 @@ ParamSpec, TypeVar, ) -from collections.abc import Awaitable, Callable, Coroutine -from aiohttp import ClientResponse +from aiohttp import ClientResponse from aiohttp.hdrs import AUTHORIZATION, USER_AGENT _LOGGER = logging.getLogger(__name__) @@ -91,7 +91,9 @@ async def async_remote_register(cloud: Cloud[_ClientT]) -> ClientResponse: @_check_token @_log_response async def async_remote_token( - cloud: Cloud[_ClientT], aes_key: bytes, aes_iv: bytes + cloud: Cloud[_ClientT], + aes_key: bytes, + aes_iv: bytes, ) -> ClientResponse: """Create a remote snitun token.""" url = f"https://{cloud.servicehandlers_server}/instance/snitun_token" @@ -105,7 +107,8 @@ async def async_remote_token( @_check_token @_log_response async def async_remote_challenge_txt( - cloud: Cloud[_ClientT], txt: str + cloud: Cloud[_ClientT], + txt: str, ) -> ClientResponse: """Set DNS challenge.""" url = f"https://{cloud.servicehandlers_server}/instance/dns_challenge_txt" @@ -119,7 +122,8 @@ async def async_remote_challenge_txt( @_check_token @_log_response async def async_remote_challenge_cleanup( - cloud: Cloud[_ClientT], txt: str + cloud: Cloud[_ClientT], + txt: str, ) -> ClientResponse: """Remove DNS challenge.""" url = f"https://{cloud.servicehandlers_server}/instance/dns_challenge_cleanup" diff --git a/hass_nabucasa/google_report_state.py b/hass_nabucasa/google_report_state.py index d9ce35e5b..447c36c45 100644 --- a/hass_nabucasa/google_report_state.py +++ b/hass_nabucasa/google_report_state.py @@ -72,7 +72,7 @@ async def async_send_message(self, msg: Any) -> None: if self._to_send.full(): discard_msg = self._to_send.get_nowait() self._response_handler.pop(discard_msg["msgid"]).set_exception( - ErrorResponse(ERR_DISCARD_CODE, ERR_DISCARD_MSG) + ErrorResponse(ERR_DISCARD_CODE, ERR_DISCARD_MSG), ) fut = self._response_handler[msgid] = asyncio.Future() @@ -91,7 +91,7 @@ def async_handle_message(self, msg: dict[str, Any]) -> None: if response_handler is not None: if "error" in msg: response_handler.set_exception( - ErrorResponse(msg["error"], msg["message"]) + ErrorResponse(msg["error"], msg["message"]), ) else: response_handler.set_result(msg.get("payload")) diff --git a/hass_nabucasa/iot.py b/hass_nabucasa/iot.py index e0be4b042..fb3ed2416 100644 --- a/hass_nabucasa/iot.py +++ b/hass_nabucasa/iot.py @@ -94,7 +94,7 @@ async def async_send_message( try: await self.async_send_json_message( - {"msgid": msgid, "handler": handler, "payload": payload} + {"msgid": msgid, "handler": handler, "payload": payload}, ) if expect_answer and fut is not None: @@ -124,7 +124,7 @@ async def _async_handle_handler_message(self, message: dict[str, Any]) -> None: handler = HANDLERS.get(message["handler"]) if handler is None: - raise UnknownHandler() + raise UnknownHandler result = await handler(self.cloud, message.get("payload")) @@ -175,7 +175,8 @@ async def async_handle_system(cloud: Cloud[_ClientT], payload: dict[str, Any]) - @HANDLERS.register("alexa") async def async_handle_alexa( - cloud: Cloud[_ClientT], payload: dict[str, Any] + cloud: Cloud[_ClientT], + payload: dict[str, Any], ) -> dict[str, Any]: """Handle an incoming IoT message for Alexa.""" return await cloud.client.async_alexa_message(payload) @@ -183,7 +184,8 @@ async def async_handle_alexa( @HANDLERS.register("google_actions") async def async_handle_google_actions( - cloud: Cloud[_ClientT], payload: dict[str, Any] + cloud: Cloud[_ClientT], + payload: dict[str, Any], ) -> dict[str, Any]: """Handle an incoming IoT message for Google Actions.""" return await cloud.client.async_google_message(payload) @@ -198,7 +200,8 @@ async def async_handle_cloud(cloud: Cloud[_ClientT], payload: dict[str, Any]) -> # Log out of Home Assistant Cloud await cloud.logout() _LOGGER.error( - "You have been logged out from Home Assistant cloud: %s", payload["reason"] + "You have been logged out from Home Assistant cloud: %s", + payload["reason"], ) elif action == "disconnect_remote": # Disconnect Remote connection @@ -245,7 +248,8 @@ async def async_handle_connection_info( @HANDLERS.register("webhook") async def async_handle_webhook( - cloud: Cloud[_ClientT], payload: dict[str, Any] + cloud: Cloud[_ClientT], + payload: dict[str, Any], ) -> dict[str, Any]: """Handle an incoming IoT message for cloud webhooks.""" return await cloud.client.async_webhook_message(payload) diff --git a/hass_nabucasa/iot_base.py b/hass_nabucasa/iot_base.py index da9e1ff81..2bd976b8e 100644 --- a/hass_nabucasa/iot_base.py +++ b/hass_nabucasa/iot_base.py @@ -3,22 +3,22 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Callable import dataclasses import logging import pprint import random from socket import gaierror from typing import TYPE_CHECKING, Any -from collections.abc import Awaitable, Callable from aiohttp import ( ClientError, + ClientWebSocketResponse, WSMessage, WSMsgType, WSServerHandshakeError, client_exceptions, hdrs, - ClientWebSocketResponse, ) from .auth import CloudError @@ -104,7 +104,8 @@ def register_on_connect(self, on_connect_cb: Callable[[], Awaitable[None]]) -> N self._on_connect.append(on_connect_cb) def register_on_disconnect( - self, on_disconnect_cb: Callable[[], Awaitable[None]] + self, + on_disconnect_cb: Callable[[], Awaitable[None]], ) -> None: """Register an async on_disconnect callback.""" self._on_disconnect.append(on_disconnect_cb) @@ -172,7 +173,7 @@ async def _wait_retry(self) -> None: """Wait until it's time till the next retry.""" # Sleep 2^tries + 0…tries*3 seconds between retries self.retry_task = asyncio.create_task( - asyncio.sleep(2 ** min(9, self.tries) + random.randint(0, self.tries * 3)) + asyncio.sleep(2 ** min(9, self.tries) + random.randint(0, self.tries * 3)), ) await self.retry_task self.retry_task = None @@ -183,14 +184,17 @@ async def _handle_connection(self) -> None: await self.cloud.auth.async_check_token() except CloudError as err: self._logger.warning( - "Cannot connect because unable to refresh token: %s", err + "Cannot connect because unable to refresh token: %s", + err, ) return if self.require_subscription and self.cloud.subscription_expired: self._logger.debug("Cloud subscription expired. Cancelling connecting.") self.cloud.client.user_message( - "cloud_subscription_expired", "Home Assistant Cloud", MESSAGE_EXPIRATION + "cloud_subscription_expired", + "Home Assistant Cloud", + MESSAGE_EXPIRATION, ) self.close_requested = True return @@ -256,7 +260,8 @@ async def _handle_connection(self) -> None: if self._logger.isEnabledFor(logging.DEBUG): self._logger.debug( - "Received message:\n%s\n", pprint.pformat(msg_content) + "Received message:\n%s\n", + pprint.pformat(msg_content), ) try: diff --git a/hass_nabucasa/remote.py b/hass_nabucasa/remote.py index 05019941e..214cbdefd 100644 --- a/hass_nabucasa/remote.py +++ b/hass_nabucasa/remote.py @@ -8,7 +8,7 @@ from enum import Enum import logging import random -import ssl +from ssl import SSLContext from typing import TYPE_CHECKING, cast import aiohttp @@ -24,6 +24,7 @@ if TYPE_CHECKING: from . import Cloud, _ClientT + _LOGGER = logging.getLogger(__name__) RENEW_IF_EXPIRES_DAYS = 25 @@ -168,7 +169,7 @@ def certificate(self) -> Certificate | None: alternative_names=self._acme.alternative_names, ) - async def _create_context(self) -> ssl.SSLContext: + async def _create_context(self) -> SSLContext: """Create SSL context with acme certificate.""" context = utils.server_context_modern() @@ -244,14 +245,16 @@ async def load_backend(self) -> bool: ): for alias in self.alias or []: if not await self._custom_domain_dns_configuration_is_valid( - instance_domain, alias + instance_domain, + alias, ): domains.remove(alias) if ca_domains != set(domains): if ca_domains: _LOGGER.warning( - "Invalid certificate found for: (%s)", ",".join(ca_domains) + "Invalid certificate found for: (%s)", + ",".join(ca_domains), ) await self._recreate_acme(domains, email) @@ -307,7 +310,8 @@ async def load_backend(self) -> bool: self.cloud.client.dispatcher_message(const.DISPATCH_REMOTE_BACKEND_UP) _LOGGER.debug( - "Connecting remote backend: %s", self.cloud.client.remote_autostart + "Connecting remote backend: %s", + self.cloud.client.remote_autostart, ) # Connect to remote is autostart enabled if self.cloud.client.remote_autostart: @@ -354,7 +358,7 @@ async def _refresh_snitun_token(self) -> None: return if self.cloud.subscription_expired: - raise SubscriptionExpired() + raise SubscriptionExpired # Generate session token aes_key, aes_iv = generate_aes_keyset() @@ -362,16 +366,16 @@ async def _refresh_snitun_token(self) -> None: async with async_timeout.timeout(30): resp = await cloud_api.async_remote_token(self.cloud, aes_key, aes_iv) if resp.status == 409: - raise RemoteInsecureVersion() + raise RemoteInsecureVersion if resp.status == 403: msg = "" if "application/json" in (resp.content_type or ""): msg = (await resp.json()).get("message", "") raise RemoteForbidden(msg) if resp.status not in (200, 201): - raise RemoteBackendError() + raise RemoteBackendError except (TimeoutError, aiohttp.ClientError): - raise RemoteBackendError() from None + raise RemoteBackendError from None data = await resp.json() self._token = SniTunToken( @@ -546,7 +550,9 @@ async def _check_cname(self, hostname: str) -> list[str]: return [] async def _custom_domain_dns_configuration_is_valid( - self, instance_domain: str, custom_domain: str + self, + instance_domain: str, + custom_domain: str, ) -> bool: """Validate custom domain.""" # Check primary entry @@ -555,7 +561,7 @@ async def _custom_domain_dns_configuration_is_valid( # Check LE entry if f"_acme-challenge.{instance_domain}" not in await self._check_cname( - f"_acme-challenge.{custom_domain}" + f"_acme-challenge.{custom_domain}", ): return False @@ -591,9 +597,10 @@ async def _should_renew_certificates(self) -> bool: for alias in check_alias: # Check primary entry if not await self._custom_domain_dns_configuration_is_valid( - self.instance_domain, alias + self.instance_domain, + alias, ): - bad_alias.append(alias) + bad_alias.append(alias) # noqa: PERF401 if not bad_alias: # No bad configuration detected diff --git a/hass_nabucasa/thingtalk.py b/hass_nabucasa/thingtalk.py index de6984c40..e0034cb9d 100644 --- a/hass_nabucasa/thingtalk.py +++ b/hass_nabucasa/thingtalk.py @@ -15,7 +15,8 @@ class ThingTalkConversionError(Exception): async def async_convert(cloud: Cloud[_ClientT], query: str) -> dict[str, Any]: """Convert sentence.""" resp = await cloud.client.websession.post( - f"https://{cloud.thingtalk_server}/convert", json={"query": query} + f"https://{cloud.thingtalk_server}/convert", + json={"query": query}, ) if resp.status in (200, 201): content: dict[str, Any] = await resp.json() diff --git a/hass_nabucasa/utils.py b/hass_nabucasa/utils.py index 5a4a1cb85..9989d0de0 100644 --- a/hass_nabucasa/utils.py +++ b/hass_nabucasa/utils.py @@ -3,15 +3,15 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Callable import datetime as dt -import logging +from logging import Logger import ssl from typing import TypeVar -from collections.abc import Awaitable, Callable import ciso8601 -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # noqa pylint: disable=invalid-name +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name UTC = dt.UTC @@ -56,7 +56,7 @@ def server_context_modern() -> ssl.SSLContext: "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" - "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" + "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256", ) return context @@ -65,13 +65,18 @@ def server_context_modern() -> ssl.SSLContext: def next_midnight() -> float: """Return the seconds till next local midnight.""" midnight = dt.datetime.now().replace( - hour=0, minute=0, second=0, microsecond=0 + hour=0, + minute=0, + second=0, + microsecond=0, ) + dt.timedelta(days=1) return (midnight - dt.datetime.now()).total_seconds() async def gather_callbacks( - logger: logging.Logger, name: str, callbacks: list[Callable[[], Awaitable[None]]] + logger: Logger, + name: str, + callbacks: list[Callable[[], Awaitable[None]]], ) -> None: results = await asyncio.gather(*[cb() for cb in callbacks], return_exceptions=True) for result, callback in zip(results, callbacks, strict=False): diff --git a/hass_nabucasa/voice.py b/hass_nabucasa/voice.py index 49c3794f1..fd284ba9b 100644 --- a/hass_nabucasa/voice.py +++ b/hass_nabucasa/voice.py @@ -2,11 +2,11 @@ from __future__ import annotations +from collections.abc import AsyncIterable from datetime import datetime from enum import Enum -from typing import TYPE_CHECKING -from collections.abc import AsyncIterable import logging +from typing import TYPE_CHECKING import xml.etree.ElementTree as ET from aiohttp.hdrs import ACCEPT, AUTHORIZATION, CONTENT_TYPE, USER_AGENT @@ -1238,7 +1238,7 @@ async def _update_token(self) -> None: """Update token details.""" resp = await cloud_api.async_voice_connection_details(self.cloud) if resp.status != 200: - raise VoiceTokenError() + raise VoiceTokenError data = await resp.json() self._token = data["authorized_key"] @@ -1285,13 +1285,14 @@ async def process_stt( ) if resp.status not in (200, 201): raise VoiceReturnError( - f"Error processing {language} speech: {resp.status} {await resp.text()}" + f"Error processing {language} speech: {resp.status} {await resp.text()}", ) data = await resp.json() # Parse Answer return STTResponse( - data["RecognitionStatus"] == "Success", data.get("DisplayText") + data["RecognitionStatus"] == "Success", + data.get("DisplayText"), ) async def process_tts( @@ -1365,6 +1366,6 @@ async def process_tts( ) if resp.status not in (200, 201): raise VoiceReturnError( - f"Error receiving TTS with {language}/{voice}: {resp.status} {await resp.text()}" + f"Error receiving TTS with {language}/{voice}: {resp.status} {await resp.text()}", ) return await resp.read() diff --git a/pyproject.toml b/pyproject.toml index dd303e33c..3a64e6ad9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -135,13 +135,18 @@ target-version = "py311" [tool.ruff.lint] ignore = [ - "ANN101", - "ANN401", - "COM812", - "PLR2004", - "S101", - "TRY003", - "S303", + "ANN101", # https://docs.astral.sh/ruff/rules/missing-type-self/ + "ANN401", # https://docs.astral.sh/ruff/rules/any-type/ + "PLR2004", # https://docs.astral.sh/ruff/rules/magic-value-comparison/ + "RUF006", # https://docs.astral.sh/ruff/rules/asyncio-dangling-task/ + "S101", # https://docs.astral.sh/ruff/rules/assert/ + "S303", # https://docs.astral.sh/ruff/rules/suspicious-insecure-hash-usage/ + "S311", # https://docs.astral.sh/ruff/rules/suspicious-non-cryptographic-random-usage/ + "TCH002", # https://docs.astral.sh/ruff/rules/typing-only-third-party-import/ + "TCH003", # https://docs.astral.sh/ruff/rules/typing-only-standard-library-import/ + "TRY003", # https://docs.astral.sh/ruff/rules/raise-vanilla-args/ + "TRY301", # https://docs.astral.sh/ruff/rules/raise-within-try/ + "TRY400", # https://docs.astral.sh/ruff/rules/error-instead-of-exception/ "ARG001", # TEMPORARY DISABLED "ARG002", # TEMPORARY DISABLED @@ -163,23 +168,6 @@ ignore = [ "I001", # TEMPORARY DISABLED "N817", # TEMPORARY DISABLED "N818", # TEMPORARY DISABLED - "PERF401", # TEMPORARY DISABLED - "PGH004", # TEMPORARY DISABLED - "PLR0911", # TEMPORARY DISABLED - "PLR0912", # TEMPORARY DISABLED - "PLR0915", # TEMPORARY DISABLED - "PTH112", # TEMPORARY DISABLED - "RSE102", # TEMPORARY DISABLED - "RUF006", # TEMPORARY DISABLED - "RUF100", # TEMPORARY DISABLED - "S311", # TEMPORARY DISABLED - "SIM114", # TEMPORARY DISABLED - "TCH002", # TEMPORARY DISABLED - "TCH003", # TEMPORARY DISABLED - "TRY300", # TEMPORARY DISABLED - "TRY301", # TEMPORARY DISABLED - "TRY400", # TEMPORARY DISABLED - "UP006", # TEMPORARY DISABLED ] select = [ @@ -203,6 +191,9 @@ known-first-party = [ [tool.ruff.lint.pylint] max-args = 15 +max-branches = 30 +max-returns = 7 +max-statements = 80 [tool.setuptools] include-package-data = true diff --git a/tests/common.py b/tests/common.py index eff9e2c9a..51e619375 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,9 +3,9 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine from pathlib import Path from typing import Any, Literal -from collections.abc import Coroutine from unittest.mock import Mock from hass_nabucasa.client import CloudClient @@ -141,7 +141,7 @@ async def async_create_repair_issue( "translation_key": translation_key, "placeholders": placeholders, "severity": severity, - } + }, ) @@ -248,7 +248,11 @@ async def stop(self): self.call_stop = True async def connect( - self, token: bytes, aes_key: bytes, aes_iv: bytes, throttling=None + self, + token: bytes, + aes_key: bytes, + aes_iv: bytes, + throttling=None, ): """Connect snitun.""" self.call_connect = True diff --git a/tests/conftest.py b/tests/conftest.py index 266e12124..15cec5172 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -96,7 +96,8 @@ async def close(self): # Trigger cancelled error to avoid reconnect. org_websession = cloud_mock.websession with patch( - "hass_nabucasa.iot_base.BaseIoT._wait_retry", side_effect=asyncio.CancelledError + "hass_nabucasa.iot_base.BaseIoT._wait_retry", + side_effect=asyncio.CancelledError, ): websession.ws_connect.side_effect = AsyncMock(return_value=client) cloud_mock.websession = websession diff --git a/tests/test_account_link.py b/tests/test_account_link.py index 0de880c23..f4178ecd3 100644 --- a/tests/test_account_link.py +++ b/tests/test_account_link.py @@ -29,12 +29,14 @@ async def websocket_handler(request): async def create_helper_instance( - aiohttp_client, handle_server_msgs, service + aiohttp_client, + handle_server_msgs, + service, ) -> account_link.AuthorizeAccountHelper: """Create a auth helper instance.""" client = await create_account_link_server(aiohttp_client, handle_server_msgs) mock_cloud = Mock( - client=Mock(websession=Mock(ws_connect=AsyncMock(return_value=client))) + client=Mock(websession=Mock(ws_connect=AsyncMock(return_value=client))), ) return account_link.AuthorizeAccountHelper(mock_cloud, service) diff --git a/tests/test_auth.py b/tests/test_auth.py index acd7a1436..62e3b06cd 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -66,7 +66,9 @@ async def test_login(mock_cognito, mock_cloud): assert len(mock_cognito.authenticate.mock_calls) == 1 mock_cloud.update_token.assert_called_once_with( - "test_id_token", "test_access_token", "test_refresh_token" + "test_id_token", + "test_access_token", + "test_refresh_token", ) @@ -74,7 +76,9 @@ async def test_register(mock_cognito, cloud_mock): """Test registering an account.""" auth = auth_api.CognitoAuth(cloud_mock) await auth.async_register( - "email@home-assistant.io", "password", client_metadata={"test": "metadata"} + "email@home-assistant.io", + "password", + client_metadata={"test": "metadata"}, ) assert len(mock_cognito.register.mock_calls) == 1 diff --git a/tests/test_cloud_api.py b/tests/test_cloud_api.py index 2f786f636..f5561a512 100644 --- a/tests/test_cloud_api.py +++ b/tests/test_cloud_api.py @@ -130,7 +130,9 @@ async def test_subscription_info(auth_cloud_mock, aioclient_mock): auth_cloud_mock.accounts_server = "example.com" with patch.object( - auth_cloud_mock.auth, "async_renew_access_token", AsyncMock() + auth_cloud_mock.auth, + "async_renew_access_token", + AsyncMock(), ) as mock_renew: data = await cloud_api.async_subscription_info(auth_cloud_mock) assert len(aioclient_mock.mock_calls) == 1 @@ -149,7 +151,9 @@ async def test_subscription_info(auth_cloud_mock, aioclient_mock): }, ) with patch.object( - auth_cloud_mock.auth, "async_renew_access_token", AsyncMock() + auth_cloud_mock.auth, + "async_renew_access_token", + AsyncMock(), ) as mock_renew: data = await cloud_api.async_subscription_info(auth_cloud_mock) diff --git a/tests/test_cloudhooks.py b/tests/test_cloudhooks.py index 409c3b234..ab9b99b1e 100644 --- a/tests/test_cloudhooks.py +++ b/tests/test_cloudhooks.py @@ -50,7 +50,7 @@ async def test_disable(mock_cloudhooks): "webhook_id": "mock-webhook-id", "cloudhook_id": "mock-cloud-id", "cloudhook_url": "https://hooks.nabu.casa/ZXCZCXZ", - } + }, } await mock_cloudhooks.async_delete("mock-webhook-id") diff --git a/tests/test_init.py b/tests/test_init.py index f852a447b..7902305c3 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -22,7 +22,7 @@ def test_constructor_loads_info_from_constant(cloud_client): "cognito_client_id": "test-cognito_client_id", "user_pool_id": "test-user_pool_id", "region": "test-region", - } + }, }, ), patch.dict( @@ -38,7 +38,7 @@ def test_constructor_loads_info_from_constant(cloud_client): "account_link": "test-account-link-url", "servicehandlers": "test-servicehandlers-url", "thingtalk": "test-thingtalk-url", - } + }, }, ), ): @@ -77,8 +77,8 @@ async def test_initialize_loads_info(cloud_client): "id_token": "test-id-token", "access_token": "test-access-token", "refresh_token": "test-refresh-token", - } - ) + }, + ), ), exists=Mock(return_value=True), ) @@ -164,7 +164,8 @@ async def test_logout_clears_info(cloud_client): cl._on_stop.clear() info_file = MagicMock( - exists=Mock(return_value=True), unlink=Mock(return_value=True) + exists=Mock(return_value=True), + unlink=Mock(return_value=True), ) cl.id_token = "id_token" @@ -181,7 +182,7 @@ async def test_logout_clears_info(cloud_client): cl.remote.disconnect = AsyncMock() cl._on_stop.extend( - [cl.iot.disconnect, cl.remote.disconnect, cl.google_report_state.disconnect] + [cl.iot.disconnect, cl.remote.disconnect, cl.google_report_state.disconnect], ) with patch( @@ -277,7 +278,12 @@ def test_subscription_expired(cloud_client): patch( "hass_nabucasa.utcnow", return_value=utcnow().replace( - year=2017, month=11, day=19, hour=23, minute=59, second=59 + year=2017, + month=11, + day=19, + hour=23, + minute=59, + second=59, ), ), ): @@ -288,7 +294,12 @@ def test_subscription_expired(cloud_client): patch( "hass_nabucasa.utcnow", return_value=utcnow().replace( - year=2017, month=11, day=20, hour=0, minute=0, second=0 + year=2017, + month=11, + day=20, + hour=0, + minute=0, + second=0, ), ), ): diff --git a/tests/test_iot.py b/tests/test_iot.py index 7399ce4b7..3eafff622 100644 --- a/tests/test_iot.py +++ b/tests/test_iot.py @@ -35,7 +35,7 @@ async def receive_mock(_timeout): mock_iot_client.receive = receive_mock mock_iot_client.send_json = AsyncMock( - side_effect=lambda _: handler_respond_set.set() + side_effect=lambda _: handler_respond_set.set(), ) @@ -98,7 +98,7 @@ async def test_connection_msg_for_unknown_handler(mock_iot_client, cloud_mock_io "msgid": "test-msg-id", "handler": "non-existing-test-handler", "payload": "test-payload", - } + }, ), ), ) @@ -114,7 +114,8 @@ async def test_connection_msg_for_unknown_handler(mock_iot_client, cloud_mock_io async def test_connection_msg_for_handler_raising_handler_error( - mock_iot_client, cloud_mock_iot + mock_iot_client, + cloud_mock_iot, ): """Test we sent error when handler raises HandlerError exception.""" conn = iot.CloudIoT(cloud_mock_iot) @@ -128,7 +129,7 @@ async def test_connection_msg_for_handler_raising_handler_error( "msgid": "test-msg-id", "handler": "test-handler", "payload": "test-payload", - } + }, ), ), ) @@ -160,13 +161,14 @@ async def test_connection_msg_for_handler_raising(mock_iot_client, cloud_mock_io "msgid": "test-msg-id", "handler": "test-handler", "payload": "test-payload", - } + }, ), ), ) with patch.dict( - iot.HANDLERS, {"test-handler": Mock(side_effect=Exception("Broken"))} + iot.HANDLERS, + {"test-handler": Mock(side_effect=Exception("Broken"))}, ): await conn.connect() @@ -182,7 +184,8 @@ async def test_handling_core_messages_logout(cloud_mock_iot): """Test handling core messages.""" cloud_mock_iot.logout = AsyncMock() await iot.async_handle_cloud( - cloud_mock_iot, {"action": "logout", "reason": "Logged in at two places."} + cloud_mock_iot, + {"action": "logout", "reason": "Logged in at two places."}, ) assert len(cloud_mock_iot.logout.mock_calls) == 1 @@ -264,7 +267,7 @@ async def test_send_message_answer(cloud_mock_iot): with patch("hass_nabucasa.iot.uuid.uuid4", return_value=MagicMock(hex=uuid)): send_task = asyncio.create_task( - cloud_iot.async_send_message("webhook", {"msg": "yo"}) + cloud_iot.async_send_message("webhook", {"msg": "yo"}), ) await asyncio.sleep(0) diff --git a/tests/test_iot_base.py b/tests/test_iot_base.py index aa23f9575..9e0ad16fa 100644 --- a/tests/test_iot_base.py +++ b/tests/test_iot_base.py @@ -70,7 +70,7 @@ def cloud_mock_iot(auth_cloud_mock): type=WSMsgType.CLOSING, data=4002, extra="Another instance connected", - ) + ), ], iot_base.DisconnectReason( False, @@ -132,8 +132,9 @@ async def test_cloud_sending_invalid_json(mock_iot_client, caplog, cloud_mock_io conn = MockIoT(cloud_mock_iot) mock_iot_client.receive = AsyncMock( return_value=MagicMock( - type=WSMsgType.TEXT, json=MagicMock(side_effect=ValueError) - ) + type=WSMsgType.TEXT, + json=MagicMock(side_effect=ValueError), + ), ) await conn.connect() @@ -156,7 +157,9 @@ async def test_cloud_connect_invalid_auth(mock_iot_client, caplog, cloud_mock_io conn = MockIoT(cloud_mock_iot) request_info = Mock(real_url="http://example.com") mock_iot_client.receive.side_effect = client_exceptions.WSServerHandshakeError( - request_info=request_info, history=None, status=401 + request_info=request_info, + history=None, + status=401, ) await conn.connect() @@ -165,33 +168,40 @@ async def test_cloud_connect_invalid_auth(mock_iot_client, caplog, cloud_mock_io async def test_cloud_unable_to_connect( - cloud_mock, caplog, cloud_mock_iot, mock_iot_client + cloud_mock, + caplog, + cloud_mock_iot, + mock_iot_client, ): """Test unable to connect error.""" conn = MockIoT(cloud_mock_iot) cloud_mock.websession.ws_connect.side_effect = client_exceptions.ClientError( - "SSL Verification failed" + "SSL Verification failed", ) await conn.connect() assert conn.last_disconnect_reason == iot_base.DisconnectReason( - False, "Unable to connect: SSL Verification failed" + False, + "Unable to connect: SSL Verification failed", ) assert "Unable to connect:" in caplog.text async def test_cloud_connection_reset_exception( - mock_iot_client, caplog, cloud_mock_iot + mock_iot_client, + caplog, + cloud_mock_iot, ): """Test connection reset exception.""" conn = MockIoT(cloud_mock_iot) mock_iot_client.receive.side_effect = ConnectionResetError( - "Cannot write to closing transport" + "Cannot write to closing transport", ) await conn.connect() assert conn.last_disconnect_reason == iot_base.DisconnectReason( - False, "Connection closed: Cannot write to closing transport" + False, + "Connection closed: Cannot write to closing transport", ) assert "Cannot write to closing transport" in caplog.text diff --git a/tests/test_remote.py b/tests/test_remote.py index 3a417e8a6..3f025125f 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -35,7 +35,8 @@ def ignore_context(): """Ignore ssl context.""" with patch( - "hass_nabucasa.remote.RemoteUI._create_context", return_value=None + "hass_nabucasa.remote.RemoteUI._create_context", + return_value=None, ) as context: yield context @@ -72,7 +73,11 @@ def test_init_remote(auth_cloud_mock): async def test_load_backend_exists_cert( - auth_cloud_mock, valid_acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + valid_acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -142,7 +147,11 @@ async def test_load_backend_exists_cert( async def test_load_backend_not_exists_cert( - auth_cloud_mock, acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -200,7 +209,11 @@ async def test_load_backend_not_exists_cert( async def test_load_and_unload_backend( - auth_cloud_mock, valid_acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + valid_acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -259,7 +272,11 @@ async def test_load_and_unload_backend( async def test_load_backend_exists_wrong_cert( - auth_cloud_mock, valid_acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + valid_acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -323,7 +340,11 @@ async def test_load_backend_exists_wrong_cert( async def test_call_disconnect( - auth_cloud_mock, acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -361,7 +382,11 @@ async def test_call_disconnect( async def test_load_backend_no_autostart( - auth_cloud_mock, valid_acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + valid_acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -411,7 +436,11 @@ async def test_load_backend_no_autostart( async def test_get_certificate_details( - auth_cloud_mock, acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -456,7 +485,11 @@ async def test_get_certificate_details( async def test_certificate_task_no_backend( - auth_cloud_mock, acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -488,7 +521,7 @@ async def test_certificate_task_no_backend( patch("random.randint", return_value=0), ): acme_task = remote._acme_task = asyncio.create_task( - remote._certificate_handler() + remote._certificate_handler(), ) await asyncio.sleep(0.1) assert acme_mock.call_issue @@ -501,7 +534,11 @@ async def test_certificate_task_no_backend( async def test_certificate_task_renew_cert( - auth_cloud_mock, acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -533,7 +570,7 @@ async def test_certificate_task_renew_cert( patch("random.randint", return_value=0), ): acme_task = remote._acme_task = asyncio.create_task( - remote._certificate_handler() + remote._certificate_handler(), ) await remote.load_backend() @@ -555,7 +592,11 @@ async def test_refresh_token_no_sub(auth_cloud_mock): async def test_load_connect_insecure( - auth_cloud_mock, valid_acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + valid_acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -597,7 +638,12 @@ async def test_load_connect_insecure( async def test_load_connect_forbidden( - auth_cloud_mock, valid_acme_mock, mock_cognito, aioclient_mock, snitun_mock, caplog + auth_cloud_mock, + valid_acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, + caplog, ): """Initialize backend.""" auth_cloud_mock.servicehandlers_server = "test.local" @@ -634,7 +680,11 @@ async def test_load_connect_forbidden( async def test_call_disconnect_clean_token( - auth_cloud_mock, acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Initialize backend.""" valid = utcnow() + timedelta(days=1) @@ -673,7 +723,11 @@ async def test_call_disconnect_clean_token( async def test_recreating_old_certificate_with_bad_dns_config( - auth_cloud_mock, valid_acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + valid_acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Test recreating old certificate with bad DNS config for alias.""" valid = utcnow() + timedelta(days=1) @@ -708,7 +762,7 @@ async def test_recreating_old_certificate_with_bad_dns_config( valid_acme_mock.common_name = "test.dui.nabu.casa" valid_acme_mock.alternative_names = ["test.dui.nabu.casa", "example.com"] valid_acme_mock.expire_date = utils.utcnow() + timedelta( - days=WARN_RENEW_FAILED_DAYS + days=WARN_RENEW_FAILED_DAYS, ) await remote.load_backend() await asyncio.sleep(0.1) @@ -731,7 +785,7 @@ async def test_recreating_old_certificate_with_bad_dns_config( assert len(auth_cloud_mock.client.mock_repairs) == 1 repair = auth_cloud_mock.client.mock_repairs[0] assert set(repair.keys()) == set( - ["identifier", "translation_key", "severity", "placeholders"] + ["identifier", "translation_key", "severity", "placeholders"], ) assert repair["identifier"].startswith("reset_bad_custom_domain_configuration_") assert repair["translation_key"] == "reset_bad_custom_domain_configuration" @@ -749,7 +803,11 @@ async def test_recreating_old_certificate_with_bad_dns_config( async def test_warn_about_bad_dns_config_for_old_certificate( - auth_cloud_mock, valid_acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + valid_acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Test warn about old certificate with bad DNS config for alias.""" valid = utcnow() + timedelta(days=1) @@ -799,7 +857,7 @@ async def test_warn_about_bad_dns_config_for_old_certificate( assert len(auth_cloud_mock.client.mock_repairs) == 1 repair = auth_cloud_mock.client.mock_repairs[0] assert set(repair.keys()) == set( - ["identifier", "translation_key", "severity", "placeholders"] + ["identifier", "translation_key", "severity", "placeholders"], ) assert repair["identifier"].startswith("warn_bad_custom_domain_configuration_") assert repair["translation_key"] == "warn_bad_custom_domain_configuration" @@ -817,7 +875,11 @@ async def test_warn_about_bad_dns_config_for_old_certificate( async def test_regeneration_without_warning_for_good_dns_config( - auth_cloud_mock, valid_acme_mock, mock_cognito, aioclient_mock, snitun_mock + auth_cloud_mock, + valid_acme_mock, + mock_cognito, + aioclient_mock, + snitun_mock, ): """Test no warning for good dns config.""" valid = utcnow() + timedelta(days=1) diff --git a/tests/test_voice.py b/tests/test_voice.py index 5c7b32d89..1fd0ac398 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -144,7 +144,10 @@ async def test_process_tts_bad_voice(voice_api): async def test_process_tss_429( - voice_api, mock_voice_connection_details, aioclient_mock, caplog + voice_api, + mock_voice_connection_details, + aioclient_mock, + caplog, ): """Test handling of voice with 429.""" aioclient_mock.post( @@ -166,7 +169,10 @@ async def test_process_tss_429( async def test_process_stt_429( - voice_api, mock_voice_connection_details, aioclient_mock, caplog + voice_api, + mock_voice_connection_details, + aioclient_mock, + caplog, ): """Test handling of voice with 429.""" aioclient_mock.post( diff --git a/tests/utils/aiohttp.py b/tests/utils/aiohttp.py index 87d05632e..5731b5f35 100644 --- a/tests/utils/aiohttp.py +++ b/tests/utils/aiohttp.py @@ -64,8 +64,14 @@ def request( self._mocks.append( AiohttpClientMockResponse( - method, url, status, content, cookies, exc, headers - ) + method, + url, + status, + content, + cookies, + exc, + headers, + ), ) def get(self, *args, **kwargs): @@ -136,7 +142,9 @@ async def match_request( return response assert False, "No mock registered for {} {} {}".format( - method.upper(), url, params + method.upper(), + url, + params, ) @@ -144,7 +152,14 @@ class AiohttpClientMockResponse: """Mock Aiohttp client response.""" def __init__( - self, method, url, status, response, cookies=None, exc=None, headers=None + self, + method, + url, + status, + response, + cookies=None, + exc=None, + headers=None, ): """Initialize a fake response.""" self.method = method @@ -234,7 +249,10 @@ def raise_for_status(self): """Raise error if status is 400 or higher.""" if self.status >= 400: raise ClientResponseError( - None, None, status=self.status, headers=self.headers + None, + None, + status=self.status, + headers=self.headers, ) def close(self): @@ -250,7 +268,8 @@ def mock_aiohttp_client(loop): mocker = AiohttpClientMocker() with mock.patch( - "hass_nabucasa.Cloud.websession", new_callable=mock.PropertyMock + "hass_nabucasa.Cloud.websession", + new_callable=mock.PropertyMock, ) as mock_websession: session = mocker.create_session(loop) mock_websession.return_value = session