Skip to content

Commit 9d5d21d

Browse files
authored
1 parent ca4efb8 commit 9d5d21d

5 files changed

Lines changed: 297 additions & 3 deletions

File tree

‎hcloud/networks/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from .domain import (
55
CreateNetworkResponse,
66
Network,
7+
NetworkMember,
8+
NetworkMemberStatus,
9+
NetworkMemberType,
710
NetworkProtection,
811
NetworkRoute,
912
NetworkSubnet,
@@ -18,4 +21,7 @@
1821
"NetworkSubnet",
1922
"NetworksClient",
2023
"NetworksPageResult",
24+
"NetworkMemberType",
25+
"NetworkMemberStatus",
26+
"NetworkMember",
2127
]

‎hcloud/networks/client.py‎

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any, NamedTuple
3+
from typing import TYPE_CHECKING, Any, Literal, NamedTuple
44

55
from ..actions import (
66
ActionSort,
@@ -11,7 +11,14 @@
1111
)
1212
from ..actions.client import ResourceClientBaseActionsMixin
1313
from ..core import BoundModelBase, Meta, ResourceClientBase
14-
from .domain import Network, NetworkRoute, NetworkSubnet
14+
from .domain import (
15+
Network,
16+
NetworkMember,
17+
NetworkMemberStatus,
18+
NetworkMemberType,
19+
NetworkRoute,
20+
NetworkSubnet,
21+
)
1522

1623
if TYPE_CHECKING:
1724
from .._client import Client
@@ -185,6 +192,27 @@ class NetworksPageResult(NamedTuple):
185192
meta: Meta
186193

187194

195+
NetworkMemberSort = Literal[
196+
"id",
197+
"id:asc",
198+
"id:desc",
199+
"type",
200+
"type:asc",
201+
"type:desc",
202+
"status",
203+
"status:asc",
204+
"status:desc",
205+
"ip",
206+
"ip:asc",
207+
"ip:desc",
208+
]
209+
210+
211+
class NetworkMembersPageResult(NamedTuple):
212+
members: list[NetworkMember]
213+
meta: Meta
214+
215+
188216
class NetworksClient(
189217
ResourceClientBaseActionsMixin,
190218
ResourceClientBase,
@@ -372,6 +400,82 @@ def delete(self, network: Network | BoundNetwork) -> bool:
372400
self._client.request(url=f"{self._base_url}/{network.id}", method="DELETE")
373401
return True
374402

403+
def get_member_list(
404+
self,
405+
network: Network | BoundNetwork,
406+
*,
407+
type: list[NetworkMemberType] | None = None,
408+
status: list[NetworkMemberStatus] | None = None,
409+
subnet: list[str] | None = None,
410+
sort: list[NetworkMemberSort] | None = None,
411+
page: int | None = None,
412+
per_page: int | None = None,
413+
) -> NetworkMembersPageResult:
414+
"""
415+
Returns a paginated list of Members for a Network.
416+
417+
:param network: Network to get the Members for.
418+
:param type: Filter the Members by type.
419+
:param status: Filter the Members by status.
420+
:param subnet: Filter the Members by the subnet they are attached to.
421+
:param sort: Sort Members by field and direction.
422+
:param page: Page number to get.
423+
:param per_page: Maximum number of Members returned per page.
424+
"""
425+
params: dict[str, Any] = {}
426+
if type is not None:
427+
params["type"] = type
428+
if status is not None:
429+
params["status"] = status
430+
if subnet is not None:
431+
params["subnet"] = subnet
432+
if sort is not None:
433+
params["sort"] = sort
434+
if page is not None:
435+
params["page"] = page
436+
if per_page is not None:
437+
params["per_page"] = per_page
438+
439+
response = self._client.request(
440+
url=f"{self._base_url}/{network.id}/members",
441+
method="GET",
442+
params=params,
443+
)
444+
445+
return NetworkMembersPageResult(
446+
members=[NetworkMember(**o) for o in response["members"]],
447+
meta=Meta.parse_meta(response),
448+
)
449+
450+
def get_member_all(
451+
self,
452+
network: Network | BoundNetwork,
453+
*,
454+
type: list[NetworkMemberType] | None = None,
455+
status: list[NetworkMemberStatus] | None = None,
456+
subnet: list[str] | None = None,
457+
sort: list[NetworkMemberSort] | None = None,
458+
) -> list[NetworkMember]:
459+
"""
460+
Returns all of Members for a Network.
461+
462+
:param network: Network to get the Members for.
463+
:param type: Filter the Members by type.
464+
:param status: Filter the Members by status.
465+
:param subnet: Filter the Members by the subnet they are attached to.
466+
:param sort: Sort Members by field and direction.
467+
:param page: Page number to get.
468+
:param per_page: Maximum number of Members returned per page.
469+
"""
470+
return self._iter_pages(
471+
self.get_member_list,
472+
network,
473+
type=type,
474+
status=status,
475+
subnet=subnet,
476+
sort=sort,
477+
)
478+
375479
def get_actions_list(
376480
self,
377481
network: Network | BoundNetwork,

‎hcloud/networks/domain.py‎

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import warnings
4-
from typing import TYPE_CHECKING, TypedDict
4+
from typing import TYPE_CHECKING, Literal, TypedDict
55

66
from ..core import BaseDomain, DomainIdentityMixin
77

@@ -16,6 +16,9 @@
1616
"NetworkSubnet",
1717
"NetworkRoute",
1818
"CreateNetworkResponse",
19+
"NetworkMemberType",
20+
"NetworkMemberStatus",
21+
"NetworkMember",
1922
]
2023

2124

@@ -180,3 +183,74 @@ def __init__(
180183
):
181184
self.network = network
182185
self.action = action
186+
187+
188+
NetworkMemberType = Literal[
189+
"server",
190+
"load_balancer",
191+
]
192+
193+
NetworkMemberStatus = Literal[
194+
"ok",
195+
"attaching",
196+
"detaching",
197+
"updating",
198+
"error",
199+
]
200+
201+
202+
class NetworkMember(BaseDomain):
203+
"""Network Member Domain
204+
205+
:param id: ID of the resource attached to the Network.
206+
:param type: Type of the resource attached to the Network.
207+
:param ip: IP address of the resource within the Network.
208+
:param alias_ips: Additional IP address of the resource within the Network.
209+
:param subnet: IP range of the subnet the resource is attached to.
210+
:param status: Status of the resource within the Network.
211+
"""
212+
213+
TYPE_SERVER: NetworkMemberType = "server"
214+
"""The member is a Server."""
215+
TYPE_LOAD_BALANCER: NetworkMemberType = "load_balancer"
216+
"""The member is a Load Balancer."""
217+
218+
STATUS_OK: NetworkMemberStatus = "ok"
219+
"""The resource is attached and its network configuration is up to date."""
220+
STATUS_ATTACHING: NetworkMemberStatus = "attaching"
221+
"""The resource is being attached to the Network."""
222+
STATUS_DETACHING: NetworkMemberStatus = "detaching"
223+
"""The resource is being detached from the Network."""
224+
STATUS_UPDATING: NetworkMemberStatus = "updating"
225+
"""The resource network configuration is being updated."""
226+
STATUS_ERROR: NetworkMemberStatus = "error"
227+
"""
228+
The last operation on this member failed. Its network configuration might be out of date,
229+
and the resource might not be reachable within the Network.
230+
"""
231+
232+
__api_properties__ = (
233+
"id",
234+
"type",
235+
"ip",
236+
"alias_ips",
237+
"subnet",
238+
"status",
239+
)
240+
__slots__ = __api_properties__
241+
242+
def __init__(
243+
self,
244+
id: int,
245+
type: NetworkMemberType,
246+
ip: str,
247+
alias_ips: list[str],
248+
subnet: str,
249+
status: NetworkMemberStatus,
250+
):
251+
self.id = id
252+
self.type = type
253+
self.ip = ip
254+
self.alias_ips = alias_ips
255+
self.subnet = subnet
256+
self.status = status

‎tests/unit/networks/conftest.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,27 @@ def response_get_actions():
210210
}
211211
]
212212
}
213+
214+
215+
@pytest.fixture()
216+
def network_member1():
217+
return {
218+
"type": "server",
219+
"id": 123,
220+
"ip": "10.0.1.2",
221+
"status": "attaching",
222+
"alias_ips": ["10.0.1.20"],
223+
"subnet": "10.0.1.0/24",
224+
}
225+
226+
227+
@pytest.fixture()
228+
def network_member2():
229+
return {
230+
"type": "load_balancer",
231+
"id": 456,
232+
"ip": "10.0.1.3",
233+
"status": "updating",
234+
"alias_ips": [],
235+
"subnet": "10.0.1.0/24",
236+
}

‎tests/unit/networks/test_client.py‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,92 @@ def test_delete(
458458

459459
assert delete_success is True
460460

461+
@pytest.mark.parametrize(
462+
"params",
463+
[
464+
{"type": ["server"]},
465+
{"status": ["ok"]},
466+
{"subnet": ["10.0.1.0/24"]},
467+
{"sort": ["ip:asc"]},
468+
{"page": 1, "per_page": 50},
469+
{},
470+
],
471+
)
472+
@pytest.mark.parametrize(
473+
"network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))]
474+
)
475+
def test_get_member_list(
476+
self,
477+
request_mock: mock.MagicMock,
478+
networks_client: NetworksClient,
479+
network,
480+
network_member1,
481+
network_member2,
482+
params,
483+
):
484+
request_mock.return_value = {"members": [network_member1, network_member2]}
485+
486+
result = networks_client.get_member_list(network, **params)
487+
488+
request_mock.assert_called_with(
489+
method="GET",
490+
url="/networks/1/members",
491+
params=params,
492+
)
493+
494+
assert result.meta is not None
495+
assert len(result.members) == 2
496+
497+
assert result.members[0].id == 123
498+
assert result.members[0].type == "server"
499+
500+
assert result.members[0].ip == "10.0.1.2"
501+
assert result.members[0].alias_ips == ["10.0.1.20"]
502+
assert result.members[0].subnet == "10.0.1.0/24"
503+
assert result.members[0].status == "attaching"
504+
505+
assert result.members[1].id == 456
506+
assert result.members[1].type == "load_balancer"
507+
508+
@pytest.mark.parametrize(
509+
"params",
510+
[
511+
{"type": ["server"]},
512+
{"status": ["ok"]},
513+
{"subnet": ["10.0.1.0/24"]},
514+
{"sort": ["ip:asc"]},
515+
{},
516+
],
517+
)
518+
@pytest.mark.parametrize(
519+
"network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))]
520+
)
521+
def test_get_member_all(
522+
self,
523+
request_mock: mock.MagicMock,
524+
networks_client: NetworksClient,
525+
network,
526+
network_member1,
527+
network_member2,
528+
params,
529+
):
530+
request_mock.return_value = {"members": [network_member1, network_member2]}
531+
532+
result = networks_client.get_member_all(network, **params)
533+
534+
request_mock.assert_called_with(
535+
method="GET",
536+
url="/networks/1/members",
537+
params={**params, "page": 1, "per_page": 50},
538+
)
539+
540+
assert len(result) == 2
541+
assert result[0].id == 123
542+
assert result[0].type == "server"
543+
544+
assert result[1].id == 456
545+
assert result[1].type == "load_balancer"
546+
461547
@pytest.mark.parametrize(
462548
"network", [Network(id=1), BoundNetwork(mock.MagicMock(), dict(id=1))]
463549
)

0 commit comments

Comments
 (0)