mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-22 08:36:30 +00:00
Improve type hinting for add-ons (#1769)
* Improve type hinting for add-ons * Split installed vs AnyAddons extraction
This commit is contained in:
parent
f13d08d37a
commit
9fe35b4fb5
@ -143,7 +143,7 @@ class Addon(AddonModel):
|
|||||||
return self.persist.get(ATTR_BOOT, super().boot)
|
return self.persist.get(ATTR_BOOT, super().boot)
|
||||||
|
|
||||||
@boot.setter
|
@boot.setter
|
||||||
def boot(self, value: bool):
|
def boot(self, value: bool) -> None:
|
||||||
"""Store user boot options."""
|
"""Store user boot options."""
|
||||||
self.persist[ATTR_BOOT] = value
|
self.persist[ATTR_BOOT] = value
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ class Addon(AddonModel):
|
|||||||
return self.persist.get(ATTR_AUTO_UPDATE, super().auto_update)
|
return self.persist.get(ATTR_AUTO_UPDATE, super().auto_update)
|
||||||
|
|
||||||
@auto_update.setter
|
@auto_update.setter
|
||||||
def auto_update(self, value: bool):
|
def auto_update(self, value: bool) -> None:
|
||||||
"""Set auto update."""
|
"""Set auto update."""
|
||||||
self.persist[ATTR_AUTO_UPDATE] = value
|
self.persist[ATTR_AUTO_UPDATE] = value
|
||||||
|
|
||||||
@ -190,7 +190,7 @@ class Addon(AddonModel):
|
|||||||
return self.persist[ATTR_PROTECTED]
|
return self.persist[ATTR_PROTECTED]
|
||||||
|
|
||||||
@protected.setter
|
@protected.setter
|
||||||
def protected(self, value: bool):
|
def protected(self, value: bool) -> None:
|
||||||
"""Set add-on in protected mode."""
|
"""Set add-on in protected mode."""
|
||||||
self.persist[ATTR_PROTECTED] = value
|
self.persist[ATTR_PROTECTED] = value
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ class Addon(AddonModel):
|
|||||||
return self.persist.get(ATTR_NETWORK, super().ports)
|
return self.persist.get(ATTR_NETWORK, super().ports)
|
||||||
|
|
||||||
@ports.setter
|
@ports.setter
|
||||||
def ports(self, value: Optional[Dict[str, Optional[int]]]):
|
def ports(self, value: Optional[Dict[str, Optional[int]]]) -> None:
|
||||||
"""Set custom ports of add-on."""
|
"""Set custom ports of add-on."""
|
||||||
if value is None:
|
if value is None:
|
||||||
self.persist.pop(ATTR_NETWORK, None)
|
self.persist.pop(ATTR_NETWORK, None)
|
||||||
@ -232,6 +232,8 @@ class Addon(AddonModel):
|
|||||||
if not url:
|
if not url:
|
||||||
return None
|
return None
|
||||||
webui = RE_WEBUI.match(url)
|
webui = RE_WEBUI.match(url)
|
||||||
|
if not webui:
|
||||||
|
return None
|
||||||
|
|
||||||
# extract arguments
|
# extract arguments
|
||||||
t_port = webui.group("t_port")
|
t_port = webui.group("t_port")
|
||||||
@ -274,7 +276,7 @@ class Addon(AddonModel):
|
|||||||
return self.persist[ATTR_INGRESS_PANEL]
|
return self.persist[ATTR_INGRESS_PANEL]
|
||||||
|
|
||||||
@ingress_panel.setter
|
@ingress_panel.setter
|
||||||
def ingress_panel(self, value: bool):
|
def ingress_panel(self, value: bool) -> None:
|
||||||
"""Return True if the add-on access support ingress."""
|
"""Return True if the add-on access support ingress."""
|
||||||
self.persist[ATTR_INGRESS_PANEL] = value
|
self.persist[ATTR_INGRESS_PANEL] = value
|
||||||
|
|
||||||
@ -310,50 +312,50 @@ class Addon(AddonModel):
|
|||||||
return input_data
|
return input_data
|
||||||
|
|
||||||
@audio_input.setter
|
@audio_input.setter
|
||||||
def audio_input(self, value: Optional[str]):
|
def audio_input(self, value: Optional[str]) -> None:
|
||||||
"""Set audio input settings."""
|
"""Set audio input settings."""
|
||||||
self.persist[ATTR_AUDIO_INPUT] = value
|
self.persist[ATTR_AUDIO_INPUT] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self):
|
def image(self) -> Optional[str]:
|
||||||
"""Return image name of add-on."""
|
"""Return image name of add-on."""
|
||||||
return self.persist.get(ATTR_IMAGE)
|
return self.persist.get(ATTR_IMAGE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def need_build(self):
|
def need_build(self) -> bool:
|
||||||
"""Return True if this add-on need a local build."""
|
"""Return True if this add-on need a local build."""
|
||||||
return ATTR_IMAGE not in self.data
|
return ATTR_IMAGE not in self.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_data(self):
|
def path_data(self) -> Path:
|
||||||
"""Return add-on data path inside Supervisor."""
|
"""Return add-on data path inside Supervisor."""
|
||||||
return Path(self.sys_config.path_addons_data, self.slug)
|
return Path(self.sys_config.path_addons_data, self.slug)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_data(self):
|
def path_extern_data(self) -> PurePath:
|
||||||
"""Return add-on data path external for Docker."""
|
"""Return add-on data path external for Docker."""
|
||||||
return PurePath(self.sys_config.path_extern_addons_data, self.slug)
|
return PurePath(self.sys_config.path_extern_addons_data, self.slug)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_options(self):
|
def path_options(self) -> Path:
|
||||||
"""Return path to add-on options."""
|
"""Return path to add-on options."""
|
||||||
return Path(self.path_data, "options.json")
|
return Path(self.path_data, "options.json")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_pulse(self):
|
def path_pulse(self) -> Path:
|
||||||
"""Return path to asound config."""
|
"""Return path to asound config."""
|
||||||
return Path(self.sys_config.path_tmp, f"{self.slug}_pulse")
|
return Path(self.sys_config.path_tmp, f"{self.slug}_pulse")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_pulse(self):
|
def path_extern_pulse(self) -> Path:
|
||||||
"""Return path to asound config for Docker."""
|
"""Return path to asound config for Docker."""
|
||||||
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_pulse")
|
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_pulse")
|
||||||
|
|
||||||
def save_persist(self):
|
def save_persist(self) -> None:
|
||||||
"""Save data of add-on."""
|
"""Save data of add-on."""
|
||||||
self.sys_addons.data.save_data()
|
self.sys_addons.data.save_data()
|
||||||
|
|
||||||
async def write_options(self):
|
async def write_options(self) -> None:
|
||||||
"""Return True if add-on options is written to data."""
|
"""Return True if add-on options is written to data."""
|
||||||
schema = self.schema
|
schema = self.schema
|
||||||
options = self.options
|
options = self.options
|
||||||
@ -378,7 +380,7 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
raise AddonsError()
|
raise AddonsError()
|
||||||
|
|
||||||
async def remove_data(self):
|
async def remove_data(self) -> None:
|
||||||
"""Remove add-on data."""
|
"""Remove add-on data."""
|
||||||
if not self.path_data.is_dir():
|
if not self.path_data.is_dir():
|
||||||
return
|
return
|
||||||
@ -386,7 +388,7 @@ class Addon(AddonModel):
|
|||||||
_LOGGER.info("Remove add-on data folder %s", self.path_data)
|
_LOGGER.info("Remove add-on data folder %s", self.path_data)
|
||||||
await remove_data(self.path_data)
|
await remove_data(self.path_data)
|
||||||
|
|
||||||
def write_pulse(self):
|
def write_pulse(self) -> None:
|
||||||
"""Write asound config to file and return True on success."""
|
"""Write asound config to file and return True on success."""
|
||||||
pulse_config = self.sys_plugins.audio.pulse_client(
|
pulse_config = self.sys_plugins.audio.pulse_client(
|
||||||
input_profile=self.audio_input, output_profile=self.audio_output
|
input_profile=self.audio_input, output_profile=self.audio_output
|
||||||
@ -518,7 +520,7 @@ class Addon(AddonModel):
|
|||||||
except DockerAPIError:
|
except DockerAPIError:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from None
|
||||||
|
|
||||||
async def write_stdin(self, data):
|
async def write_stdin(self, data) -> None:
|
||||||
"""Write data to add-on stdin.
|
"""Write data to add-on stdin.
|
||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
|
@ -185,7 +185,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_VERSION]
|
return self.data[ATTR_VERSION]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self) -> str:
|
def version(self) -> Optional[str]:
|
||||||
"""Return version of add-on."""
|
"""Return version of add-on."""
|
||||||
return self.data[ATTR_VERSION]
|
return self.data[ATTR_VERSION]
|
||||||
|
|
||||||
@ -288,7 +288,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_HOST_DBUS]
|
return self.data[ATTR_HOST_DBUS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self) -> Optional[List[str]]:
|
def devices(self) -> List[str]:
|
||||||
"""Return devices of add-on."""
|
"""Return devices of add-on."""
|
||||||
return self.data.get(ATTR_DEVICES, [])
|
return self.data.get(ATTR_DEVICES, [])
|
||||||
|
|
||||||
@ -452,7 +452,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data.get(ATTR_MACHINE, [])
|
return self.data.get(ATTR_MACHINE, [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> str:
|
def image(self) -> Optional[str]:
|
||||||
"""Generate image name from data."""
|
"""Generate image name from data."""
|
||||||
return self._image(self.data)
|
return self._image(self.data)
|
||||||
|
|
||||||
|
@ -122,9 +122,7 @@ SCHEMA_SECURITY = vol.Schema({vol.Optional(ATTR_PROTECTED): vol.Boolean()})
|
|||||||
class APIAddons(CoreSysAttributes):
|
class APIAddons(CoreSysAttributes):
|
||||||
"""Handle RESTful API for add-on functions."""
|
"""Handle RESTful API for add-on functions."""
|
||||||
|
|
||||||
def _extract_addon(
|
def _extract_addon(self, request: web.Request) -> AnyAddon:
|
||||||
self, request: web.Request, check_installed: bool = True
|
|
||||||
) -> AnyAddon:
|
|
||||||
"""Return addon, throw an exception it it doesn't exist."""
|
"""Return addon, throw an exception it it doesn't exist."""
|
||||||
addon_slug: str = request.match_info.get("addon")
|
addon_slug: str = request.match_info.get("addon")
|
||||||
|
|
||||||
@ -139,9 +137,12 @@ class APIAddons(CoreSysAttributes):
|
|||||||
if not addon:
|
if not addon:
|
||||||
raise APIError(f"Addon {addon_slug} does not exist")
|
raise APIError(f"Addon {addon_slug} does not exist")
|
||||||
|
|
||||||
if check_installed and not addon.is_installed:
|
return addon
|
||||||
raise APIError(f"Addon {addon.slug} is not installed")
|
|
||||||
|
|
||||||
|
def _extract_addon_installed(self, request: web.Request) -> Addon:
|
||||||
|
addon = self._extract_addon(request)
|
||||||
|
if not isinstance(addon, Addon) or not addon.is_installed:
|
||||||
|
raise APIError("Addon is not installed")
|
||||||
return addon
|
return addon
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@ -190,7 +191,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return add-on information."""
|
"""Return add-on information."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon: AnyAddon = self._extract_addon(request)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
ATTR_NAME: addon.name,
|
ATTR_NAME: addon.name,
|
||||||
@ -257,7 +258,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_INGRESS_PANEL: None,
|
ATTR_INGRESS_PANEL: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if addon.is_installed:
|
if isinstance(addon, Addon) and addon.is_installed:
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
ATTR_STATE: await addon.state(),
|
ATTR_STATE: await addon.state(),
|
||||||
@ -279,7 +280,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def options(self, request: web.Request) -> None:
|
async def options(self, request: web.Request) -> None:
|
||||||
"""Store user options for add-on."""
|
"""Store user options for add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
|
|
||||||
# Update secrets for validation
|
# Update secrets for validation
|
||||||
await self.sys_secrets.reload()
|
await self.sys_secrets.reload()
|
||||||
@ -312,7 +313,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def security(self, request: web.Request) -> None:
|
async def security(self, request: web.Request) -> None:
|
||||||
"""Store security options for add-on."""
|
"""Store security options for add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
body: Dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
body: Dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
||||||
|
|
||||||
if ATTR_PROTECTED in body:
|
if ATTR_PROTECTED in body:
|
||||||
@ -324,7 +325,8 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
|
|
||||||
stats: DockerStats = await addon.stats()
|
stats: DockerStats = await addon.stats()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -341,62 +343,55 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
def install(self, request: web.Request) -> Awaitable[None]:
|
def install(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Install add-on."""
|
"""Install add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request)
|
||||||
return asyncio.shield(addon.install())
|
return asyncio.shield(addon.install())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Uninstall add-on."""
|
"""Uninstall add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
return asyncio.shield(addon.uninstall())
|
return asyncio.shield(addon.uninstall())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def start(self, request: web.Request) -> Awaitable[None]:
|
def start(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Start add-on."""
|
"""Start add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
return asyncio.shield(addon.start())
|
return asyncio.shield(addon.start())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def stop(self, request: web.Request) -> Awaitable[None]:
|
def stop(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Stop add-on."""
|
"""Stop add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
return asyncio.shield(addon.stop())
|
return asyncio.shield(addon.stop())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def update(self, request: web.Request) -> Awaitable[None]:
|
def update(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Update add-on."""
|
"""Update add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon: Addon = self._extract_addon_installed(request)
|
||||||
|
|
||||||
if addon.latest_version == addon.version:
|
|
||||||
raise APIError("No update available!")
|
|
||||||
|
|
||||||
return asyncio.shield(addon.update())
|
return asyncio.shield(addon.update())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart add-on."""
|
"""Restart add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon: Addon = self._extract_addon_installed(request)
|
||||||
return asyncio.shield(addon.restart())
|
return asyncio.shield(addon.restart())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Rebuild local build add-on."""
|
"""Rebuild local build add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
if not addon.need_build:
|
|
||||||
raise APIError("Only local build addons are supported")
|
|
||||||
|
|
||||||
return asyncio.shield(addon.rebuild())
|
return asyncio.shield(addon.rebuild())
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||||
"""Return logs from add-on."""
|
"""Return logs from add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
return addon.logs()
|
return addon.logs()
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_PNG)
|
@api_process_raw(CONTENT_TYPE_PNG)
|
||||||
async def icon(self, request: web.Request) -> bytes:
|
async def icon(self, request: web.Request) -> bytes:
|
||||||
"""Return icon from add-on."""
|
"""Return icon from add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request)
|
||||||
if not addon.with_icon:
|
if not addon.with_icon:
|
||||||
raise APIError(f"No icon found for add-on {addon.slug}!")
|
raise APIError(f"No icon found for add-on {addon.slug}!")
|
||||||
|
|
||||||
@ -406,7 +401,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process_raw(CONTENT_TYPE_PNG)
|
@api_process_raw(CONTENT_TYPE_PNG)
|
||||||
async def logo(self, request: web.Request) -> bytes:
|
async def logo(self, request: web.Request) -> bytes:
|
||||||
"""Return logo from add-on."""
|
"""Return logo from add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request)
|
||||||
if not addon.with_logo:
|
if not addon.with_logo:
|
||||||
raise APIError(f"No logo found for add-on {addon.slug}!")
|
raise APIError(f"No logo found for add-on {addon.slug}!")
|
||||||
|
|
||||||
@ -416,7 +411,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process_raw(CONTENT_TYPE_TEXT)
|
@api_process_raw(CONTENT_TYPE_TEXT)
|
||||||
async def changelog(self, request: web.Request) -> str:
|
async def changelog(self, request: web.Request) -> str:
|
||||||
"""Return changelog from add-on."""
|
"""Return changelog from add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request)
|
||||||
if not addon.with_changelog:
|
if not addon.with_changelog:
|
||||||
raise APIError(f"No changelog found for add-on {addon.slug}!")
|
raise APIError(f"No changelog found for add-on {addon.slug}!")
|
||||||
|
|
||||||
@ -426,7 +421,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process_raw(CONTENT_TYPE_TEXT)
|
@api_process_raw(CONTENT_TYPE_TEXT)
|
||||||
async def documentation(self, request: web.Request) -> str:
|
async def documentation(self, request: web.Request) -> str:
|
||||||
"""Return documentation from add-on."""
|
"""Return documentation from add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request)
|
||||||
if not addon.with_documentation:
|
if not addon.with_documentation:
|
||||||
raise APIError(f"No documentation found for add-on {addon.slug}!")
|
raise APIError(f"No documentation found for add-on {addon.slug}!")
|
||||||
|
|
||||||
@ -436,7 +431,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def stdin(self, request: web.Request) -> None:
|
async def stdin(self, request: web.Request) -> None:
|
||||||
"""Write to stdin of add-on."""
|
"""Write to stdin of add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
if not addon.with_stdin:
|
if not addon.with_stdin:
|
||||||
raise APIError(f"STDIN not supported the {addon.slug} add-on")
|
raise APIError(f"STDIN not supported the {addon.slug} add-on")
|
||||||
|
|
||||||
@ -448,7 +443,7 @@ def _pretty_devices(addon: AnyAddon) -> List[str]:
|
|||||||
"""Return a simplified device list."""
|
"""Return a simplified device list."""
|
||||||
dev_list = addon.devices
|
dev_list = addon.devices
|
||||||
if not dev_list:
|
if not dev_list:
|
||||||
return None
|
return []
|
||||||
return [row.split(":")[0] for row in dev_list]
|
return [row.split(":")[0] for row in dev_list]
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user