Skip to content

Commit

Permalink
fixup! fixup! client: add wait() method to poll for operation result
Browse files Browse the repository at this point in the history
  • Loading branch information
brutasse committed Dec 16, 2024
1 parent 3fb5d0b commit f271465
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 27 deletions.
37 changes: 27 additions & 10 deletions exoscale/api/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@
... version=version,
... )
>>> c.wait(operation["id"])
{'id': 'e2047130-b86e-11ef-83b3-0d8312b2c2d7', 'state': 'success', 'reference': {'id': '8561ee34-09f0-42da-a765-abde807f944b', 'link': '/v2/sks-cluster/8561ee34-09f0-42da-a765-abde807f944b', 'command': 'get-sks-cluster'}}
{'id': 'e2047130-b86e-11ef-83b3-0d8312b2c2d7',
'state': 'success',
'reference': {
'id': '8561ee34-09f0-42da-a765-abde807f944b',
'link': '/v2/sks-cluster/8561ee34-09f0-42da-a765-abde807f944b',
'command': 'get-sks-cluster'}}
"""

import copy
Expand All @@ -41,16 +46,16 @@
from pathlib import Path

from exoscale_auth import ExoscaleV2Auth

import requests

from .exceptions import (
ExoscaleAPIAuthException,
ExoscaleAPIClientException,
ExoscaleAPIServerException,
ExoscaleAPIAuthException,
)


import requests


with open(Path(__file__).parent.parent / "openapi.json", "r") as f:
API_SPEC = json.load(f)

Expand Down Expand Up @@ -185,6 +190,16 @@ def _poll_interval(run_time):
return interval


def _time():
return time.time()


def _sleep(start_time):
run_time = _time() - start_time
interval = _poll_interval(run_time)
return time.sleep(interval)


class BaseClient:
def __init__(self, key, secret, url=None, **kwargs):
if url is None:
Expand Down Expand Up @@ -274,12 +289,14 @@ def wait(self, operation_id: str, max_wait_time: int = None):
Args:
operation_id (str)
max_wait_time (int): When set, stop waiting after this time in seconds. Defaults to ``None``, which waits until operation completion.
max_wait_time (int): When set, stop waiting after this time in
seconds. Defaults to ``None``, which waits until operation
completion.
Returns:
{ret}
"""
start_time = time.time()
start_time = _time()
subsequent_errors = 0
while True:
try:
Expand All @@ -291,6 +308,7 @@ def wait(self, operation_id: str, max_wait_time: int = None):
raise ExoscaleAPIServerException(
"Server error while polling operation"
) from e
_sleep(start_time)
continue
state = result["state"]
if state == "success":
Expand All @@ -300,13 +318,12 @@ def wait(self, operation_id: str, max_wait_time: int = None):
f"Operation error: {state}, {result['reason']}"
)
elif state == "pending":
run_time = time.time() - start_time
run_time = _time() - start_time
if max_wait_time is not None and run_time > max_wait_time:
raise ExoscaleAPIClientException(
"Operation max wait time reached"
)
interval = _poll_interval(run_time)
time.sleep(interval)
_sleep(start_time)
else:
raise ExoscaleAPIServerException(
f"Invalid operation state: {state}"
Expand Down
113 changes: 96 additions & 17 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import pytest
from exoscale.api.v2 import Client, _poll_interval
import json

from unittest.mock import patch

from exoscale.api.exceptions import (
ExoscaleAPIAuthException,
ExoscaleAPIClientException,
ExoscaleAPIServerException,
ExoscaleAPIAuthException,
)
from exoscale.api.v2 import Client, _poll_interval

import pytest


def test_client_creation():
Expand Down Expand Up @@ -96,36 +101,110 @@ def test_operation_poll_failure(requests_mock):
raise AssertionError("exception not raised")


def test_operation_abort_on_500(requests_mock):
def test_operation_invalid_state(requests_mock):
requests_mock.get(
"https://api-ch-gva-2.exoscale.com/v2/operation/e2047130-b86e-11ef-83b3-0d8312b2c2d7", # noqa
status_code=500,
text='{"message": "server error"}',
status_code=200,
text='{"id": "4c5547c0-b870-11ef-83b3-0d8312b2c2d7", "state": "weird", "reference": {"id": "97d7426f-8b25-4591-91d5-4a19e9a1d61a", "link": "/v2/sks-cluster/97d7426f-8b25-4591-91d5-4a19e9a1d61a", "command": "get-sks-cluster"}}', # noqa
)

client = Client(key="EXOtest", secret="sdsd")
try:
client.wait(operation_id="e2047130-b86e-11ef-83b3-0d8312b2c2d7")
except ExoscaleAPIServerException as e:
assert "Server error while polling operation" in str(e)
assert "Invalid operation state: weird" in str(e)
else:
raise AssertionError("exception not raised")


def test_operation_invalid_state(requests_mock):
def _mock_poll_response(poll_counts, status_code=200, result="success"):
return [
{
"status_code": status_code,
"text": '{"id": "4c5547c0-b870-11ef-83b3-0d8312b2c2d7", "state": "pending", "reference": {"id": "97d7426f-8b25-4591-91d5-4a19e9a1d61a", "link": "/v2/sks-cluster/97d7426f-8b25-4591-91d5-4a19e9a1d61a", "command": "get-sks-cluster"}}', # noqa
}
] * (poll_counts - 1) + [
{
"status_code": status_code,
"text": json.dumps(
{
"id": "4c5547c0-b870-11ef-83b3-0d8312b2c2d7",
"state": result,
"reason": "some reason",
"reference": {
"id": "97d7426f-8b25-4591-91d5-4a19e9a1d61a",
"link": "/v2/sks-cluster/97d7426f-8b25-4591-91d5-4a19e9a1d61a", # noqa
"command": "get-sks-cluster",
},
}
),
}
]


def test_wait_time_success(requests_mock):
requests_mock.get(
"https://api-ch-gva-2.exoscale.com/v2/operation/e2047130-b86e-11ef-83b3-0d8312b2c2d7", # noqa
status_code=200,
text='{"id": "4c5547c0-b870-11ef-83b3-0d8312b2c2d7", "state": "weird", "reference": {"id": "97d7426f-8b25-4591-91d5-4a19e9a1d61a", "link": "/v2/sks-cluster/97d7426f-8b25-4591-91d5-4a19e9a1d61a", "command": "get-sks-cluster"}}', # noqa
_mock_poll_response(3),
)

client = Client(key="EXOtest", secret="sdsd")
try:
with patch(
"exoscale.api.v2._time",
side_effect=[
0, # start of poll
1, # duration of first loop: 1s
5, # duration of second loop: 4s
],
) as time, patch("exoscale.api.v2._sleep") as sleep:
client = Client(key="EXOtest", secret="sdsd")
client.wait(operation_id="e2047130-b86e-11ef-83b3-0d8312b2c2d7")
except ExoscaleAPIServerException as e:
assert "Invalid operation state: weird" in str(e)
else:
raise AssertionError("exception not raised")
assert len(time.call_args_list) == 3
assert len(sleep.call_args_list) == 2


def test_wait_time_poll_errors(requests_mock):
requests_mock.get(
"https://api-ch-gva-2.exoscale.com/v2/operation/e2047130-b86e-11ef-83b3-0d8312b2c2d7", # noqa
_mock_poll_response(6, status_code=500),
)
with patch(
"exoscale.api.v2._time",
side_effect=[
0, # start of poll
],
) as time, patch("exoscale.api.v2._sleep") as sleep:
client = Client(key="EXOtest", secret="sdsd")
try:
client.wait(operation_id="e2047130-b86e-11ef-83b3-0d8312b2c2d7")
except ExoscaleAPIServerException:
pass
else:
raise AssertionError("Exception not raised")
assert len(time.call_args_list) == 1
assert len(sleep.call_args_list) == 4


def test_wait_time_failure(requests_mock):
requests_mock.get(
"https://api-ch-gva-2.exoscale.com/v2/operation/e2047130-b86e-11ef-83b3-0d8312b2c2d7", # noqa
_mock_poll_response(3, result="failure"),
)
with patch(
"exoscale.api.v2._time",
side_effect=[
0, # start of poll
1, # duration of first loop: 1s
5, # duration of second loop: 4s
],
) as time, patch("exoscale.api.v2._sleep") as sleep:
client = Client(key="EXOtest", secret="sdsd")
try:
client.wait(operation_id="e2047130-b86e-11ef-83b3-0d8312b2c2d7")
except ExoscaleAPIServerException as e:
assert "Operation error" in str(e)
else:
raise AssertionError("Exception not raised")
assert len(time.call_args_list) == 3
assert len(sleep.call_args_list) == 2


if __name__ == "__main__":
Expand Down

0 comments on commit f271465

Please sign in to comment.