From 0beb45dfcd9773773852c6bdfc27bcd7f69058b8 Mon Sep 17 00:00:00 2001 From: Will Foster Date: Thu, 14 May 2026 15:20:23 +0100 Subject: [PATCH 1/3] feat: add os-list functionality to library --- src/quads_lib/quads.py | 15 ++++++- tests/test_quads.py | 94 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 95 insertions(+), 14 deletions(-) diff --git a/src/quads_lib/quads.py b/src/quads_lib/quads.py index bb82fd2..9af7d84 100644 --- a/src/quads_lib/quads.py +++ b/src/quads_lib/quads.py @@ -211,7 +211,9 @@ def filter_available(self, data: dict) -> dict: def create_assignment(self, data: dict) -> dict: response = self.post("assignments", data) if response and {"id", "cloud"} <= response.keys(): - print(f"Assignment created - ID: {response['id']}, Cloud: {response['cloud']['name']}") + print( + f"Assignment created - ID: {response['id']}, Cloud: {response['cloud']['name']}" + ) return response @returns("Assignment") @@ -219,7 +221,9 @@ def create_self_assignment(self, data: dict) -> dict: endpoint = Path("assignments") / "self" response = self.post(str(endpoint), data) if response and {"id", "cloud"} <= response.keys(): - print(f"Self-assignment created - ID: {response['id']}, Cloud: {response['cloud']['name']}") + print( + f"Self-assignment created - ID: {response['id']}, Cloud: {response['cloud']['name']}" + ) return response @returns("Assignment") @@ -348,6 +352,13 @@ def update_vlan(self, vlan_id: int, data: dict) -> dict: def create_vlan(self, data: dict) -> dict: return self.post("vlans", data) + # OS + @returns("List[OS]") + def get_os_list(self) -> dict: + endpoint = Path("hosts") / "os_list" + json_response = self.get(str(endpoint)) + return json_response + # Moves def get_moves(self, date: Optional[str] = None) -> dict: url = "moves" diff --git a/tests/test_quads.py b/tests/test_quads.py index ebf40f3..074c201 100644 --- a/tests/test_quads.py +++ b/tests/test_quads.py @@ -933,7 +933,11 @@ def test_get_schedules(self, mock_get): @patch("requests.Session.request") def test_get_schedules_with_params(self, mock_get): query_data = {"cloud": "cloud1", "start": "2024-03-20"} - expected_response = {"schedules": [{"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"}]} + expected_response = { + "schedules": [ + {"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"} + ] + } mock_response = Mock() mock_response.json.return_value = expected_response mock_get.return_value = mock_response @@ -941,14 +945,20 @@ def test_get_schedules_with_params(self, mock_get): result = self.api.get_schedules(query_data) mock_get.assert_called_once() - assert str(mock_get.call_args[0][1]).endswith("/schedules?cloud=cloud1&start=2024-03-20") or str(mock_get.call_args[0][1]).endswith( + assert str(mock_get.call_args[0][1]).endswith( + "/schedules?cloud=cloud1&start=2024-03-20" + ) or str(mock_get.call_args[0][1]).endswith( "/schedules?start=2024-03-20&cloud=cloud1" ) assert result == expected_response @patch("requests.Session.request") def test_get_current_schedules(self, mock_get): - expected_response = {"schedules": [{"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"}]} + expected_response = { + "schedules": [ + {"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"} + ] + } mock_response = Mock() mock_response.json.return_value = expected_response mock_get.return_value = mock_response @@ -962,7 +972,11 @@ def test_get_current_schedules(self, mock_get): @patch("requests.Session.request") def test_get_current_schedules_with_params(self, mock_get): query_data = {"cloud": "cloud1"} - expected_response = {"schedules": [{"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"}]} + expected_response = { + "schedules": [ + {"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"} + ] + } mock_response = Mock() mock_response.json.return_value = expected_response mock_get.return_value = mock_response @@ -1041,7 +1055,11 @@ def test_get_future_schedules(self, mock_get): @patch("requests.Session.request") def test_get_future_schedules_with_params(self, mock_get): query_data = {"cloud": "cloud1"} - expected_response = {"schedules": [{"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"}]} + expected_response = { + "schedules": [ + {"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"} + ] + } mock_response = Mock() mock_response.json.return_value = expected_response mock_get.return_value = mock_response @@ -1293,7 +1311,9 @@ def test_update_notification(self, mock_patch): result = self.api.update_notification(notification_id, update_data) mock_patch.assert_called_once() - assert str(mock_patch.call_args[0][1]).endswith(f"/notifications/{notification_id}") + assert str(mock_patch.call_args[0][1]).endswith( + f"/notifications/{notification_id}" + ) assert mock_patch.call_args[1]["json"] == update_data assert result == update_data @@ -1358,7 +1378,9 @@ def test_get_active_cloud_assignment(self, mock_get): result = self.api.get_active_cloud_assignment(cloud_name) mock_get.assert_called_once() - assert str(mock_get.call_args[0][1]).endswith(f"/assignments/active/{cloud_name}") + assert str(mock_get.call_args[0][1]).endswith( + f"/assignments/active/{cloud_name}" + ) assert result == expected_response @patch("requests.Session.request") @@ -1444,7 +1466,9 @@ def test_remove_interface(self, mock_delete): result = self.api.remove_interface(hostname, if_name) mock_delete.assert_called_once() - assert str(mock_delete.call_args[0][1]).endswith(f"/interfaces/{hostname}/{if_name}") + assert str(mock_delete.call_args[0][1]).endswith( + f"/interfaces/{hostname}/{if_name}" + ) assert result == {} @patch("requests.Session.request") @@ -1694,6 +1718,33 @@ def test_create_vlan(self, mock_post): assert mock_post.call_args[1]["json"] == vlan_data assert result == vlan_data + @patch("requests.Session.request") + def test_get_os_list(self, mock_get): + expected_response = [ + {"Id": 1, "Title": "RHEL 9.4", "Release Name": "Plow", "Family": "rhel"}, + {"Id": 2, "Title": "RHEL 8.10", "Release Name": "Ootpa", "Family": "rhel"}, + ] + mock_response = Mock() + mock_response.json.return_value = expected_response + mock_get.return_value = mock_response + + result = self.api.get_os_list() + + mock_get.assert_called_once() + assert str(mock_get.call_args[0][1]).endswith("/hosts/os_list") + assert result == expected_response + + @patch("requests.Session.request") + def test_get_os_list_empty(self, mock_get): + mock_response = Mock() + mock_response.json.return_value = [] + mock_get.return_value = mock_response + + result = self.api.get_os_list() + + mock_get.assert_called_once() + assert result == [] + @patch("requests.Session.request") def test_get_moves(self, mock_get): expected_response = { @@ -1725,7 +1776,11 @@ def test_get_moves(self, mock_get): @patch("requests.Session.request") def test_get_moves_with_date(self, mock_get): date = "2024-03-20" - expected_response = {"moves": [{"id": 1, "host": "host1", "from_cloud": "cloud1", "to_cloud": "cloud2"}]} + expected_response = { + "moves": [ + {"id": 1, "host": "host1", "from_cloud": "cloud1", "to_cloud": "cloud2"} + ] + } mock_response = Mock() mock_response.json.return_value = expected_response mock_get.return_value = mock_response @@ -1776,6 +1831,15 @@ def test_get_vlans_error(self, mock_get): with pytest.raises(APIServerException, match="Check the flask server logs"): self.api.get_vlans() + @patch("requests.Session.request") + def test_get_os_list_error(self, mock_get): + mock_response = Mock() + mock_response.status_code = 500 + mock_get.return_value = mock_response + + with pytest.raises(APIServerException, match="Check the flask server logs"): + self.api.get_os_list() + @patch("requests.Session.request") def test_get_version_error(self, mock_get): mock_response = Mock() @@ -1807,7 +1871,9 @@ def test_create_self_assignment(self, mock_post): with patch.object(self.api, "post") as mock_post: mock_post.return_value = expected_response result = self.api.create_self_assignment(test_data) - mock_post.assert_called_once_with(str(Path("assignments") / "self"), test_data) + mock_post.assert_called_once_with( + str(Path("assignments") / "self"), test_data + ) assert result == expected_response @patch("requests.Session.request") @@ -1983,7 +2049,9 @@ def test_create_self_assignment_logging(self, mock_request, mock_print): self.api.create_self_assignment(assignment_data) - mock_print.assert_called_once_with("Self-assignment created - ID: 123, Cloud: cloud1") + mock_print.assert_called_once_with( + "Self-assignment created - ID: 123, Cloud: cloud1" + ) @patch("builtins.print") @patch("requests.Session.request") @@ -2040,7 +2108,9 @@ def setup(self): @pytest.fixture def quads_base(self): - return QuadsBase(username=self.username, password=self.password, base_url=self.base_url) + return QuadsBase( + username=self.username, password=self.password, base_url=self.base_url + ) def test_context_manager_enter(self, quads_base): quads_base.login = Mock() From 4bcae9a85a86fafbf98ff7fe85e476d49722a211 Mon Sep 17 00:00:00 2001 From: Will Foster Date: Thu, 14 May 2026 15:28:11 +0100 Subject: [PATCH 2/3] chore: fix tests --- src/quads_lib/quads.py | 8 ++---- tests/test_quads.py | 58 +++++++++--------------------------------- 2 files changed, 14 insertions(+), 52 deletions(-) diff --git a/src/quads_lib/quads.py b/src/quads_lib/quads.py index 9af7d84..986d5c8 100644 --- a/src/quads_lib/quads.py +++ b/src/quads_lib/quads.py @@ -211,9 +211,7 @@ def filter_available(self, data: dict) -> dict: def create_assignment(self, data: dict) -> dict: response = self.post("assignments", data) if response and {"id", "cloud"} <= response.keys(): - print( - f"Assignment created - ID: {response['id']}, Cloud: {response['cloud']['name']}" - ) + print(f"Assignment created - ID: {response['id']}, Cloud: {response['cloud']['name']}") return response @returns("Assignment") @@ -221,9 +219,7 @@ def create_self_assignment(self, data: dict) -> dict: endpoint = Path("assignments") / "self" response = self.post(str(endpoint), data) if response and {"id", "cloud"} <= response.keys(): - print( - f"Self-assignment created - ID: {response['id']}, Cloud: {response['cloud']['name']}" - ) + print(f"Self-assignment created - ID: {response['id']}, Cloud: {response['cloud']['name']}") return response @returns("Assignment") diff --git a/tests/test_quads.py b/tests/test_quads.py index 074c201..10eccf7 100644 --- a/tests/test_quads.py +++ b/tests/test_quads.py @@ -933,11 +933,7 @@ def test_get_schedules(self, mock_get): @patch("requests.Session.request") def test_get_schedules_with_params(self, mock_get): query_data = {"cloud": "cloud1", "start": "2024-03-20"} - expected_response = { - "schedules": [ - {"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"} - ] - } + expected_response = {"schedules": [{"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"}]} mock_response = Mock() mock_response.json.return_value = expected_response mock_get.return_value = mock_response @@ -945,20 +941,14 @@ def test_get_schedules_with_params(self, mock_get): result = self.api.get_schedules(query_data) mock_get.assert_called_once() - assert str(mock_get.call_args[0][1]).endswith( - "/schedules?cloud=cloud1&start=2024-03-20" - ) or str(mock_get.call_args[0][1]).endswith( + assert str(mock_get.call_args[0][1]).endswith("/schedules?cloud=cloud1&start=2024-03-20") or str(mock_get.call_args[0][1]).endswith( "/schedules?start=2024-03-20&cloud=cloud1" ) assert result == expected_response @patch("requests.Session.request") def test_get_current_schedules(self, mock_get): - expected_response = { - "schedules": [ - {"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"} - ] - } + expected_response = {"schedules": [{"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"}]} mock_response = Mock() mock_response.json.return_value = expected_response mock_get.return_value = mock_response @@ -972,11 +962,7 @@ def test_get_current_schedules(self, mock_get): @patch("requests.Session.request") def test_get_current_schedules_with_params(self, mock_get): query_data = {"cloud": "cloud1"} - expected_response = { - "schedules": [ - {"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"} - ] - } + expected_response = {"schedules": [{"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"}]} mock_response = Mock() mock_response.json.return_value = expected_response mock_get.return_value = mock_response @@ -1055,11 +1041,7 @@ def test_get_future_schedules(self, mock_get): @patch("requests.Session.request") def test_get_future_schedules_with_params(self, mock_get): query_data = {"cloud": "cloud1"} - expected_response = { - "schedules": [ - {"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"} - ] - } + expected_response = {"schedules": [{"id": 1, "cloud": "cloud1", "start": "2024-03-20", "end": "2024-03-21"}]} mock_response = Mock() mock_response.json.return_value = expected_response mock_get.return_value = mock_response @@ -1311,9 +1293,7 @@ def test_update_notification(self, mock_patch): result = self.api.update_notification(notification_id, update_data) mock_patch.assert_called_once() - assert str(mock_patch.call_args[0][1]).endswith( - f"/notifications/{notification_id}" - ) + assert str(mock_patch.call_args[0][1]).endswith(f"/notifications/{notification_id}") assert mock_patch.call_args[1]["json"] == update_data assert result == update_data @@ -1378,9 +1358,7 @@ def test_get_active_cloud_assignment(self, mock_get): result = self.api.get_active_cloud_assignment(cloud_name) mock_get.assert_called_once() - assert str(mock_get.call_args[0][1]).endswith( - f"/assignments/active/{cloud_name}" - ) + assert str(mock_get.call_args[0][1]).endswith(f"/assignments/active/{cloud_name}") assert result == expected_response @patch("requests.Session.request") @@ -1466,9 +1444,7 @@ def test_remove_interface(self, mock_delete): result = self.api.remove_interface(hostname, if_name) mock_delete.assert_called_once() - assert str(mock_delete.call_args[0][1]).endswith( - f"/interfaces/{hostname}/{if_name}" - ) + assert str(mock_delete.call_args[0][1]).endswith(f"/interfaces/{hostname}/{if_name}") assert result == {} @patch("requests.Session.request") @@ -1776,11 +1752,7 @@ def test_get_moves(self, mock_get): @patch("requests.Session.request") def test_get_moves_with_date(self, mock_get): date = "2024-03-20" - expected_response = { - "moves": [ - {"id": 1, "host": "host1", "from_cloud": "cloud1", "to_cloud": "cloud2"} - ] - } + expected_response = {"moves": [{"id": 1, "host": "host1", "from_cloud": "cloud1", "to_cloud": "cloud2"}]} mock_response = Mock() mock_response.json.return_value = expected_response mock_get.return_value = mock_response @@ -1871,9 +1843,7 @@ def test_create_self_assignment(self, mock_post): with patch.object(self.api, "post") as mock_post: mock_post.return_value = expected_response result = self.api.create_self_assignment(test_data) - mock_post.assert_called_once_with( - str(Path("assignments") / "self"), test_data - ) + mock_post.assert_called_once_with(str(Path("assignments") / "self"), test_data) assert result == expected_response @patch("requests.Session.request") @@ -2049,9 +2019,7 @@ def test_create_self_assignment_logging(self, mock_request, mock_print): self.api.create_self_assignment(assignment_data) - mock_print.assert_called_once_with( - "Self-assignment created - ID: 123, Cloud: cloud1" - ) + mock_print.assert_called_once_with("Self-assignment created - ID: 123, Cloud: cloud1") @patch("builtins.print") @patch("requests.Session.request") @@ -2108,9 +2076,7 @@ def setup(self): @pytest.fixture def quads_base(self): - return QuadsBase( - username=self.username, password=self.password, base_url=self.base_url - ) + return QuadsBase(username=self.username, password=self.password, base_url=self.base_url) def test_context_manager_enter(self, quads_base): quads_base.login = Mock() From 64a37247c6f40b286eb83cd60f474f179adffe0e Mon Sep 17 00:00:00 2001 From: Will Foster Date: Thu, 14 May 2026 15:48:34 +0100 Subject: [PATCH 3/3] [publish] --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 03fe882..12278b8 100644 --- a/README.rst +++ b/README.rst @@ -54,7 +54,7 @@ Installation pip install quads-lib -You can also install the in-development version with:: +You can also install the development version with:: pip install https://github.com/quadsproject/python-quads-lib/archive/development.zip