Add /store API (#2626)

This commit is contained in:
Joakim Sørensen 2021-02-25 16:15:51 +01:00 committed by GitHub
parent 739cfbb273
commit 752068bb56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 301 additions and 4 deletions

View File

@ -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
View 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)

View File

@ -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

View File

@ -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
View 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

View File

@ -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
View 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"
}