Skip to content

Commit 130af48

Browse files
committed
Resolve "Implement client for Image endpoint"
1 parent 353aaa0 commit 130af48

9 files changed

Lines changed: 505 additions & 25 deletions

File tree

hcloud/hcloud.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from hcloud.servers.client import ServersClient
88
from hcloud.server_types.client import ServerTypesClient
99
from hcloud.volumes.client import VolumesClient
10+
from hcloud.images.client import ImagesClient
1011
from hcloud.locations.client import LocationsClient
1112
from hcloud.datacenters.client import DatacentersClient
1213

13-
1414
from .version import VERSION
1515

1616

@@ -34,6 +34,7 @@ def __init__(self, token):
3434
self.server_types = ServerTypesClient(self)
3535
self.volumes = VolumesClient(self)
3636
self.actions = ActionsClient(self)
37+
self.images = ImagesClient(self)
3738

3839
def _get_user_agent(self):
3940
return "hcloud-python/" + self.version
@@ -52,9 +53,10 @@ def request(self, method, url, **kwargs):
5253
headers=self._get_headers(),
5354
**kwargs
5455
)
55-
56+
result = response.content
5657
try:
57-
result = response.json()
58+
if len(response.content) > 0:
59+
result = response.json()
5860
except (TypeError, ValueError):
5961
raise HcloudAPIException(
6062
code=response.status_code,
@@ -65,11 +67,17 @@ def request(self, method, url, **kwargs):
6567
)
6668

6769
if not response.ok:
68-
69-
raise HcloudAPIException(
70-
code=result['error']['code'],
71-
message=result['error']['message'],
72-
details=result['error']['details']
73-
)
70+
if len(response.content) > 0:
71+
raise HcloudAPIException(
72+
code=result['error']['code'],
73+
message=result['error']['message'],
74+
details=result['error']['details']
75+
)
76+
else:
77+
raise HcloudAPIException(
78+
code="unknown_error",
79+
message="An unknown error occurred.",
80+
details=""
81+
)
7482

7583
return result

hcloud/images/client.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,96 @@
11
# -*- coding: utf-8 -*-
2-
from hcloud.core.client import BoundModelBase
2+
from hcloud.actions.client import BoundAction
3+
from hcloud.core.client import BoundModelBase, ClientEntityBase
34

45
from hcloud.images.domain import Image
56

67

78
class BoundImage(BoundModelBase):
89
model = Image
10+
11+
def __init__(self, client, data):
12+
from hcloud.servers.client import BoundServer
13+
created_from = data.get("created_from")
14+
if created_from is not None:
15+
data['created_from'] = BoundServer(client._client.servers, created_from, complete=False)
16+
bound_to = data.get("bound_to")
17+
if bound_to is not None:
18+
data['bound_to'] = BoundServer(client._client.servers, {"id": bound_to}, complete=False)
19+
20+
super(BoundImage, self).__init__(client, data)
21+
22+
def get_actions(self, sort=None):
23+
# type: (Optional[List[str]]) -> List[BoundAction]
24+
return self._client.get_actions(self, sort)
25+
26+
def update(self, description=None, type=None, labels=None):
27+
# type: (Optional[str], Optional[Dict[str, str]]) -> BoundImage
28+
return self._client.update(self, description, type, labels)
29+
30+
def delete(self):
31+
# type: () -> bool
32+
return self._client.delete(self)
33+
34+
def change_protection(self, delete=None):
35+
# type: (Optional[bool]) -> BoundAction
36+
return self._client.change_protection(self, delete)
37+
38+
39+
class ImagesClient(ClientEntityBase):
40+
41+
def get_actions(self, image, sort=None):
42+
# type: (Image, Optional[List[str]]) -> List[BoundAction]
43+
params = {}
44+
if sort is not None:
45+
params.update({"sort": sort})
46+
response = self._client.request(url="/images/{image_id}/actions".format(image_id=image.id), method="GET", params=params)
47+
return [BoundAction(self._client.actions, action_data) for action_data in response['actions']]
48+
49+
def get_by_id(self, id):
50+
# type: (int) -> BoundImage
51+
response = self._client.request(url="/images/{image_id}".format(image_id=id), method="GET")
52+
return BoundImage(self, response['image'])
53+
54+
def get_all(self, name=None, label_selector=None, bound_to=None, type=None, sort=None):
55+
# type: (Optional[str], Optional[str], Optional[List[str]], Optional[List[str]],Optional[List[str]]) -> List[BoundImage]
56+
params = {}
57+
if name:
58+
params['name'] = name
59+
if label_selector:
60+
params['label_selector'] = label_selector
61+
if bound_to:
62+
params['bound_to'] = bound_to
63+
if type:
64+
params['type'] = type
65+
if sort:
66+
params['sort'] = sort
67+
68+
response = self._client.request(url="/images", method="GET", params=params)
69+
return [BoundImage(self, image_data) for image_data in response['images']]
70+
71+
def update(self, image, description=None, type=None, labels=None):
72+
# type:(Image, Optional[str], Optional[str], Optional[Dict[str, str]]) -> BoundImage
73+
data = {}
74+
if description is not None:
75+
data.update({"description": description})
76+
if type is not None:
77+
data.update({"type": type})
78+
if labels is not None:
79+
data.update({"labels": labels})
80+
response = self._client.request(url="/images/{image_id}".format(image_id=image.id), method="PUT", json=data)
81+
return BoundImage(self, response['image'])
82+
83+
def delete(self, image):
84+
# type: (Image) -> bool
85+
self._client.request(url="/images/{image_id}".format(image_id=image.id), method="DELETE")
86+
# Return allays true, because the API does not return an action for it. When an error occurs a HcloudAPIException will be raised
87+
return True
88+
89+
def change_protection(self, image, delete=None):
90+
# type: (Image, Optional[bool], Optional[bool]) -> BoundAction
91+
data = {}
92+
if delete is not None:
93+
data.update({"delete": delete})
94+
95+
response = self._client.request(url="/images/{image_id}/actions/change_protection".format(image_id=image.id), method="POST", json=data)
96+
return BoundAction(self._client.actions, response['action'])

hcloud/images/domain.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ class CreateImageResponse(BaseDomain):
7070

7171
def __init__(
7272
self,
73-
action, # type: Action
74-
image # type: Image
73+
action, # type: BoundAction
74+
image # type: BoundImage
7575
):
7676
self.action = action
7777
self.image = image

hcloud/servers/client.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from hcloud.actions.client import BoundAction
55
from hcloud.servers.domain import Server, CreateServerResponse, ResetPasswordResponse, EnableRescueResponse, RequestConsoleResponse
66
from hcloud.volumes.client import BoundVolume
7-
from hcloud.images.domain import Image, CreateImageResponse
7+
from hcloud.images.domain import CreateImageResponse
8+
from hcloud.images.client import BoundImage
89
from hcloud.iso.domain import Iso
910
from hcloud.server_types.client import BoundServerType
1011
from hcloud.datacenters.client import BoundDatacenter
@@ -13,7 +14,7 @@
1314
class BoundServer(BoundModelBase):
1415
model = Server
1516

16-
def __init__(self, client, data):
17+
def __init__(self, client, data, complete=False):
1718

1819
datacenter = data.get('datacenter')
1920
if datacenter is not None:
@@ -26,8 +27,7 @@ def __init__(self, client, data):
2627

2728
image = data.get("image", None)
2829
if image is not None:
29-
# data['image'] = BoundImage(client._client.images, image, complete=True) # When Image Client is implemented
30-
data['image'] = Image(**image)
30+
data['image'] = BoundImage(client._client.images, image)
3131

3232
iso = data.get("iso", None)
3333
if iso is not None:
@@ -38,10 +38,10 @@ def __init__(self, client, data):
3838
if server_type is not None:
3939
data['server_type'] = BoundServerType(client._client.server_types, server_type)
4040

41-
super(BoundServer, self).__init__(client, data)
41+
super(BoundServer, self).__init__(client, data, complete)
4242

4343
def get_actions(self, status=None, sort=None):
44-
# type: # type: (Optional[List[str], Optional[List[str]]) -> List[BoundAction]
44+
# type: (Optional[List[str]], Optional[List[str]]) -> List[BoundAction]
4545
return self._client.get_actions(self, status, sort)
4646

4747
def update(self, name=None, labels=None):
@@ -301,7 +301,7 @@ def create_image(self, server, description=None, type=None, labels=None):
301301
data.update({"type": labels})
302302

303303
response = self._client.request(url="/servers/{server_id}/actions/create_image".format(server_id=server.id), method="POST", json=data)
304-
return CreateImageResponse(action=BoundAction(self._client.actions, response['action']), image=Image(**response['image']))
304+
return CreateImageResponse(action=BoundAction(self._client.actions, response['action']), image=BoundImage(self._client.images, response['image']))
305305

306306
def rebuild(self, server, image):
307307
# type: (servers.domain.Server, Image) -> actions.domainAction
Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,77 @@
1-
# import mock
2-
# import pytest
1+
import mock
2+
import pytest
33

4-
# from hcloud.images.client import ImagesClient
5-
# from hcloud.images.domain import Image
4+
from hcloud.images.client import BoundImage
5+
from hcloud.images.domain import Image
66

7-
# TODO
7+
8+
class TestBoundImage(object):
9+
@pytest.fixture()
10+
def bound_image(self, hetzner_client):
11+
return BoundImage(client=hetzner_client.images, data=dict(id=42))
12+
13+
def test_get_actions(self, bound_image):
14+
actions = bound_image.get_actions()
15+
16+
assert len(actions) == 1
17+
assert actions[0].id == 13
18+
assert actions[0].command == "change_protection"
19+
20+
def test_update(self, bound_image):
21+
image = bound_image.update(description="My new Image description", type="the new image type", labels={})
22+
23+
assert image.id == 4711
24+
assert image.description == "My new Image description"
25+
26+
def test_delete(self, bound_image):
27+
delete_success = bound_image.delete()
28+
29+
assert delete_success is True
30+
31+
def test_change_protection(self, bound_image):
32+
action = bound_image.change_protection(True)
33+
34+
assert action.id == 13
35+
assert action.command == "change_protection"
36+
37+
38+
class TestImagesClient(object):
39+
def test_get_by_id(self, hetzner_client):
40+
image = hetzner_client.images.get_by_id(1)
41+
assert image.id == 4711
42+
assert image.name == "ubuntu-16.04"
43+
assert image.description == "Ubuntu 16.04 Standard 64 bit"
44+
45+
def test_get_all(self, hetzner_client):
46+
images = hetzner_client.images.get_all()
47+
assert images[0].id == 4711
48+
assert images[0].name == "ubuntu-16.04"
49+
assert images[0].description == "Ubuntu 16.04 Standard 64 bit"
50+
51+
@pytest.mark.parametrize("image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))])
52+
def test_get_actions(self, hetzner_client, image):
53+
actions = hetzner_client.images.get_actions(image)
54+
55+
assert len(actions) == 1
56+
assert actions[0].id == 13
57+
assert actions[0].command == "change_protection"
58+
59+
@pytest.mark.parametrize("image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))])
60+
def test_update(self, hetzner_client, image):
61+
image = hetzner_client.images.update(image, description="My new Image description", type="the new image type", labels={})
62+
63+
assert image.id == 4711
64+
assert image.description == "My new Image description"
65+
66+
@pytest.mark.parametrize("image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))])
67+
def test_delete(self, hetzner_client, image):
68+
delete_success = hetzner_client.images.delete(image)
69+
70+
assert delete_success is True
71+
72+
@pytest.mark.parametrize("image", [Image(id=1), BoundImage(mock.MagicMock(), dict(id=1))])
73+
def test_change_protection(self, hetzner_client, image):
74+
action = hetzner_client.images.change_protection(image, delete=True)
75+
76+
assert action.id == 13
77+
assert action.command == "change_protection"

0 commit comments

Comments
 (0)