Skip to content

Commit 803d982

Browse files
committed
add method for listing flows
so the API client can be used to see the current running and historical flows for a specific app. this is the same information that hypernode-log would show on a hypernode.
1 parent 6c79a06 commit 803d982

4 files changed

Lines changed: 261 additions & 1 deletion

File tree

hypernode_api_python/client.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
HYPERNODE_API_APP_DETAIL_WITH_ADDONS_ENDPOINT = "/v2/app/{}/with_addons?destroyed=false"
1010
HYPERNODE_API_APP_EAV_DESCRIPTION_ENDPOINT = "/v2/app/eav_descriptions/"
1111
HYPERNODE_API_APP_FLAVOR_ENDPOINT = "/v2/app/{}/flavor/"
12+
HYPERNODE_API_APP_FLOWS_ENDPOINT = "/logbook/v1/logbooks/{}/flows"
1213
HYPERNODE_API_BRANCHER_APP_ENDPOINT = "/v2/brancher/app/{}/"
1314
HYPERNODE_API_BRANCHER_ENDPOINT = "/v2/brancher/{}/"
1415
HYPERNODE_API_APP_NEXT_BEST_PLAN_ENDPOINT = "/v2/app/{}/next_best_plan/"
@@ -169,6 +170,88 @@ def get_app_flavor(self, app_name):
169170
"""
170171
return self.requests("GET", HYPERNODE_API_APP_FLAVOR_ENDPOINT.format(app_name))
171172

173+
def get_flows(self, app_name):
174+
"""
175+
List the flows for an app. Take note that this result is paginated and will not
176+
retrieve all flows. If you are looking to retrieve all flows and not those available
177+
on just the first page, look at the get_all_flows method instead.
178+
179+
Example:
180+
> client.get_flows('yourhypernodeappname').json()
181+
> {'count': 2,
182+
> 'next': None,
183+
> 'previous': None,
184+
> 'results': [{'uuid': '1d1b8437-c8c0-4e96-a28a-26cc311c1f4c',
185+
> 'state': 'success',
186+
> 'name': 'update_node',
187+
> 'created_at': '2023-03-04T14:42:24Z',
188+
> 'updated_at': '2023-03-04T14:42:57Z',
189+
> 'progress': {'running': [], 'total': 8, 'completed': 8},
190+
> 'logbook': 'yourhypernodeappname',
191+
> 'tracker': {'uuid': '6012bf2c-952d-4cb0-97ff-1022614b50b7',
192+
> 'description': None}},
193+
> {'uuid': 'fe696417-024e-4a57-8442-4967f6df24a3',
194+
> 'state': 'success',
195+
> 'name': 'create_backup',
196+
> 'created_at': '2023-03-04T14:13:00Z',
197+
> 'updated_at': '2023-03-04T14:13:12Z',
198+
> 'progress': {'running': [], 'total': 2, 'completed': 2},
199+
> 'logbook': 'yourhypernodeappname',
200+
> 'tracker': {'uuid': None, 'description': None}}]
201+
> }
202+
203+
204+
:param str app_name: The name of the app to get the flows for
205+
:return obj response: The request response object
206+
"""
207+
return self.requests("GET", HYPERNODE_API_APP_FLOWS_ENDPOINT.format(app_name))
208+
209+
def get_all_flows(self, app_name, limit=None):
210+
"""
211+
List all the flows for an app (paginate over the results), or
212+
retrieve results until we have enough (if limit is specified).
213+
Note that this method does not return a response object but a
214+
list of dicts instead.
215+
Example:
216+
> client.get_all_flows('yourhypernodeappname')
217+
> [{'uuid': '03bd6e10-5493-4ee8-92fc-cc429faebead',
218+
> 'state': None,
219+
> 'name': 'update_node',
220+
> 'created_at': '2023-03-05T14:01:56Z',
221+
> 'updated_at': None,
222+
> 'progress': {'running': [], 'total': 0, 'completed': 0},
223+
> 'logbook': 'yourhypernodeappname',
224+
> 'tracker': {'uuid': '0dd83d83-6b9b-4fb5-9665-79fcf8235069',
225+
> 'description': None}},
226+
> {'uuid': 'ace36ab9-323e-4a95-a0c6-46f96945b623',
227+
> 'state': None,
228+
> 'name': 'update_node',
229+
> 'created_at': '2023-03-05T14:01:55Z',
230+
> 'updated_at': None,
231+
> 'progress': {'running': [], 'total': 0, 'completed': 0},
232+
> 'logbook': 'yourhypernodeappname',
233+
> 'tracker': {'uuid': '0006206e-df48-4af4-8e7b-b3db86d35bb0',
234+
> 'description': None}},
235+
> ...
236+
> ]
237+
238+
:param str app_name: The name of the app to get all the flows for
239+
:param int limit: How many flows to get. If None is specified all
240+
available flows will be retrieved.
241+
:return list flows: A list of all flows
242+
"""
243+
flows = []
244+
next_url = HYPERNODE_API_APP_FLOWS_ENDPOINT.format(app_name)
245+
while next_url:
246+
if limit and len(flows) >= limit:
247+
break
248+
response = self.requests("GET", next_url.replace(HYPERNODE_API_URL, ""))
249+
response_data = response.json()
250+
results = response_data["results"]
251+
flows.extend(results)
252+
next_url = response_data["next"]
253+
return flows[:limit]
254+
172255
def get_slas(self):
173256
"""
174257
List the available SLAs

tests/client/test_get_all_flows.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
from unittest.mock import Mock, call
2+
3+
from tests.testcase import TestCase
4+
from hypernode_api_python.client import (
5+
HypernodeAPIPython,
6+
)
7+
8+
9+
class TestGetAllFlows(TestCase):
10+
def setUp(self):
11+
self.client = HypernodeAPIPython(token="mytoken")
12+
self.mock_request = Mock()
13+
self.client.requests = self.mock_request
14+
self.flow1 = {
15+
"uuid": "e1db2b60-882d-4b43-8910-ce6d38ca5393",
16+
"state": None,
17+
"name": "create_backup",
18+
"created_at": "2023-03-05T14:13:21Z",
19+
"updated_at": None,
20+
"progress": {"running": [], "total": 0, "completed": 0},
21+
"logbook": "my_app",
22+
"tracker": {"uuid": None, "description": None},
23+
}
24+
self.flow2 = {
25+
"uuid": "03bd6e10-5493-4ee8-92fc-cc429faebead",
26+
"state": None,
27+
"name": "update_node",
28+
"created_at": "2023-03-05T14:01:56Z",
29+
"updated_at": None,
30+
"progress": {"running": [], "total": 0, "completed": 0},
31+
"logbook": "my_app",
32+
"tracker": {
33+
"uuid": "0dd83d83-6b9b-4fb5-9665-79fcf8235069",
34+
"description": None,
35+
},
36+
}
37+
38+
def test_get_all_flows_returns_flows_if_only_one_page(self):
39+
self.mock_request.return_value.json.return_value = {
40+
"count": 2,
41+
"next": None,
42+
"previous": None,
43+
"results": [self.flow1, self.flow2],
44+
}
45+
46+
ret = self.client.get_all_flows("my_app")
47+
48+
expected_calls = [
49+
call("GET", "/logbook/v1/logbooks/my_app/flows"),
50+
call().json(),
51+
]
52+
self.assertEqual(expected_calls, self.mock_request.mock_calls)
53+
expected_results = [self.flow1, self.flow2]
54+
self.assertEqual(expected_results, ret)
55+
56+
def test_get_all_flows_returns_flows_if_only_one_page_but_limited_results_requested(
57+
self,
58+
):
59+
self.mock_request.return_value.json.return_value = {
60+
"count": 2,
61+
"next": None,
62+
"previous": None,
63+
"results": [self.flow1, self.flow2],
64+
}
65+
66+
ret = self.client.get_all_flows("my_app", limit=1)
67+
68+
expected_calls = [
69+
call("GET", "/logbook/v1/logbooks/my_app/flows"),
70+
call().json(),
71+
]
72+
self.assertEqual(expected_calls, self.mock_request.mock_calls)
73+
expected_results = [self.flow1]
74+
self.assertEqual(expected_results, ret)
75+
76+
def test_get_all_flows_returns_flows_if_more_than_one_page(self):
77+
self.mock_request.return_value.json.side_effect = [
78+
{
79+
"count": 101,
80+
"next": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50",
81+
"previous": None,
82+
"results": [self.flow1, self.flow2] * 25,
83+
},
84+
{
85+
"count": 101,
86+
"next": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=100",
87+
"previous": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50",
88+
"results": [self.flow1, self.flow2] * 25,
89+
},
90+
{
91+
"count": 101,
92+
"next": None,
93+
"previous": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=100",
94+
"results": [self.flow1],
95+
},
96+
]
97+
98+
ret = self.client.get_all_flows("my_app")
99+
100+
expected_calls = [
101+
call("GET", "/logbook/v1/logbooks/my_app/flows"),
102+
call().json(),
103+
call("GET", "/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50"),
104+
call().json(),
105+
call("GET", "/logbook/v1/logbooks/my_app/flows/?limit=50&offset=100"),
106+
call().json(),
107+
]
108+
self.assertEqual(expected_calls, self.mock_request.mock_calls)
109+
expected_results = [self.flow1, self.flow2] * 50 + [self.flow1]
110+
self.assertEqual(expected_results, ret)
111+
112+
def test_get_all_flows_returns_flows_if_more_than_one_page_but_limited_results_requested(
113+
self,
114+
):
115+
self.mock_request.return_value.json.side_effect = [
116+
{
117+
"count": 101,
118+
"next": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50",
119+
"previous": None,
120+
"results": [self.flow1, self.flow2] * 25,
121+
},
122+
{
123+
"count": 101,
124+
"next": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=100",
125+
"previous": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50",
126+
"results": [self.flow1, self.flow2] * 25,
127+
},
128+
{
129+
"count": 101,
130+
"next": None,
131+
"previous": "https://api.hypernode.com/logbook/v1/logbooks/my_app/flows/?limit=50&offset=100",
132+
"results": [self.flow1],
133+
},
134+
]
135+
136+
ret = self.client.get_all_flows("my_app", limit=51)
137+
138+
expected_calls = [
139+
call("GET", "/logbook/v1/logbooks/my_app/flows"),
140+
call().json(),
141+
call("GET", "/logbook/v1/logbooks/my_app/flows/?limit=50&offset=50"),
142+
call().json(),
143+
]
144+
self.assertEqual(expected_calls, self.mock_request.mock_calls)
145+
expected_results = [self.flow1, self.flow2] * 25 + [self.flow1]
146+
self.assertEqual(expected_results, ret)

tests/client/test_get_app_flavor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def setUp(self):
1616
def test_app_flavor_endpoint_is_correct(self):
1717
self.assertEqual("/v2/app/{}/flavor/", HYPERNODE_API_APP_FLAVOR_ENDPOINT)
1818

19-
def test_calls_app_flavor_endpoint_propertly(self):
19+
def test_calls_app_flavor_endpoint_properly(self):
2020
self.client.get_app_flavor("my_app")
2121

2222
self.mock_request.assert_called_once_with("GET", "/v2/app/my_app/flavor/")

tests/client/test_get_flows.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from unittest.mock import Mock
2+
3+
from tests.testcase import TestCase
4+
from hypernode_api_python.client import (
5+
HypernodeAPIPython,
6+
HYPERNODE_API_APP_FLOWS_ENDPOINT,
7+
)
8+
9+
10+
class TestGetFlows(TestCase):
11+
def setUp(self):
12+
self.client = HypernodeAPIPython(token="mytoken")
13+
self.mock_request = Mock()
14+
self.client.requests = self.mock_request
15+
16+
def test_flows_endpoint_is_correct(self):
17+
self.assertEqual(
18+
"/logbook/v1/logbooks/{}/flows", HYPERNODE_API_APP_FLOWS_ENDPOINT
19+
)
20+
21+
def test_calls_flows_endpoint_properly(self):
22+
self.client.get_flows("my_app")
23+
24+
self.mock_request.assert_called_once_with(
25+
"GET", "/logbook/v1/logbooks/my_app/flows"
26+
)
27+
28+
def test_returns_flows_data(self):
29+
self.assertEqual(
30+
self.client.get_flows("my_app"), self.mock_request.return_value
31+
)

0 commit comments

Comments
 (0)