From 894b33de7ed685658d22e91f8d954a6773f40862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Mon, 11 Feb 2019 14:44:17 +0100 Subject: [PATCH 01/10] Closes #4 --- hcloud/actions/client.py | 18 +++++--- hcloud/actions/domain.py | 9 ++++ hcloud/hcloud.py | 4 ++ tests/unit/actions/conftest.py | 72 +++++++++++++++++++++++++++++++ tests/unit/actions/test_client.py | 26 ++++++++++- 5 files changed, 123 insertions(+), 6 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index aa6ed9d8..3deb0e46 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,12 +1,20 @@ # -*- coding: utf-8 -*- from hcloud.core.client import ClientEntityBase, BoundModelBase - -from hcloud.actions.domain import Action +from hcloud.actions.domain import Action, ActionFailedException +from hcloud import hcloud +import time class BoundAction(BoundModelBase): model = Action + def wait_until_finished(self): + while self.status == self.STATE_RUNNING: + self.reload() + time.sleep(hcloud.HcloudClient.poll_interval) + if self.status == self.STATE_ERROR: + raise ActionFailedException(action=self) + class ActionsClient(ClientEntityBase): results_list_attribute_name = 'actions' @@ -17,9 +25,9 @@ def get_by_id(self, id): return BoundAction(self, response['action']) def get_list(self, - status=None, # type: Optional[List[str]] - sort=None, # type: Optional[List[str]] - page=None, # type: Optional[int] + status=None, # type: Optional[List[str]] + sort=None, # type: Optional[List[str]] + page=None, # type: Optional[int] per_page=None, # type: Optional[int] ): # type: (...) -> PageResults[List[BoundAction]] diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index e752b401..dbf40e9f 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -5,6 +5,10 @@ class Action(BaseDomain): + STATE_RUNNING = "running" + STATE_SUCCESS = "success" + STATE_ERROR = "error" + started = ISODateTime() finished = ISODateTime() @@ -36,3 +40,8 @@ def __init__( self.finished = finished self.resources = resources self.error = error + + +class ActionFailedException(Exception): + def __init__(self, action): + self.action = action diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index 4e20f243..d849c68c 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -28,6 +28,7 @@ def __init__(self, code, message, details): class HcloudClient(object): version = VERSION retry_wait_time = 0.5 + poll_interval = 1 def __init__(self, token): self.token = token @@ -54,6 +55,9 @@ def _get_headers(self): } return headers + def with_poll_interval(self, poll_interval): + self.poll_interval = poll_interval + def _raise_exception_from_response(self, response): raise HcloudAPIException( code=response.status_code, diff --git a/tests/unit/actions/conftest.py b/tests/unit/actions/conftest.py index 54344701..3d358587 100644 --- a/tests/unit/actions/conftest.py +++ b/tests/unit/actions/conftest.py @@ -43,3 +43,75 @@ def generic_action_list(): } ] } + + +@pytest.fixture() +def running_action(): + return { + "action": { + "id": 2, + "command": "stop_server", + "status": "running", + "progress": 100, + "started": "2016-01-30T23:55:00+00:00", + "finished": "2016-01-30T23:56:00+00:00", + "resources": [ + { + "id": 42, + "type": "server" + } + ], + "error": { + "code": "action_failed", + "message": "Action failed" + } + } + } + + +@pytest.fixture() +def successfully_action(): + return { + "action": { + "id": 2, + "command": "stop_server", + "status": "success", + "progress": 100, + "started": "2016-01-30T23:55:00+00:00", + "finished": "2016-01-30T23:56:00+00:00", + "resources": [ + { + "id": 42, + "type": "server" + } + ], + "error": { + "code": "action_failed", + "message": "Action failed" + } + } + } + + +@pytest.fixture() +def failed_action(): + return { + "action": { + "id": 2, + "command": "stop_server", + "status": "error", + "progress": 100, + "started": "2016-01-30T23:55:00+00:00", + "finished": "2016-01-30T23:56:00+00:00", + "resources": [ + { + "id": 42, + "type": "server" + } + ], + "error": { + "code": "action_failed", + "message": "Action failed" + } + } + } \ No newline at end of file diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index 21d283bf..b90ed5f6 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -1,7 +1,31 @@ import mock import pytest +import time -from hcloud.actions.client import ActionsClient +from hcloud.actions.client import ActionsClient, BoundAction +from hcloud.actions.domain import Action, ActionFailedException + + +class TestBoundAction(object): + @pytest.fixture() + def bound_running_action(self, mocked_requests): + return BoundAction(client=ActionsClient(client=mocked_requests), data=dict(id=14, status=Action.STATE_RUNNING)) + + def test_wait_until_finished(self, bound_running_action, mocked_requests, running_action, successfully_action): + mocked_requests.request.side_effect = [running_action, successfully_action] + start_time = round(time.time()) + bound_running_action.wait_until_finished() + end_time = round(time.time()) + assert bound_running_action.status == "success" + assert end_time - start_time == 2 # We should only have a wait timeout from 1 sec + + def test_wait_until_finished_with_error(self, bound_running_action, mocked_requests, running_action, failed_action): + mocked_requests.request.side_effect = [running_action, failed_action] + with pytest.raises(ActionFailedException) as exception_info: + bound_running_action.wait_until_finished() + + assert bound_running_action.status == "error" + assert exception_info.value.action.id == 2 class TestActionsClient(object): From 2d5911f940f382b5c5cd7c27e6254ba9c1f1b9fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Mon, 11 Feb 2019 16:14:57 +0100 Subject: [PATCH 02/10] Fix missing new line at the end --- tests/unit/actions/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/actions/conftest.py b/tests/unit/actions/conftest.py index 3d358587..811277c0 100644 --- a/tests/unit/actions/conftest.py +++ b/tests/unit/actions/conftest.py @@ -114,4 +114,4 @@ def failed_action(): "message": "Action failed" } } - } \ No newline at end of file + } From c59707b750884448674c2cd5b57b5f062524bde0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Tue, 12 Feb 2019 06:52:47 +0100 Subject: [PATCH 03/10] Fix tests --- hcloud/actions/client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 3deb0e46..377e6498 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -1,17 +1,18 @@ # -*- coding: utf-8 -*- +import time + from hcloud.core.client import ClientEntityBase, BoundModelBase from hcloud.actions.domain import Action, ActionFailedException -from hcloud import hcloud -import time class BoundAction(BoundModelBase): model = Action def wait_until_finished(self): + from hcloud import HcloudClient while self.status == self.STATE_RUNNING: self.reload() - time.sleep(hcloud.HcloudClient.poll_interval) + time.sleep(HcloudClient.poll_interval) if self.status == self.STATE_ERROR: raise ActionFailedException(action=self) From 3aeeaa0761c965c9b18feb47e65f2d031fd36268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 13 Feb 2019 11:12:09 +0100 Subject: [PATCH 04/10] Apply review results --- hcloud/actions/client.py | 4 ++-- tests/unit/actions/test_client.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 377e6498..82c3d0db 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -10,10 +10,10 @@ class BoundAction(BoundModelBase): def wait_until_finished(self): from hcloud import HcloudClient - while self.status == self.STATE_RUNNING: + while self.status == Action.STATE_RUNNING: self.reload() time.sleep(HcloudClient.poll_interval) - if self.status == self.STATE_ERROR: + if self.status == Action.STATE_ERROR: raise ActionFailedException(action=self) diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index b90ed5f6..718b7f2e 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -1,6 +1,5 @@ import mock import pytest -import time from hcloud.actions.client import ActionsClient, BoundAction from hcloud.actions.domain import Action, ActionFailedException @@ -13,11 +12,9 @@ def bound_running_action(self, mocked_requests): def test_wait_until_finished(self, bound_running_action, mocked_requests, running_action, successfully_action): mocked_requests.request.side_effect = [running_action, successfully_action] - start_time = round(time.time()) bound_running_action.wait_until_finished() - end_time = round(time.time()) assert bound_running_action.status == "success" - assert end_time - start_time == 2 # We should only have a wait timeout from 1 sec + assert mocked_requests.request.call_count == 2 def test_wait_until_finished_with_error(self, bound_running_action, mocked_requests, running_action, failed_action): mocked_requests.request.side_effect = [running_action, failed_action] From f49c01e0dfba9a42ecbf62c1c1fe9b5384748439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 13 Feb 2019 13:27:12 +0100 Subject: [PATCH 05/10] Test with_poll_interval Use correct poll_interval variable from client --- hcloud/actions/client.py | 3 +-- hcloud/hcloud.py | 1 + tests/unit/test_hcloud.py | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 82c3d0db..21fc6fde 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -9,10 +9,9 @@ class BoundAction(BoundModelBase): model = Action def wait_until_finished(self): - from hcloud import HcloudClient while self.status == Action.STATE_RUNNING: self.reload() - time.sleep(HcloudClient.poll_interval) + time.sleep(self._client._client.poll_interval) if self.status == Action.STATE_ERROR: raise ActionFailedException(action=self) diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index d849c68c..6da72ef9 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -57,6 +57,7 @@ def _get_headers(self): def with_poll_interval(self, poll_interval): self.poll_interval = poll_interval + return self def _raise_exception_from_response(self, response): raise HcloudAPIException( diff --git a/tests/unit/test_hcloud.py b/tests/unit/test_hcloud.py index 2215c11a..00355294 100644 --- a/tests/unit/test_hcloud.py +++ b/tests/unit/test_hcloud.py @@ -55,6 +55,11 @@ def test__get_user_agent(self, client): user_agent = client._get_user_agent() assert user_agent == "hcloud-python/0.0.0" + def test_with_poll_interval(self, client): + assert client.poll_interval == 1 + client.with_poll_interval(0.5) + assert client.poll_interval == 0.5 + def test__get_headers(self, client): headers = client._get_headers() assert headers == { From 68bf32f9dadb2861598a7db3396cd0a08cfbe149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 13 Feb 2019 14:34:35 +0100 Subject: [PATCH 06/10] remove with_poll_interval method --- hcloud/hcloud.py | 8 ++------ tests/unit/test_hcloud.py | 7 +++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/hcloud/hcloud.py b/hcloud/hcloud.py index 6da72ef9..83e3b6dc 100644 --- a/hcloud/hcloud.py +++ b/hcloud/hcloud.py @@ -28,10 +28,10 @@ def __init__(self, code, message, details): class HcloudClient(object): version = VERSION retry_wait_time = 0.5 - poll_interval = 1 - def __init__(self, token): + def __init__(self, token, poll_interval=1): self.token = token + self.poll_interval = poll_interval self.api_endpoint = "https://api.hetzner.cloud/v1" self.datacenters = DatacentersClient(self) @@ -55,10 +55,6 @@ def _get_headers(self): } return headers - def with_poll_interval(self, poll_interval): - self.poll_interval = poll_interval - return self - def _raise_exception_from_response(self, response): raise HcloudAPIException( code=response.status_code, diff --git a/tests/unit/test_hcloud.py b/tests/unit/test_hcloud.py index 00355294..ba090bda 100644 --- a/tests/unit/test_hcloud.py +++ b/tests/unit/test_hcloud.py @@ -55,10 +55,9 @@ def test__get_user_agent(self, client): user_agent = client._get_user_agent() assert user_agent == "hcloud-python/0.0.0" - def test_with_poll_interval(self, client): - assert client.poll_interval == 1 - client.with_poll_interval(0.5) - assert client.poll_interval == 0.5 + def test_with_poll_interval(self): + client = HcloudClient(token="project_token", poll_interval=5) + assert client.poll_interval == 5 def test__get_headers(self, client): headers = client._get_headers() From 003b1e990dd815d562cb6996286076aca9958f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 13 Feb 2019 14:41:40 +0100 Subject: [PATCH 07/10] Rename Action Status from STATE_* to STATUS_* --- hcloud/actions/client.py | 4 ++-- hcloud/actions/domain.py | 6 +++--- tests/unit/actions/test_client.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 21fc6fde..8a8d9fe3 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -9,10 +9,10 @@ class BoundAction(BoundModelBase): model = Action def wait_until_finished(self): - while self.status == Action.STATE_RUNNING: + while self.status == Action.STATUS_RUNNING: self.reload() time.sleep(self._client._client.poll_interval) - if self.status == Action.STATE_ERROR: + if self.status == Action.STATUS_ERROR: raise ActionFailedException(action=self) diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index dbf40e9f..40e1f747 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -5,9 +5,9 @@ class Action(BaseDomain): - STATE_RUNNING = "running" - STATE_SUCCESS = "success" - STATE_ERROR = "error" + STATUS_RUNNING = "running" + STATUS_SUCCESS = "success" + STATUS_ERROR = "error" started = ISODateTime() finished = ISODateTime() diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index 718b7f2e..31969fee 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -8,7 +8,7 @@ class TestBoundAction(object): @pytest.fixture() def bound_running_action(self, mocked_requests): - return BoundAction(client=ActionsClient(client=mocked_requests), data=dict(id=14, status=Action.STATE_RUNNING)) + return BoundAction(client=ActionsClient(client=mocked_requests), data=dict(id=14, status=Action.STATUS_RUNNING)) def test_wait_until_finished(self, bound_running_action, mocked_requests, running_action, successfully_action): mocked_requests.request.side_effect = [running_action, successfully_action] From ea8cf356bd77f06461b73fd834e5187a73b7762c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 13 Feb 2019 15:20:06 +0100 Subject: [PATCH 08/10] remove with_poll_interval test --- tests/unit/test_hcloud.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/unit/test_hcloud.py b/tests/unit/test_hcloud.py index ba090bda..2215c11a 100644 --- a/tests/unit/test_hcloud.py +++ b/tests/unit/test_hcloud.py @@ -55,10 +55,6 @@ def test__get_user_agent(self, client): user_agent = client._get_user_agent() assert user_agent == "hcloud-python/0.0.0" - def test_with_poll_interval(self): - client = HcloudClient(token="project_token", poll_interval=5) - assert client.poll_interval == 5 - def test__get_headers(self, client): headers = client._get_headers() assert headers == { From fd91a1a9492422e036534e44070e5044a60671ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Wed, 13 Feb 2019 18:01:52 +0100 Subject: [PATCH 09/10] Add a default timeout of 100sec --- hcloud/actions/client.py | 13 +++++++++---- hcloud/actions/domain.py | 5 +++++ tests/unit/actions/test_client.py | 11 ++++++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 8a8d9fe3..3839f93e 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -2,16 +2,21 @@ import time from hcloud.core.client import ClientEntityBase, BoundModelBase -from hcloud.actions.domain import Action, ActionFailedException +from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException class BoundAction(BoundModelBase): model = Action - def wait_until_finished(self): + def wait_until_finished(self, timeout=100): while self.status == Action.STATUS_RUNNING: - self.reload() - time.sleep(self._client._client.poll_interval) + if timeout > 0: + self.reload() + time.sleep(self._client._client.poll_interval) + timeout = timeout - 1 + else: + raise ActionTimeoutException(action=self) + if self.status == Action.STATUS_ERROR: raise ActionFailedException(action=self) diff --git a/hcloud/actions/domain.py b/hcloud/actions/domain.py index 40e1f747..05df5980 100644 --- a/hcloud/actions/domain.py +++ b/hcloud/actions/domain.py @@ -45,3 +45,8 @@ def __init__( class ActionFailedException(Exception): def __init__(self, action): self.action = action + + +class ActionTimeoutException(Exception): + def __init__(self, action): + self.action = action diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index 31969fee..ccf47778 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -2,7 +2,7 @@ import pytest from hcloud.actions.client import ActionsClient, BoundAction -from hcloud.actions.domain import Action, ActionFailedException +from hcloud.actions.domain import Action, ActionFailedException, ActionTimeoutException class TestBoundAction(object): @@ -24,6 +24,15 @@ def test_wait_until_finished_with_error(self, bound_running_action, mocked_reque assert bound_running_action.status == "error" assert exception_info.value.action.id == 2 + def test_wait_until_finished_timeout(self, bound_running_action, mocked_requests, running_action, successfully_action): + mocked_requests.request.side_effect = [running_action, running_action, successfully_action] + with pytest.raises(ActionTimeoutException) as exception_info: + bound_running_action.wait_until_finished(timeout=1) + + assert bound_running_action.status == "running" + assert exception_info.value.action.id == 2 + assert mocked_requests.request.call_count == 1 + class TestActionsClient(object): From c85f65ade0f2d516eaf72163a10552e3a8fe7400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=A4mmerling?= Date: Thu, 14 Feb 2019 09:43:18 +0100 Subject: [PATCH 10/10] Rename timeout to max_retries --- hcloud/actions/client.py | 6 +++--- tests/unit/actions/test_client.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hcloud/actions/client.py b/hcloud/actions/client.py index 3839f93e..06dda1be 100644 --- a/hcloud/actions/client.py +++ b/hcloud/actions/client.py @@ -8,12 +8,12 @@ class BoundAction(BoundModelBase): model = Action - def wait_until_finished(self, timeout=100): + def wait_until_finished(self, max_retries=100): while self.status == Action.STATUS_RUNNING: - if timeout > 0: + if max_retries > 0: self.reload() time.sleep(self._client._client.poll_interval) - timeout = timeout - 1 + max_retries = max_retries - 1 else: raise ActionTimeoutException(action=self) diff --git a/tests/unit/actions/test_client.py b/tests/unit/actions/test_client.py index ccf47778..8cf97cd4 100644 --- a/tests/unit/actions/test_client.py +++ b/tests/unit/actions/test_client.py @@ -24,10 +24,10 @@ def test_wait_until_finished_with_error(self, bound_running_action, mocked_reque assert bound_running_action.status == "error" assert exception_info.value.action.id == 2 - def test_wait_until_finished_timeout(self, bound_running_action, mocked_requests, running_action, successfully_action): + def test_wait_until_finished_max_retries(self, bound_running_action, mocked_requests, running_action, successfully_action): mocked_requests.request.side_effect = [running_action, running_action, successfully_action] with pytest.raises(ActionTimeoutException) as exception_info: - bound_running_action.wait_until_finished(timeout=1) + bound_running_action.wait_until_finished(max_retries=1) assert bound_running_action.status == "running" assert exception_info.value.action.id == 2