Skip to content

Commit cf9cfea

Browse files
authored
Add wait_until_finished Method to Action (#10)
Closes #4
1 parent e0f2f9c commit cf9cfea

5 files changed

Lines changed: 137 additions & 7 deletions

File tree

hcloud/actions/client.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
# -*- coding: utf-8 -*-
2-
from hcloud.core.client import ClientEntityBase, BoundModelBase
2+
import time
33

4-
from hcloud.actions.domain import Action
4+
from hcloud.core.client import ClientEntityBase, BoundModelBase
5+
from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException
56

67

78
class BoundAction(BoundModelBase):
89
model = Action
910

11+
def wait_until_finished(self, max_retries=100):
12+
while self.status == Action.STATUS_RUNNING:
13+
if max_retries > 0:
14+
self.reload()
15+
time.sleep(self._client._client.poll_interval)
16+
max_retries = max_retries - 1
17+
else:
18+
raise ActionTimeoutException(action=self)
19+
20+
if self.status == Action.STATUS_ERROR:
21+
raise ActionFailedException(action=self)
22+
1023

1124
class ActionsClient(ClientEntityBase):
1225
results_list_attribute_name = 'actions'
@@ -17,9 +30,9 @@ def get_by_id(self, id):
1730
return BoundAction(self, response['action'])
1831

1932
def get_list(self,
20-
status=None, # type: Optional[List[str]]
21-
sort=None, # type: Optional[List[str]]
22-
page=None, # type: Optional[int]
33+
status=None, # type: Optional[List[str]]
34+
sort=None, # type: Optional[List[str]]
35+
page=None, # type: Optional[int]
2336
per_page=None, # type: Optional[int]
2437
):
2538
# type: (...) -> PageResults[List[BoundAction]]

hcloud/actions/domain.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66

77
class Action(BaseDomain):
8+
STATUS_RUNNING = "running"
9+
STATUS_SUCCESS = "success"
10+
STATUS_ERROR = "error"
11+
812
started = ISODateTime()
913
finished = ISODateTime()
1014

@@ -36,3 +40,13 @@ def __init__(
3640
self.finished = finished
3741
self.resources = resources
3842
self.error = error
43+
44+
45+
class ActionFailedException(Exception):
46+
def __init__(self, action):
47+
self.action = action
48+
49+
50+
class ActionTimeoutException(Exception):
51+
def __init__(self, action):
52+
self.action = action

hcloud/hcloud.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ class HcloudClient(object):
2929
version = VERSION
3030
retry_wait_time = 0.5
3131

32-
def __init__(self, token, api_endpoint="https://api.hetzner.cloud/v1", application_name=None, application_version=None):
32+
def __init__(self, token, api_endpoint="https://api.hetzner.cloud/v1", application_name=None, application_version=None, poll_interval=1):
3333
self.token = token
3434
self._api_endpoint = api_endpoint
3535
self._application_name = application_name
3636
self._application_version = application_version
37+
self.poll_interval = poll_interval
3738

3839
self.datacenters = DatacentersClient(self)
3940
self.locations = LocationsClient(self)

tests/unit/actions/conftest.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,75 @@ def generic_action_list():
4343
}
4444
]
4545
}
46+
47+
48+
@pytest.fixture()
49+
def running_action():
50+
return {
51+
"action": {
52+
"id": 2,
53+
"command": "stop_server",
54+
"status": "running",
55+
"progress": 100,
56+
"started": "2016-01-30T23:55:00+00:00",
57+
"finished": "2016-01-30T23:56:00+00:00",
58+
"resources": [
59+
{
60+
"id": 42,
61+
"type": "server"
62+
}
63+
],
64+
"error": {
65+
"code": "action_failed",
66+
"message": "Action failed"
67+
}
68+
}
69+
}
70+
71+
72+
@pytest.fixture()
73+
def successfully_action():
74+
return {
75+
"action": {
76+
"id": 2,
77+
"command": "stop_server",
78+
"status": "success",
79+
"progress": 100,
80+
"started": "2016-01-30T23:55:00+00:00",
81+
"finished": "2016-01-30T23:56:00+00:00",
82+
"resources": [
83+
{
84+
"id": 42,
85+
"type": "server"
86+
}
87+
],
88+
"error": {
89+
"code": "action_failed",
90+
"message": "Action failed"
91+
}
92+
}
93+
}
94+
95+
96+
@pytest.fixture()
97+
def failed_action():
98+
return {
99+
"action": {
100+
"id": 2,
101+
"command": "stop_server",
102+
"status": "error",
103+
"progress": 100,
104+
"started": "2016-01-30T23:55:00+00:00",
105+
"finished": "2016-01-30T23:56:00+00:00",
106+
"resources": [
107+
{
108+
"id": 42,
109+
"type": "server"
110+
}
111+
],
112+
"error": {
113+
"code": "action_failed",
114+
"message": "Action failed"
115+
}
116+
}
117+
}

tests/unit/actions/test_client.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,37 @@
11
import mock
22
import pytest
33

4-
from hcloud.actions.client import ActionsClient
4+
from hcloud.actions.client import ActionsClient, BoundAction
5+
from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException
6+
7+
8+
class TestBoundAction(object):
9+
@pytest.fixture()
10+
def bound_running_action(self, mocked_requests):
11+
return BoundAction(client=ActionsClient(client=mocked_requests), data=dict(id=14, status=Action.STATUS_RUNNING))
12+
13+
def test_wait_until_finished(self, bound_running_action, mocked_requests, running_action, successfully_action):
14+
mocked_requests.request.side_effect = [running_action, successfully_action]
15+
bound_running_action.wait_until_finished()
16+
assert bound_running_action.status == "success"
17+
assert mocked_requests.request.call_count == 2
18+
19+
def test_wait_until_finished_with_error(self, bound_running_action, mocked_requests, running_action, failed_action):
20+
mocked_requests.request.side_effect = [running_action, failed_action]
21+
with pytest.raises(ActionFailedException) as exception_info:
22+
bound_running_action.wait_until_finished()
23+
24+
assert bound_running_action.status == "error"
25+
assert exception_info.value.action.id == 2
26+
27+
def test_wait_until_finished_max_retries(self, bound_running_action, mocked_requests, running_action, successfully_action):
28+
mocked_requests.request.side_effect = [running_action, running_action, successfully_action]
29+
with pytest.raises(ActionTimeoutException) as exception_info:
30+
bound_running_action.wait_until_finished(max_retries=1)
31+
32+
assert bound_running_action.status == "running"
33+
assert exception_info.value.action.id == 2
34+
assert mocked_requests.request.call_count == 1
535

636

737
class TestActionsClient(object):

0 commit comments

Comments
 (0)