from typing import Any, Optional, Union import aiohttp import asyncio import logging from . import error DEFAULT_TIMEOUT = aiohttp.ClientTimeout(total=None, sock_connect=5, sock_read=5) DEFAULT_PORT = 80 logger = logging.getLogger(__name__) class ApiHost: def __init__( self, host: str, port: int, timeout: int, session: Any, loop: Any, logger: logging.Logger = logger, **auth, ): self._host = host self._port = port self._username = auth.get("username") self._password = auth.get("password") # TODO: handle empty logger? self._logger = logger self._timeout = timeout if timeout else DEFAULT_TIMEOUT self._session = session auth = None if any(data is not None for data in [self._username, self._password]): auth = aiohttp.BasicAuth(login=self._username, password=self._password) if not self._session: self._session = aiohttp.ClientSession(loop=loop, timeout=timeout, auth=auth) # TODO: remove? self._loop = loop async def async_request( self, path: str, async_method: Any, data: Union[dict, str, None] = None, allow_ota_check_response: bool = False, ) -> Optional[dict]: # TODO: check timeout client_timeout = self._timeout url = self.api_path(path) try: if data is not None: response = await async_method(url, timeout=client_timeout, data=data) else: response = await async_method(url, timeout=client_timeout) accepted_statuses = (200, 202, 204) if allow_ota_check_response else (200,) if response.status not in accepted_statuses: if response.status == 401: raise error.UnauthorizedRequest( f"Request to {url} failed with HTTP {response.status}, UNAUTHORISED" ) raise error.HttpError( f"Request to {url} failed with HTTP {response.status}" ) if response.content_type != "application/json": return None return await response.json() except asyncio.TimeoutError as ex: raise error.TimeoutError( f"Failed to connect to {self.host}:{self.port} within {client_timeout}s: ({ex})" ) from ex except aiohttp.ClientConnectionError as ex: raise error.ConnectionError( f"Failed to connect to {self.host}:{self.port}: {ex}" ) from ex except aiohttp.ClientError as ex: raise error.ClientError(f"API request {url} failed: {ex}") from ex except UnicodeDecodeError as ex: raise error.ConnectionError( f"Invalid response encoding from {url}: {ex}" ) from ex async def async_api_get(self, path: str) -> Optional[dict]: try: return await self.async_request(path, self._session.get) except Exception as ex: logger.error(f"EXCEPTION DURING API CALL: {ex}") raise ex async def async_api_get_ota(self, path: str) -> Optional[dict]: try: return await self.async_request( path, self._session.get, allow_ota_check_response=True ) except Exception as ex: logger.error(f"EXCEPTION DURING API CALL: {ex}") raise ex async def async_api_post( self, path: str, data: Union[dict, str, None] ) -> Optional[dict]: return await self.async_request(path, self._session.post, data) def api_path(self, path: str) -> str: host = self._host port = self._port # TODO: url lib return f"http://{host}:{port}/{path[1:]}" @property def logger(self) -> Any: return self._logger @property def host(self) -> str: return self._host @property def port(self) -> int: return self._port