from abc import abstractmethod from collections.abc import Awaitable, Callable from typing import Any import aiohttp from awesomeversion import AwesomeVersion from ..const import SupportedFeatures from ..exceptions import EnvoyHTTPStatusError from ..json import json_loads from ..models.common import CommonProperties from ..models.envoy import EnvoyData class EnvoyUpdater: """Base class for Envoy updaters.""" def __init__( self, envoy_version: AwesomeVersion, probe_request: Callable[[str], Awaitable[aiohttp.ClientResponse]], request: Callable[[str], Awaitable[aiohttp.ClientResponse]], common_properties: CommonProperties, ) -> None: """ Base class for Envoy updaters. Updaters should implement a subclass of EnvoyUpdater. .. code-block:: python class EnvoyXyzUpdater(EnvoyUpdater): :param envoy_version: firmware version Envoy is running :param probe_request: callable specified by :any:`Envoy` to send probe request to the Envoy during :any:`Envoy.probe` :param request: callable specified by :any:`Envoy` to send request to the Envoy during :any:`Envoy.update` :param common_properties: properties to share between probe and update or between updaters """ self._envoy_version = envoy_version self._probe_request = probe_request self._request = request self._supported_features = SupportedFeatures(0) self._common_properties = common_properties async def _json_request(self, end_point: str) -> Any: """ Make a request to the Envoy and return the JSON response. Updaters should use this to collect data during :any:`update` method. .. code-block:: python xyz_json: dict[str, Any] = await self._json_request( "/xyz/endpoint" ) :param end_point: Envoy endpoint to request. See :any:`Envoy.request` :raises EnvoyHTTPStatusError: If http status not in 2xx range :return: JSON content from response :seealso: :any:`Envoy.request` """ response = await self._request(end_point) if not (200 <= response.status < 300): raise EnvoyHTTPStatusError(response.status, str(response.url)) return json_loads(end_point, await response.read()) async def _json_probe_request(self, end_point: str) -> Any: """ Make a probe request to the Envoy and return the JSON response. Updaters should use this to collect data during :any:`probe` method. .. code-block:: python xyz_json: dict[str, Any] = await self._json_probe_request( "/xyz/endpoint" ) :param end_point: Envoy endpoint to request. See :any:`Envoy.probe` :raises EnvoyHTTPStatusError: If http status not in 2xx range :return: JSON content from response :seealso: :any:`Envoy.probe_request` """ response = await self._probe_request(end_point) if not (200 <= response.status < 300): raise EnvoyHTTPStatusError(response.status, str(response.url)) return json_loads(end_point, await response.read()) @abstractmethod async def probe( self, discovered_features: SupportedFeatures ) -> SupportedFeatures | None: """ Probe the Envoy for this updater and return SupportedFeatures. Updater must implement a probe method to report which features it supports. Probe method of each registered updater is called by :any:`Envoy.probe`. Intent of probe is to determine if this updater should be used to obtain the data in its scope from the Envoy. If the Envoy model does not provide the data the method should return None. If the Envoy model does provide the data in scope, the method should return a supported features mask for which it can provide data. Any supported feature that is already included in the passed discovered_features parameter should NOT be included in the result as these are already provided by another updater and this updater should back off from providing that feature data. .. code-block:: python async def probe( self, discovered_features: SupportedFeatures ) -> SupportedFeatures | None: \"\"\"Probe the Envoy for this endpoint and return SupportedFeatures.\"\"\" if SupportedFeatures.MY_FEATURE in discovered_features: # Already discovered from another updater return None try: json_data: dict[str, Any] = await self._json_probe_request(MY_ENDPOINT) except ENDPOINT_PROBE_EXCEPTIONS as e: _LOGGER.debug("Endpoint not found at %s: %s", MY_ENDPOINT, e) return None # process data return SupportedFeatures.MY_FEATURE :param discovered_features: Mask of already discovered SupportedFeatures :return: Mask of SupportedFeatures to be added to already discovered features (and not yet in discovered_features). """ @abstractmethod async def update(self, envoy_data: EnvoyData) -> None: """ Get data from the Envoy and store in EnvoyData. Updater must implement an update method to add/update data in :any:`EnvoyData`. Update method of each registered updater is called by :any:`Envoy.update`. The update method is expected to obtain the required data from the envoy, map it to the internal data model and store the data. It should also store retrieved raw data in :any:`EnvoyData.raw` .. code-block:: python async def update(self, envoy_data: EnvoyData) -> None: \"\"\"Get data from the Envoy and store in EnvoyData.\"\"\" json_data = await self._json_request(self.end_point) envoy_data.raw[self.end_point] = json_data # store applicable data in envoy_data :param envoy_data: Envoy data model to store collected data in """