Skip to content

Commit f7c61b1

Browse files
committed
Add support for keyed errors
1 parent a6fe784 commit f7c61b1

10 files changed

+169
-80
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: 9 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,14 @@ 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.message_template,
82+
result.extra_fields,
83+
result.job_id,
84+
)
7785
raise exc_type()
7886

7987
async def _request(

aiohasupervisor/exceptions.py

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,47 @@
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+
message_template: str | None = None,
17+
extra_fields: dict[str, Any] | None = None,
18+
job_id: str | None = None,
19+
) -> None:
820
"""Initialize exception."""
921
if message is not None:
1022
super().__init__(message)
1123
else:
1224
super().__init__()
1325

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

1646

1747
class SupervisorConnectionError(SupervisorError, ConnectionError):
@@ -46,45 +76,20 @@ class SupervisorResponseError(SupervisorError):
4676
"""Unusable response received from Supervisor with the wrong type or encoding."""
4777

4878

49-
class AddonNotSupportedError(SupervisorError):
79+
class AddonNotSupportedError(SupervisorError, ABC):
5080
"""Addon is not supported on this system."""
5181

5282

83+
@error_key("addon_not_supported_architecture_error")
5384
class AddonNotSupportedArchitectureError(AddonNotSupportedError):
5485
"""Addon is not supported on this system due to its architecture."""
5586

56-
def __init__(
57-
self, addon: str, architectures: str, job_id: str | None = None
58-
) -> None:
59-
"""Initialize exception."""
60-
super().__init__(
61-
f"Add-on {addon} not supported on this platform, "
62-
f"supported architectures: {architectures}",
63-
job_id,
64-
)
65-
6687

88+
@error_key("addon_not_supported_machine_type_error")
6789
class AddonNotSupportedMachineTypeError(AddonNotSupportedError):
6890
"""Addon is not supported on this system due to its machine type."""
6991

70-
def __init__(
71-
self, addon: str, machine_types: str, job_id: str | None = None
72-
) -> None:
73-
"""Initialize exception."""
74-
super().__init__(
75-
f"Add-on {addon} not supported on this machine, "
76-
f"supported machine types: {machine_types}",
77-
job_id,
78-
)
79-
8092

93+
@error_key("addon_not_supported_home_assistant_version_error")
8194
class AddonNotSupportedHomeAssistantVersionError(AddonNotSupportedError):
8295
"""Addon is not supported on this system due to its version of Home Assistant."""
83-
84-
def __init__(self, addon: str, version: str, job_id: str | None = None) -> None:
85-
"""Initialize exception."""
86-
super().__init__(
87-
f"Add-on {addon} not supported on this system, "
88-
f"requires Home Assistant version {version} or greater",
89-
job_id,
90-
)

aiohasupervisor/models/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ 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+
message_template: str | None = None
89+
extra_fields: dict[str, Any] | None = None
8790

8891

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

aiohasupervisor/store.py

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@
55

66
from .client import _SupervisorComponentClient
77
from .const import ResponseType
8-
from .exceptions import (
9-
AddonNotSupportedArchitectureError,
10-
AddonNotSupportedError,
11-
AddonNotSupportedHomeAssistantVersionError,
12-
AddonNotSupportedMachineTypeError,
13-
SupervisorBadRequestError,
14-
)
158
from .models.addons import (
169
Repository,
1710
StoreAddon,
@@ -79,31 +72,21 @@ async def addon_availability(self, addon: str) -> None:
7972
8073
If Supervisor adds a new reason an add-on can be restricted from being
8174
installed on some systems in the future, older versions of this client
82-
will raise the generic AddonNotSupportedError for that reason.
75+
will raise the generic SupervisorBadRequestError for that reason.
8376
"""
84-
try:
85-
await self._client.get(
86-
f"store/addons/{addon}/availability", response_type=ResponseType.NONE
87-
)
88-
except SupervisorBadRequestError as err:
89-
if match := RE_ADDON_UNAVAILABLE_ARCHITECTURE.match(str(err)):
90-
raise AddonNotSupportedArchitectureError(
91-
match.group("addon"), match.group("architectures"), err.job_id
92-
) from None
93-
if match := RE_ADDON_UNAVAILABLE_HOME_ASSISTANT.match(str(err)):
94-
raise AddonNotSupportedHomeAssistantVersionError(
95-
match.group("addon"), match.group("version"), err.job_id
96-
) from None
97-
if match := RE_ADDON_UNAVAILABLE_MACHINE_TYPE.match(str(err)):
98-
raise AddonNotSupportedMachineTypeError(
99-
match.group("addon"), match.group("machine_types"), err.job_id
100-
) from None
101-
raise AddonNotSupportedError(str(err), err.job_id) from None
77+
await self._client.get(
78+
f"store/addons/{addon}/availability", response_type=ResponseType.NONE
79+
)
10280

10381
async def install_addon(
10482
self, addon: str, options: StoreAddonInstall | None = None
10583
) -> None:
106-
"""Install an addon."""
84+
"""Install an addon.
85+
86+
Supervisor does an availability check before install. If the addon
87+
cannot be installed on this system it will raise one of those errors
88+
shown in the `addon_availability` method.
89+
"""
10790
# Must disable timeout if API call waits for install to complete
10891
kwargs: dict[str, Any] = {}
10992
if not options or not options.background:
@@ -118,7 +101,12 @@ async def install_addon(
118101
async def update_addon(
119102
self, addon: str, options: StoreAddonUpdate | None = None
120103
) -> None:
121-
"""Update an addon to latest version."""
104+
"""Update an addon to latest version.
105+
106+
Supervisor does an availability check before update. If the new version
107+
cannot be installed on this system it will raise one of those errors
108+
shown in the `addon_availability` method.
109+
"""
122110
# Must disable timeout if API call waits for update to complete
123111
kwargs: dict[str, Any] = {}
124112
if not options or not options.background:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
"message_template": "Add-on {slug} not supported on this platform, supported architectures: {architectures}",
6+
"extra_fields": {
7+
"slug": "core_mosquitto",
8+
"architectures": "i386"
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
"message_template": "Add-on {slug} not supported on this system, requires Home Assistant version {version} or greater",
6+
"extra_fields": {
7+
"slug": "core_mosquitto",
8+
"version": "2023.1.1"
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
"message_template": "Add-on {slug} not supported on this machine, supported machine types: {machine_types}",
6+
"extra_fields": {
7+
"slug": "core_mosquitto",
8+
"machine_types": "odroid-n2"
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
"message_template": "Add-on {slug} not supported on this system, requires <something new> to be {requirement}",
6+
"extra_fields": {
7+
"slug": "core_mosquitto",
8+
"requirement": "<something else>"
9+
}
10+
}

tests/test_store.py

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
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
810
from aiohasupervisor.exceptions import (
911
AddonNotSupportedArchitectureError,
10-
AddonNotSupportedError,
1112
AddonNotSupportedHomeAssistantVersionError,
1213
AddonNotSupportedMachineTypeError,
14+
SupervisorBadRequestError,
15+
SupervisorError,
1316
)
1417
from aiohasupervisor.models import StoreAddonUpdate, StoreAddRepository
1518
from aiohasupervisor.models.addons import StoreAddonInstall
@@ -187,46 +190,80 @@ async def test_store_addon_availability(
187190

188191

189192
@pytest.mark.parametrize(
190-
("error_msg", "exc_type"),
193+
("error_fixture", "error_key", "exc_type"),
191194
[
192195
(
193-
"Add-on core_mosquitto not supported on this platform, "
194-
"supported architectures: i386",
196+
"store_addon_availability_error_architecture.json",
197+
"addon_not_supported_architecture_error",
195198
AddonNotSupportedArchitectureError,
196199
),
197200
(
198-
"Add-on core_mosquitto not supported on this machine, "
199-
"supported machine types: odroid-n2",
201+
"store_addon_availability_error_machine.json",
202+
"addon_not_supported_machine_type_error",
200203
AddonNotSupportedMachineTypeError,
201204
),
202205
(
203-
"Add-on core_mosquitto not supported on this system, "
204-
"requires Home Assistant version 2023.1.1 or greater",
206+
"store_addon_availability_error_home_assistant.json",
207+
"addon_not_supported_home_assistant_version_error",
205208
AddonNotSupportedHomeAssistantVersionError,
206209
),
207210
(
208-
"Add-on core_mosquitto not supported on this system, "
209-
"requires <something new> to be <something else>",
210-
AddonNotSupportedError,
211+
"store_addon_availability_error_other.json",
212+
None,
213+
SupervisorBadRequestError,
211214
),
212215
],
213216
)
214217
async def test_store_addon_availability_error(
215218
responses: aioresponses,
216219
supervisor_client: SupervisorClient,
217-
error_msg: str,
218-
exc_type: type[AddonNotSupportedError],
220+
error_fixture: str,
221+
error_key: str | None,
222+
exc_type: type[SupervisorError],
219223
) -> None:
220-
"""Test store addon availability API error."""
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.message_template == error_data["message_template"]
231+
assert err.extra_fields == error_data["extra_fields"]
232+
return True
233+
234+
# Availability API
221235
responses.get(
222236
f"{SUPERVISOR_URL}/store/addons/core_mosquitto/availability",
223237
status=400,
224-
body=f'{{"result": "error", "message": "{error_msg}"}}',
238+
body=error_body,
225239
)
226-
227-
with pytest.raises(exc_type):
240+
with pytest.raises(
241+
exc_type, match=error_data["message"], check=check_availability_error
242+
):
228243
await supervisor_client.store.addon_availability("core_mosquitto")
229244

245+
# Install API
246+
responses.post(
247+
f"{SUPERVISOR_URL}/store/addons/core_mosquitto/install",
248+
status=400,
249+
body=error_body,
250+
)
251+
with pytest.raises(
252+
exc_type, match=error_data["message"], check=check_availability_error
253+
):
254+
await supervisor_client.store.install_addon("core_mosquitto")
255+
256+
# Update API
257+
responses.post(
258+
f"{SUPERVISOR_URL}/store/addons/core_mosquitto/update",
259+
status=400,
260+
body=error_body,
261+
)
262+
with pytest.raises(
263+
exc_type, match=error_data["message"], check=check_availability_error
264+
):
265+
await supervisor_client.store.update_addon("core_mosquitto")
266+
230267

231268
async def test_store_reload(
232269
responses: aioresponses, supervisor_client: SupervisorClient

0 commit comments

Comments
 (0)