Skip to content

Commit 184d6da

Browse files
committed
Add support for keyed errors
1 parent 3caa75c commit 184d6da

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
@@ -4,13 +4,6 @@
44

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

10179
async def install_addon(self, addon: str) -> None:
102-
"""Install an addon."""
80+
"""Install an addon.
81+
82+
Supervisor does an availability check before install. If the addon
83+
cannot be installed on this system it will raise one of those errors
84+
shown in the `addon_availability` method.
85+
"""
10386
await self._client.post(f"store/addons/{addon}/install", timeout=None)
10487

10588
async def update_addon(
10689
self, addon: str, options: StoreAddonUpdate | None = None
10790
) -> None:
108-
"""Update an addon to latest version."""
91+
"""Update an addon to latest version.
92+
93+
Supervisor does an availability check before update. If the new version
94+
cannot be installed on this system it will raise one of those errors
95+
shown in the `addon_availability` method.
96+
"""
10997
await self._client.post(
11098
f"store/addons/{addon}/update",
11199
json=options.to_dict() if options else None,
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

@@ -151,46 +154,80 @@ async def test_store_addon_availability(
151154

152155

153156
@pytest.mark.parametrize(
154-
("error_msg", "exc_type"),
157+
("error_fixture", "error_key", "exc_type"),
155158
[
156159
(
157-
"Add-on core_mosquitto not supported on this platform, "
158-
"supported architectures: i386",
160+
"store_addon_availability_error_architecture.json",
161+
"addon_not_supported_architecture_error",
159162
AddonNotSupportedArchitectureError,
160163
),
161164
(
162-
"Add-on core_mosquitto not supported on this machine, "
163-
"supported machine types: odroid-n2",
165+
"store_addon_availability_error_machine.json",
166+
"addon_not_supported_machine_type_error",
164167
AddonNotSupportedMachineTypeError,
165168
),
166169
(
167-
"Add-on core_mosquitto not supported on this system, "
168-
"requires Home Assistant version 2023.1.1 or greater",
170+
"store_addon_availability_error_home_assistant.json",
171+
"addon_not_supported_home_assistant_version_error",
169172
AddonNotSupportedHomeAssistantVersionError,
170173
),
171174
(
172-
"Add-on core_mosquitto not supported on this system, "
173-
"requires <something new> to be <something else>",
174-
AddonNotSupportedError,
175+
"store_addon_availability_error_other.json",
176+
None,
177+
SupervisorBadRequestError,
175178
),
176179
],
177180
)
178181
async def test_store_addon_availability_error(
179182
responses: aioresponses,
180183
supervisor_client: SupervisorClient,
181-
error_msg: str,
182-
exc_type: type[AddonNotSupportedError],
184+
error_fixture: str,
185+
error_key: str | None,
186+
exc_type: type[SupervisorError],
183187
) -> None:
184-
"""Test store addon availability API error."""
188+
"""Test store addon availability errors."""
189+
error_body = load_fixture(error_fixture)
190+
error_data = loads(error_body)
191+
192+
def check_availability_error(err: SupervisorError) -> bool:
193+
assert err.error_key == error_key
194+
assert err.message_template == error_data["message_template"]
195+
assert err.extra_fields == error_data["extra_fields"]
196+
return True
197+
198+
# Availability API
185199
responses.get(
186200
f"{SUPERVISOR_URL}/store/addons/core_mosquitto/availability",
187201
status=400,
188-
body=f'{{"result": "error", "message": "{error_msg}"}}',
202+
body=error_body,
189203
)
190-
191-
with pytest.raises(exc_type):
204+
with pytest.raises(
205+
exc_type, match=error_data["message"], check=check_availability_error
206+
):
192207
await supervisor_client.store.addon_availability("core_mosquitto")
193208

209+
# Install API
210+
responses.post(
211+
f"{SUPERVISOR_URL}/store/addons/core_mosquitto/install",
212+
status=400,
213+
body=error_body,
214+
)
215+
with pytest.raises(
216+
exc_type, match=error_data["message"], check=check_availability_error
217+
):
218+
await supervisor_client.store.install_addon("core_mosquitto")
219+
220+
# Update API
221+
responses.post(
222+
f"{SUPERVISOR_URL}/store/addons/core_mosquitto/update",
223+
status=400,
224+
body=error_body,
225+
)
226+
with pytest.raises(
227+
exc_type, match=error_data["message"], check=check_availability_error
228+
):
229+
await supervisor_client.store.update_addon("core_mosquitto")
230+
194231

195232
async def test_store_reload(
196233
responses: aioresponses, supervisor_client: SupervisorClient

0 commit comments

Comments
 (0)