"""evohomeasync provides an async client for the v0 Resideo TCC API.""" from __future__ import annotations import json from http import HTTPMethod from typing import TYPE_CHECKING, Any, Final import aiohttp from . import exceptions as exc from .auth import _payload from .const import ERR_MSG_LOOKUP_BASE, HINT_CHECK_NETWORK, HOSTNAME if TYPE_CHECKING: import logging from aiohttp.typedefs import StrOrURL class CredentialsManagerBase: """A base class for managing the credentials used for HTTP authentication.""" def __init__( self, client_id: str, secret: str, websession: aiohttp.ClientSession, /, *, logger: logging.Logger, _hostname: str | None = None, ) -> None: """Initialise the session manager.""" self._client_id = client_id self._secret = secret self.websession: Final = websession self._logger = logger self._hostname: Final = _hostname or HOSTNAME self._was_authenticated = False # True once credentials are proven valid def __str__(self) -> str: """Return a string representation of the object.""" return ( f"{self.__class__.__name__}" f"(client_id='{self.client_id}', hostname='{self.hostname}')" ) @property def client_id(self) -> str: """Return the client id used for HTTP authentication.""" return self._client_id @property def hostname(self) -> str: """Return the hostname used for HTTP authentication.""" return self._hostname async def _post_request(self, url: StrOrURL, /, **kwargs: Any) -> dict[str, Any]: """POST an authentication request and return the response (a dict). Will raise an exception if the authentication is not successful. """ rsp: aiohttp.ClientResponse | None = None # to prevent unbound error try: rsp = await self._request(HTTPMethod.POST, url, **kwargs) assert rsp is not None # mypy hint await rsp.read() # so we can use rsp.json()/rsp.text(), below rsp.raise_for_status() # can't assert content_length != 0 with aioresponses, so skip that check if rsp.content_type != "application/json": # usu. "text/plain", "text/html" raise exc.AuthenticationFailedError( f"Authenticator response is not JSON: {await _payload(rsp)}" ) if (response := await rsp.json()) is None: # an unanticipated edge-case raise exc.AuthenticationFailedError("Authenticator response is null") except (aiohttp.ContentTypeError, json.JSONDecodeError) as err: raise exc.AuthenticationFailedError( f"Authenticator response is not valid JSON: {await _payload(rsp)}" ) from err except aiohttp.ClientResponseError as err: # TODO: process payload and raise BadCredentialsError if code = EmailOrPasswordIncorrect if hint := ERR_MSG_LOOKUP_BASE.get(err.status): self._logger.error(hint) # noqa: TRY400 msg = f"{err.status} {err.message}, response={await _payload(rsp)}" raise exc.AuthenticationFailedError( f"Authenticator response is invalid: {msg}", status=err.status ) from err except aiohttp.ClientError as err: # e.g. ClientConnectionError self._logger.error(HINT_CHECK_NETWORK) # noqa: TRY400 raise exc.AuthenticationFailedError( f"Authenticator response is invalid: {err}", ) from err else: return response # type: ignore[no-any-return] finally: if rsp is not None: rsp.release() async def _request( # dev/test wrapper self, method: HTTPMethod, url: StrOrURL, /, **kwargs: Any ) -> aiohttp.ClientResponse: """Wrap the request to the ClientSession (useful for dev/test).""" return await self.websession.request(method, url, **kwargs)