mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-12 11:46:31 +00:00
Add /store API (#2626)
This commit is contained in:
parent
739cfbb273
commit
752068bb56
@ -28,6 +28,7 @@ from .resolution import APIResoulution
|
||||
from .security import SecurityMiddleware
|
||||
from .services import APIServices
|
||||
from .snapshots import APISnapshots
|
||||
from .store import APIStore
|
||||
from .supervisor import APISupervisor
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -80,6 +81,7 @@ class RestAPI(CoreSysAttributes):
|
||||
self._register_services()
|
||||
self._register_snapshots()
|
||||
self._register_supervisor()
|
||||
self._register_store()
|
||||
|
||||
await self.start()
|
||||
|
||||
@ -339,12 +341,10 @@ class RestAPI(CoreSysAttributes):
|
||||
web.get("/addons", api_addons.list),
|
||||
web.post("/addons/reload", api_addons.reload),
|
||||
web.get("/addons/{addon}/info", api_addons.info),
|
||||
web.post("/addons/{addon}/install", api_addons.install),
|
||||
web.post("/addons/{addon}/uninstall", api_addons.uninstall),
|
||||
web.post("/addons/{addon}/start", api_addons.start),
|
||||
web.post("/addons/{addon}/stop", api_addons.stop),
|
||||
web.post("/addons/{addon}/restart", api_addons.restart),
|
||||
web.post("/addons/{addon}/update", api_addons.update),
|
||||
web.post("/addons/{addon}/options", api_addons.options),
|
||||
web.post(
|
||||
"/addons/{addon}/options/validate", api_addons.options_validate
|
||||
@ -470,6 +470,46 @@ class RestAPI(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
def _register_store(self) -> None:
|
||||
"""Register store endpoints."""
|
||||
api_store = APIStore()
|
||||
api_store.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/store", api_store.store_info),
|
||||
web.get("/store/addons", api_store.addons_list),
|
||||
web.get("/store/addons/{addon}", api_store.addons_addon_info),
|
||||
web.get("/store/addons/{addon}/{version}", api_store.addons_addon_info),
|
||||
web.post(
|
||||
"/store/addons/{addon}/install", api_store.addons_addon_install
|
||||
),
|
||||
web.post(
|
||||
"/store/addons/{addon}/install/{version}",
|
||||
api_store.addons_addon_install,
|
||||
),
|
||||
web.post("/store/addons/{addon}/update", api_store.addons_addon_update),
|
||||
web.post(
|
||||
"/store/addons/{addon}/update/{version}",
|
||||
api_store.addons_addon_update,
|
||||
),
|
||||
web.post("/store/reload", api_store.reload),
|
||||
web.get("/store/repositories", api_store.repositories_list),
|
||||
web.get(
|
||||
"/store/repositories/{repository}",
|
||||
api_store.repositories_repository_info,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Reroute from legacy
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.post("/addons/{addon}/install", api_store.addons_addon_install),
|
||||
web.post("/addons/{addon}/update", api_store.addons_addon_update),
|
||||
]
|
||||
)
|
||||
|
||||
def _register_panel(self) -> None:
|
||||
"""Register panel for Home Assistant."""
|
||||
panel_dir = Path(__file__).parent.joinpath("panel")
|
||||
|
156
supervisor/api/store.py
Normal file
156
supervisor/api/store.py
Normal file
@ -0,0 +1,156 @@
|
||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||
import asyncio
|
||||
from typing import Any, Awaitable, Dict, List
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from ..api.utils import api_process
|
||||
from ..const import (
|
||||
ATTR_ADDONS,
|
||||
ATTR_ADVANCED,
|
||||
ATTR_AVAILABLE,
|
||||
ATTR_BUILD,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_HOMEASSISTANT,
|
||||
ATTR_ICON,
|
||||
ATTR_INSTALLED,
|
||||
ATTR_LOGO,
|
||||
ATTR_MAINTAINER,
|
||||
ATTR_NAME,
|
||||
ATTR_REPOSITORIES,
|
||||
ATTR_REPOSITORY,
|
||||
ATTR_SLUG,
|
||||
ATTR_SOURCE,
|
||||
ATTR_STAGE,
|
||||
ATTR_UPDATE_AVAILABLE,
|
||||
ATTR_URL,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError
|
||||
from ..store.addon import AddonStore
|
||||
from ..store.repository import Repository
|
||||
|
||||
|
||||
class APIStore(CoreSysAttributes):
|
||||
"""Handle RESTful API for store functions."""
|
||||
|
||||
def _extract_addon(self, request: web.Request) -> AddonStore:
|
||||
"""Return add-on, throw an exception it it doesn't exist."""
|
||||
addon_slug: str = request.match_info.get("addon")
|
||||
addon_version: str = request.match_info.get("version", "latest")
|
||||
|
||||
addon = self.sys_addons.store.get(addon_slug)
|
||||
if not addon:
|
||||
raise APIError(
|
||||
f"Addon {addon_slug} with version {addon_version} does not exist in the store"
|
||||
)
|
||||
|
||||
return addon
|
||||
|
||||
def _extract_repository(self, request: web.Request) -> Repository:
|
||||
"""Return repository, throw an exception it it doesn't exist."""
|
||||
repository_slug: str = request.match_info.get("repository")
|
||||
|
||||
repository = self.sys_store.get(repository_slug)
|
||||
if not repository:
|
||||
raise APIError(f"Repository {repository_slug} does not exist in the store")
|
||||
|
||||
return repository
|
||||
|
||||
def _generate_addon_information(self, addon: AddonStore) -> Dict[str, Any]:
|
||||
"""Generate addon information."""
|
||||
return {
|
||||
ATTR_ADVANCED: addon.advanced,
|
||||
ATTR_AVAILABLE: addon.available,
|
||||
ATTR_BUILD: addon.need_build,
|
||||
ATTR_DESCRIPTON: addon.description,
|
||||
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||
ATTR_ICON: addon.with_icon,
|
||||
ATTR_INSTALLED: addon.is_installed,
|
||||
ATTR_LOGO: addon.with_logo,
|
||||
ATTR_NAME: addon.name,
|
||||
ATTR_REPOSITORY: addon.repository,
|
||||
ATTR_SLUG: addon.slug,
|
||||
ATTR_STAGE: addon.stage,
|
||||
ATTR_UPDATE_AVAILABLE: addon.need_update if addon.is_installed else False,
|
||||
ATTR_URL: addon.url,
|
||||
ATTR_VERSION_LATEST: addon.latest_version,
|
||||
ATTR_VERSION: addon.version if addon.is_installed else None,
|
||||
}
|
||||
|
||||
def _generate_repository_information(
|
||||
self, repository: Repository
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate repository information."""
|
||||
return {
|
||||
ATTR_SLUG: repository.slug,
|
||||
ATTR_NAME: repository.name,
|
||||
ATTR_SOURCE: repository.source,
|
||||
ATTR_URL: repository.url,
|
||||
ATTR_MAINTAINER: repository.maintainer,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def reload(self, request: web.Request) -> None:
|
||||
"""Reload all add-on data from store."""
|
||||
await asyncio.shield(self.sys_store.reload())
|
||||
|
||||
@api_process
|
||||
async def store_info(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Return store information."""
|
||||
return {
|
||||
ATTR_ADDONS: [
|
||||
self._generate_addon_information(self.sys_addons.store[addon])
|
||||
for addon in self.sys_addons.store
|
||||
],
|
||||
ATTR_REPOSITORIES: [
|
||||
self._generate_repository_information(repository)
|
||||
for repository in self.sys_store.all
|
||||
],
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def addons_list(self, request: web.Request) -> List[Dict[str, Any]]:
|
||||
"""Return all store add-ons."""
|
||||
return [
|
||||
self._generate_addon_information(self.sys_addons.store[addon])
|
||||
for addon in self.sys_addons.store
|
||||
]
|
||||
|
||||
@api_process
|
||||
def addons_addon_install(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Install add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
return asyncio.shield(addon.install())
|
||||
|
||||
@api_process
|
||||
def addons_addon_update(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Update add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
if not addon.is_installed:
|
||||
raise APIError(f"Addon {addon.slug} is not installed")
|
||||
return asyncio.shield(addon.update())
|
||||
|
||||
@api_process
|
||||
async def addons_addon_info(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Return add-on information."""
|
||||
addon: AddonStore = self._extract_addon(request)
|
||||
return self._generate_addon_information(addon)
|
||||
|
||||
@api_process
|
||||
async def repositories_list(self, request: web.Request) -> List[Dict[str, Any]]:
|
||||
"""Return all repositories."""
|
||||
return [
|
||||
self._generate_repository_information(repository)
|
||||
for repository in self.sys_store.all
|
||||
]
|
||||
|
||||
@api_process
|
||||
async def repositories_repository_info(
|
||||
self, request: web.Request
|
||||
) -> Dict[str, Any]:
|
||||
"""Return repository information."""
|
||||
repository: Repository = self._extract_repository(request)
|
||||
return self._generate_repository_information(repository)
|
@ -62,7 +62,7 @@ def api_process(method):
|
||||
except (APIError, APIForbidden, HassioError) as err:
|
||||
return api_return_error(error=err)
|
||||
|
||||
if isinstance(answer, dict):
|
||||
if isinstance(answer, (dict, list)):
|
||||
return api_return_ok(data=answer)
|
||||
if isinstance(answer, web.Response):
|
||||
return answer
|
||||
|
@ -20,12 +20,12 @@ if TYPE_CHECKING:
|
||||
from .core import Core
|
||||
from .dbus import DBusManager
|
||||
from .discovery import Discovery
|
||||
from .hardware.module import HardwareManager
|
||||
from .hassos import HassOS
|
||||
from .homeassistant import HomeAssistant
|
||||
from .host import HostManager
|
||||
from .ingress import Ingress
|
||||
from .jobs import JobManager
|
||||
from .hardware.module import HardwareManager
|
||||
from .misc.scheduler import Scheduler
|
||||
from .misc.tasks import Tasks
|
||||
from .plugins import PluginManager
|
||||
|
67
tests/api/test_store.py
Normal file
67
tests/api/test_store.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Test Store API."""
|
||||
from aiohttp.test_utils import TestClient
|
||||
import pytest
|
||||
|
||||
from supervisor.store.addon import AddonStore
|
||||
from supervisor.store.repository import Repository
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_store(
|
||||
api_client: TestClient, store_addon: AddonStore, repository: Repository
|
||||
):
|
||||
"""Test /store REST API."""
|
||||
resp = await api_client.get("/store")
|
||||
result = await resp.json()
|
||||
|
||||
assert result["data"]["addons"][0]["slug"] == store_addon.slug
|
||||
assert result["data"]["repositories"][0]["slug"] == repository.slug
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_store_addons(api_client: TestClient, store_addon: AddonStore):
|
||||
"""Test /store/addons REST API."""
|
||||
print("test")
|
||||
resp = await api_client.get("/store/addons")
|
||||
result = await resp.json()
|
||||
print(result)
|
||||
|
||||
assert result["data"][0]["slug"] == store_addon.slug
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_store_addons_addon(api_client: TestClient, store_addon: AddonStore):
|
||||
"""Test /store/addons/{addon} REST API."""
|
||||
resp = await api_client.get(f"/store/addons/{store_addon.slug}")
|
||||
result = await resp.json()
|
||||
assert result["data"]["slug"] == store_addon.slug
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_store_addons_addon_version(
|
||||
api_client: TestClient, store_addon: AddonStore
|
||||
):
|
||||
"""Test /store/addons/{addon}/{version} REST API."""
|
||||
resp = await api_client.get(f"/store/addons/{store_addon.slug}/1.0.0")
|
||||
result = await resp.json()
|
||||
assert result["data"]["slug"] == store_addon.slug
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_store_repositories(api_client: TestClient, repository: Repository):
|
||||
"""Test /store/repositories REST API."""
|
||||
resp = await api_client.get("/store/repositories")
|
||||
result = await resp.json()
|
||||
|
||||
assert result["data"][0]["slug"] == repository.slug
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_store_repositories_repository(
|
||||
api_client: TestClient, repository: Repository
|
||||
):
|
||||
"""Test /store/repositories/{repository} REST API."""
|
||||
resp = await api_client.get(f"/store/repositories/{repository.slug}")
|
||||
result = await resp.json()
|
||||
|
||||
assert result["data"]["slug"] == repository.slug
|
@ -14,6 +14,8 @@ from supervisor.bootstrap import initialize_coresys
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.network import NetworkManager
|
||||
from supervisor.docker import DockerAPI
|
||||
from supervisor.store.addon import AddonStore
|
||||
from supervisor.store.repository import Repository
|
||||
from supervisor.utils.gdbus import DBus
|
||||
|
||||
from tests.common import exists_fixture, load_fixture, load_json_fixture
|
||||
@ -206,3 +208,25 @@ def run_dir(tmp_path):
|
||||
tmp_state = Path(tmp_path, "supervisor")
|
||||
mock_run.write_text = tmp_state.write_text
|
||||
yield tmp_state
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store_addon(coresys: CoreSys, tmp_path):
|
||||
"""Store add-on fixture."""
|
||||
addon_obj = AddonStore(coresys, "test_store_addon")
|
||||
|
||||
coresys.addons.store[addon_obj.slug] = addon_obj
|
||||
coresys.store.data.addons[addon_obj.slug] = load_json_fixture("add-on.json")
|
||||
yield addon_obj
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repository(coresys: CoreSys):
|
||||
"""Repository fixture."""
|
||||
repository_obj = Repository(
|
||||
coresys, "https://github.com/awesome-developer/awesome-repo"
|
||||
)
|
||||
|
||||
coresys.store.repositories[repository_obj.slug] = repository_obj
|
||||
|
||||
yield repository_obj
|
||||
|
10
tests/fixtures/add-on.json
vendored
Normal file
10
tests/fixtures/add-on.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"advanced": false,
|
||||
"arch": ["amd64"],
|
||||
"description": "Test add-on",
|
||||
"location": "tmp_path",
|
||||
"name": "Test Add-on",
|
||||
"repository": "https://github.com/awesome-developer/awesome-repo",
|
||||
"stage": "stable",
|
||||
"version": "1.0.0"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user