Skip to content

Commit 9a9bee8

Browse files
authored
Add modelable ingress APIs to client library (#290)
* Add modelable ingress APIs to client library * Fixes from feedback
1 parent 8122742 commit 9a9bee8

7 files changed

Lines changed: 174 additions & 1 deletion

File tree

aiohasupervisor/ingress.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Ingress client for supervisor."""
2+
3+
from .client import _SupervisorComponentClient
4+
from .const import ResponseType
5+
from .models.ingress import CreateSessionOptions, IngressPanel, IngressPanels, Session
6+
7+
8+
class IngressClient(_SupervisorComponentClient):
9+
"""Handles ingress access in Supervisor.
10+
11+
This includes only the APIs with fixed paths and models from Supervisor's Ingress
12+
API. The wildcard proxy endpoints that allow the UI to talk to addons through Core
13+
and Supervisor are intentionally omitted as they can't be modeled.
14+
"""
15+
16+
async def panels(self) -> dict[str, IngressPanel]:
17+
"""Get ingress panels, returns a map of addon slug to panel info."""
18+
result = await self._client.get("ingress/panels")
19+
return IngressPanels.from_dict(result.data).panels
20+
21+
async def create_session(self, options: CreateSessionOptions | None = None) -> str:
22+
"""Create a new ingress session."""
23+
result = await self._client.post(
24+
"ingress/session",
25+
json=options.to_dict() if options else None,
26+
response_type=ResponseType.JSON,
27+
)
28+
return Session.from_dict(result.data).session
29+
30+
async def validate_session(self, session: str) -> None:
31+
"""Validate an existing ingress session."""
32+
body = Session(session=session).to_dict()
33+
await self._client.post("ingress/validate_session", json=body)

aiohasupervisor/models/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
ServiceState,
7373
ShutdownOptions,
7474
)
75+
from aiohasupervisor.models.ingress import CreateSessionOptions, IngressPanel
7576
from aiohasupervisor.models.jobs import (
7677
Job,
7778
JobCondition,
@@ -195,6 +196,7 @@
195196
"CheckType",
196197
"ContextType",
197198
"CpuArch",
199+
"CreateSessionOptions",
198200
"DataDisk",
199201
"DetectBlockingIO",
200202
"Discovery",
@@ -221,11 +223,12 @@
221223
"IPv4Config",
222224
"IPv6",
223225
"IPv6Config",
226+
"IngressPanel",
224227
"InstalledAddon",
225228
"InstalledAddonComplete",
226-
"InterfaceMethod",
227229
"InterfaceAddrGenMode",
228230
"InterfaceIp6Privacy",
231+
"InterfaceMethod",
229232
"InterfaceType",
230233
"Issue",
231234
"IssueType",

aiohasupervisor/models/ingress.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Models for ingress APIs."""
2+
3+
from dataclasses import dataclass
4+
5+
from .base import Request, ResponseData
6+
7+
8+
@dataclass(frozen=True, slots=True)
9+
class IngressPanel(ResponseData):
10+
"""IngressPanel model."""
11+
12+
title: str
13+
icon: str
14+
admin: bool
15+
enable: bool | None
16+
17+
18+
@dataclass(frozen=True, slots=True)
19+
class IngressPanels(ResponseData):
20+
"""IngressPanels model."""
21+
22+
panels: dict[str, IngressPanel]
23+
24+
25+
@dataclass(frozen=True, slots=True)
26+
class CreateSessionOptions(Request):
27+
"""CreateSessionOptions model."""
28+
29+
user_id: str
30+
31+
32+
@dataclass(frozen=True, slots=True)
33+
class Session(ResponseData):
34+
"""Session model."""
35+
36+
session: str

aiohasupervisor/root.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .discovery import DiscoveryClient
1111
from .homeassistant import HomeAssistantClient
1212
from .host import HostClient
13+
from .ingress import IngressClient
1314
from .jobs import JobsClient
1415
from .models.root import AvailableUpdate, AvailableUpdates, RootInfo
1516
from .mounts import MountsClient
@@ -43,6 +44,7 @@ def __init__(
4344
self._store = StoreClient(self._client)
4445
self._supervisor = SupervisorManagementClient(self._client)
4546
self._homeassistant = HomeAssistantClient(self._client)
47+
self._ingress = IngressClient(self._client)
4648

4749
@property
4850
def addons(self) -> AddonsClient:
@@ -104,6 +106,11 @@ def supervisor(self) -> SupervisorManagementClient:
104106
"""Get supervisor component client."""
105107
return self._supervisor
106108

109+
@property
110+
def ingress(self) -> IngressClient:
111+
"""Get ingress component client."""
112+
return self._ingress
113+
107114
async def info(self) -> RootInfo:
108115
"""Get root info."""
109116
result = await self._client.get("info")

tests/fixtures/create_session.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"result": "ok",
3+
"data": {
4+
"session": "abc123"
5+
}
6+
}

tests/fixtures/ingress_panels.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"result": "ok",
3+
"data": {
4+
"panels": {
5+
"core_ssh": {
6+
"title": "Terminal",
7+
"icon": "mdi:console",
8+
"admin": true,
9+
"enable": false
10+
},
11+
"a0d7b954_vscode": {
12+
"title": "Studio Code Server",
13+
"icon": "mdi:microsoft-visual-studio-code",
14+
"admin": true,
15+
"enable": true
16+
}
17+
}
18+
}
19+
}

tests/test_ingress.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Test ingress component client."""
2+
3+
from aioresponses import aioresponses
4+
import pytest
5+
from yarl import URL
6+
7+
from aiohasupervisor import SupervisorClient
8+
from aiohasupervisor.models import CreateSessionOptions
9+
10+
from . import load_fixture
11+
from .const import SUPERVISOR_URL
12+
13+
14+
async def test_panels(
15+
responses: aioresponses, supervisor_client: SupervisorClient
16+
) -> None:
17+
"""Test panels API."""
18+
responses.get(
19+
f"{SUPERVISOR_URL}/ingress/panels",
20+
status=200,
21+
body=load_fixture("ingress_panels.json"),
22+
)
23+
result = await supervisor_client.ingress.panels()
24+
assert "core_ssh" in result
25+
assert result["core_ssh"].title == "Terminal"
26+
assert result["core_ssh"].icon == "mdi:console"
27+
assert result["core_ssh"].admin is True
28+
assert result["core_ssh"].enable is False
29+
assert "a0d7b954_vscode" in result
30+
assert result["a0d7b954_vscode"].title == "Studio Code Server"
31+
assert result["a0d7b954_vscode"].icon == "mdi:microsoft-visual-studio-code"
32+
assert result["a0d7b954_vscode"].admin is True
33+
assert result["a0d7b954_vscode"].enable is True
34+
35+
36+
@pytest.mark.parametrize(
37+
("options", "expected_body"),
38+
[(None, None), (CreateSessionOptions(user_id="test"), {"user_id": "test"})],
39+
)
40+
async def test_create_session(
41+
responses: aioresponses,
42+
supervisor_client: SupervisorClient,
43+
options: CreateSessionOptions | None,
44+
expected_body: dict[str, str] | None,
45+
) -> None:
46+
"""Test create session API."""
47+
session_url = f"{SUPERVISOR_URL}/ingress/session"
48+
responses.post(session_url, status=200, body=load_fixture("create_session.json"))
49+
result = await supervisor_client.ingress.create_session(options)
50+
assert result == "abc123"
51+
52+
assert responses.requests.keys() == {("POST", URL(session_url))}
53+
session_calls = responses.requests[("POST", URL(session_url))]
54+
assert len(session_calls) == 1
55+
assert session_calls[0].kwargs["json"] == expected_body
56+
57+
58+
async def test_validate_session(
59+
responses: aioresponses, supervisor_client: SupervisorClient
60+
) -> None:
61+
"""Test validate session API."""
62+
validate_url = f"{SUPERVISOR_URL}/ingress/validate_session"
63+
responses.post(validate_url, status=200)
64+
assert await supervisor_client.ingress.validate_session("abc123") is None
65+
66+
assert responses.requests.keys() == {("POST", URL(validate_url))}
67+
validate_calls = responses.requests[("POST", URL(validate_url))]
68+
assert len(validate_calls) == 1
69+
assert validate_calls[0].kwargs["json"] == {"session": "abc123"}

0 commit comments

Comments
 (0)