Skip to content

Commit 954b75e

Browse files
authored
Add addon availability API (#172)
* Add addon availability API * Add support for keyed errors * Remove unused regexes * Remove message_template
1 parent 7bbea79 commit 954b75e

10 files changed

+229
-8
lines changed

aiohasupervisor/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
"""Init file for aiohasupervisor."""
22

33
from aiohasupervisor.exceptions import (
4+
AddonNotSupportedArchitectureError,
5+
AddonNotSupportedError,
6+
AddonNotSupportedHomeAssistantVersionError,
7+
AddonNotSupportedMachineTypeError,
48
SupervisorAuthenticationError,
59
SupervisorBadRequestError,
610
SupervisorConnectionError,
@@ -14,14 +18,18 @@
1418
from aiohasupervisor.root import SupervisorClient
1519

1620
__all__ = [
17-
"SupervisorError",
18-
"SupervisorConnectionError",
21+
"AddonNotSupportedArchitectureError",
22+
"AddonNotSupportedError",
23+
"AddonNotSupportedHomeAssistantVersionError",
24+
"AddonNotSupportedMachineTypeError",
1925
"SupervisorAuthenticationError",
2026
"SupervisorBadRequestError",
27+
"SupervisorClient",
28+
"SupervisorConnectionError",
29+
"SupervisorError",
2130
"SupervisorForbiddenError",
2231
"SupervisorNotFoundError",
2332
"SupervisorResponseError",
2433
"SupervisorServiceUnavailableError",
2534
"SupervisorTimeoutError",
26-
"SupervisorClient",
2735
]

aiohasupervisor/client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from .const import DEFAULT_TIMEOUT, ResponseType
1919
from .exceptions import (
20+
ERROR_KEYS,
2021
SupervisorAuthenticationError,
2122
SupervisorBadRequestError,
2223
SupervisorConnectionError,
@@ -73,7 +74,13 @@ async def _raise_on_status(self, response: ClientResponse) -> None:
7374

7475
if is_json(response):
7576
result = Response.from_json(await response.text())
76-
raise exc_type(result.message, result.job_id)
77+
if result.error_key in ERROR_KEYS:
78+
exc_type = ERROR_KEYS[result.error_key]
79+
raise exc_type(
80+
result.message,
81+
result.extra_fields,
82+
result.job_id,
83+
)
7784
raise exc_type()
7885

7986
async def _request(

aiohasupervisor/exceptions.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
11
"""Exceptions from supervisor client."""
22

3+
from abc import ABC
4+
from collections.abc import Callable
5+
from typing import Any
6+
37

48
class SupervisorError(Exception):
59
"""Generic exception."""
610

7-
def __init__(self, message: str | None = None, job_id: str | None = None) -> None:
11+
error_key: str | None = None
12+
13+
def __init__(
14+
self,
15+
message: str | None = None,
16+
extra_fields: dict[str, Any] | None = None,
17+
job_id: str | None = None,
18+
) -> None:
819
"""Initialize exception."""
920
if message is not None:
1021
super().__init__(message)
1122
else:
1223
super().__init__()
1324

14-
self.job_id: str | None = job_id
25+
self.job_id = job_id
26+
self.extra_fields = extra_fields
27+
28+
29+
ERROR_KEYS: dict[str, type[SupervisorError]] = {}
30+
31+
32+
def error_key(
33+
key: str,
34+
) -> Callable[[type[SupervisorError]], type[SupervisorError]]:
35+
"""Store exception in keyed error map."""
36+
37+
def wrap(cls: type[SupervisorError]) -> type[SupervisorError]:
38+
ERROR_KEYS[key] = cls
39+
cls.error_key = key
40+
return cls
41+
42+
return wrap
1543

1644

1745
class SupervisorConnectionError(SupervisorError, ConnectionError):
@@ -44,3 +72,22 @@ class SupervisorServiceUnavailableError(SupervisorError):
4472

4573
class SupervisorResponseError(SupervisorError):
4674
"""Unusable response received from Supervisor with the wrong type or encoding."""
75+
76+
77+
class AddonNotSupportedError(SupervisorError, ABC):
78+
"""Addon is not supported on this system."""
79+
80+
81+
@error_key("addon_not_supported_architecture_error")
82+
class AddonNotSupportedArchitectureError(AddonNotSupportedError):
83+
"""Addon is not supported on this system due to its architecture."""
84+
85+
86+
@error_key("addon_not_supported_machine_type_error")
87+
class AddonNotSupportedMachineTypeError(AddonNotSupportedError):
88+
"""Addon is not supported on this system due to its machine type."""
89+
90+
91+
@error_key("addon_not_supported_home_assistant_version_error")
92+
class AddonNotSupportedHomeAssistantVersionError(AddonNotSupportedError):
93+
"""Addon is not supported on this system due to its version of Home Assistant."""

aiohasupervisor/models/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ class Response(DataClassORJSONMixin):
8484
data: Any | None = None
8585
message: str | None = None
8686
job_id: str | None = None
87+
error_key: str | None = None
88+
extra_fields: dict[str, Any] | None = None
8789

8890

8991
@dataclass(frozen=True, slots=True)

aiohasupervisor/store.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,31 @@ async def addon_documentation(self, addon: str) -> str:
4848
)
4949
return result.data
5050

51+
async def addon_availability(self, addon: str) -> None:
52+
"""Determine if latest version of addon can be installed on this system.
53+
54+
No return means it can be. If not, raises one of the following errors:
55+
- AddonUnavailableHomeAssistantVersionError
56+
- AddonUnavailableArchitectureError
57+
- AddonUnavailableMachineTypeError
58+
59+
If Supervisor adds a new reason an add-on can be restricted from being
60+
installed on some systems in the future, older versions of this client
61+
will raise the generic SupervisorBadRequestError for that reason.
62+
"""
63+
await self._client.get(
64+
f"store/addons/{addon}/availability", response_type=ResponseType.NONE
65+
)
66+
5167
async def install_addon(
5268
self, addon: str, options: StoreAddonInstall | None = None
5369
) -> None:
54-
"""Install an addon."""
70+
"""Install an addon.
71+
72+
Supervisor does an availability check before install. If the addon
73+
cannot be installed on this system it will raise one of those errors
74+
shown in the `addon_availability` method.
75+
"""
5576
# Must disable timeout if API call waits for install to complete
5677
kwargs: dict[str, Any] = {}
5778
if not options or not options.background:
@@ -66,7 +87,12 @@ async def install_addon(
6687
async def update_addon(
6788
self, addon: str, options: StoreAddonUpdate | None = None
6889
) -> None:
69-
"""Update an addon to latest version."""
90+
"""Update an addon to latest version.
91+
92+
Supervisor does an availability check before update. If the new version
93+
cannot be installed on this system it will raise one of those errors
94+
shown in the `addon_availability` method.
95+
"""
7096
# Must disable timeout if API call waits for update to complete
7197
kwargs: dict[str, Any] = {}
7298
if not options or not options.background:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"result": "error",
3+
"message": "Add-on core_mosquitto not supported on this platform, supported architectures: i386",
4+
"error_key": "addon_not_supported_architecture_error",
5+
"extra_fields": {
6+
"slug": "core_mosquitto",
7+
"architectures": "i386"
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"result": "error",
3+
"message": "Add-on core_mosquitto not supported on this system, requires Home Assistant version 2023.1.1 or greater",
4+
"error_key": "addon_not_supported_home_assistant_version_error",
5+
"extra_fields": {
6+
"slug": "core_mosquitto",
7+
"version": "2023.1.1"
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"result": "error",
3+
"message": "Add-on core_mosquitto not supported on this machine, supported machine types: odroid-n2",
4+
"error_key": "addon_not_supported_machine_type_error",
5+
"extra_fields": {
6+
"slug": "core_mosquitto",
7+
"machine_types": "odroid-n2"
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"result": "error",
3+
"message": "Add-on core_mosquitto not supported on this system, requires <something new> to be <something else>",
4+
"error_key": "addon_not_supported_other_error",
5+
"extra_fields": {
6+
"slug": "core_mosquitto",
7+
"requirement": "<something else>"
8+
}
9+
}

tests/test_store.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
"""Tests for store supervisor client."""
22

3+
from json import loads
4+
35
from aioresponses import aioresponses
46
import pytest
57
from yarl import URL
68

79
from aiohasupervisor import SupervisorClient
10+
from aiohasupervisor.exceptions import (
11+
AddonNotSupportedArchitectureError,
12+
AddonNotSupportedHomeAssistantVersionError,
13+
AddonNotSupportedMachineTypeError,
14+
SupervisorBadRequestError,
15+
SupervisorError,
16+
)
817
from aiohasupervisor.models import StoreAddonUpdate, StoreAddRepository
918
from aiohasupervisor.models.addons import StoreAddonInstall
1019

@@ -169,6 +178,92 @@ async def test_store_addon_update(
169178
)
170179

171180

181+
async def test_store_addon_availability(
182+
responses: aioresponses, supervisor_client: SupervisorClient
183+
) -> None:
184+
"""Test store addon availability API."""
185+
responses.get(
186+
f"{SUPERVISOR_URL}/store/addons/core_mosquitto/availability", status=200
187+
)
188+
189+
assert (await supervisor_client.store.addon_availability("core_mosquitto")) is None
190+
191+
192+
@pytest.mark.parametrize(
193+
("error_fixture", "error_key", "exc_type"),
194+
[
195+
(
196+
"store_addon_availability_error_architecture.json",
197+
"addon_not_supported_architecture_error",
198+
AddonNotSupportedArchitectureError,
199+
),
200+
(
201+
"store_addon_availability_error_machine.json",
202+
"addon_not_supported_machine_type_error",
203+
AddonNotSupportedMachineTypeError,
204+
),
205+
(
206+
"store_addon_availability_error_home_assistant.json",
207+
"addon_not_supported_home_assistant_version_error",
208+
AddonNotSupportedHomeAssistantVersionError,
209+
),
210+
(
211+
"store_addon_availability_error_other.json",
212+
None,
213+
SupervisorBadRequestError,
214+
),
215+
],
216+
)
217+
async def test_store_addon_availability_error(
218+
responses: aioresponses,
219+
supervisor_client: SupervisorClient,
220+
error_fixture: str,
221+
error_key: str | None,
222+
exc_type: type[SupervisorError],
223+
) -> None:
224+
"""Test store addon availability errors."""
225+
error_body = load_fixture(error_fixture)
226+
error_data = loads(error_body)
227+
228+
def check_availability_error(err: SupervisorError) -> bool:
229+
assert err.error_key == error_key
230+
assert err.extra_fields == error_data["extra_fields"]
231+
return True
232+
233+
# Availability API
234+
responses.get(
235+
f"{SUPERVISOR_URL}/store/addons/core_mosquitto/availability",
236+
status=400,
237+
body=error_body,
238+
)
239+
with pytest.raises(
240+
exc_type, match=error_data["message"], check=check_availability_error
241+
):
242+
await supervisor_client.store.addon_availability("core_mosquitto")
243+
244+
# Install API
245+
responses.post(
246+
f"{SUPERVISOR_URL}/store/addons/core_mosquitto/install",
247+
status=400,
248+
body=error_body,
249+
)
250+
with pytest.raises(
251+
exc_type, match=error_data["message"], check=check_availability_error
252+
):
253+
await supervisor_client.store.install_addon("core_mosquitto")
254+
255+
# Update API
256+
responses.post(
257+
f"{SUPERVISOR_URL}/store/addons/core_mosquitto/update",
258+
status=400,
259+
body=error_body,
260+
)
261+
with pytest.raises(
262+
exc_type, match=error_data["message"], check=check_availability_error
263+
):
264+
await supervisor_client.store.update_addon("core_mosquitto")
265+
266+
172267
async def test_store_reload(
173268
responses: aioresponses, supervisor_client: SupervisorClient
174269
) -> None:

0 commit comments

Comments
 (0)