Skip to content

Commit faefdcf

Browse files
authored
[publish] feat: add support for polling/status API
feat: add support for polling/status API [publish]
2 parents 23f0ae2 + 92dd417 commit faefdcf

7 files changed

Lines changed: 163 additions & 3 deletions

File tree

.readthedocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
1+
# See https://docs.readthedocs.io/en/latest/config-file/v2.html for details
22
version: 2
33
sphinx:
44
configuration: docs/conf.py

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Changelog
33
=========
44

5+
0.1.16 (2026-06-02)
6+
-------------------
7+
8+
* Add move status API methods: ``get_all_move_status``, ``get_move_status``,
9+
``start_move_batch``, ``update_move_status``.
10+
511
0.0.0 (2025-01-07)
612
------------------
713

docs/usage.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,18 @@ You can control certificate verification using the ``verify`` parameter:
3636
3737
# Use a custom CA bundle file
3838
quads = QuadsApi(username, password, base_url, verify="/path/to/ca-bundle.pem")
39+
40+
Tracking Move Status
41+
--------------------
42+
43+
Query the 12-stage provisioning pipeline for active host moves:
44+
45+
.. code-block:: python
46+
47+
with QuadsApi(username, password, base_url) as quads:
48+
# All active moves, optionally filtered by cloud or status
49+
moves = quads.get_all_move_status(cloud="cloud02")
50+
51+
# Single host status
52+
progress = quads.get_move_status("host01.example.com")
53+
print(f"{progress['host']}: {progress['status']}")

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def read(*names, **kwargs):
1313

1414
setup(
1515
name="quads-lib",
16-
version="0.1.15",
16+
version="0.1.16",
1717
license="LGPL-3.0-only",
1818
description="Python client library for interacting with the QUADS API",
1919
long_description="{}\n{}".format(

src/quads_lib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.1.15"
1+
__version__ = "0.1.16"
22

33
from .quads import QuadsApi
44

src/quads_lib/quads.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,51 @@ def get_moves(self, date: Optional[str] = None) -> dict:
367367
json_response = self.get(url)
368368
return json_response
369369

370+
# Move Status
371+
def get_all_move_status(self, cloud: Optional[str] = None, status: Optional[str] = None) -> dict:
372+
"""Retrieve all active move status records.
373+
374+
Args:
375+
cloud: Filter by target cloud name.
376+
status: Filter by move status (e.g. provisioning, failed).
377+
"""
378+
url = "moves/progress/"
379+
params = {}
380+
if cloud:
381+
params["cloud"] = cloud
382+
if status:
383+
params["status"] = status
384+
if params:
385+
url = f"{url}?{urlencode(params)}"
386+
return self.get(url)
387+
388+
def get_move_status(self, hostname: str) -> dict:
389+
"""Retrieve move status for a specific host.
390+
391+
Args:
392+
hostname: The host to query status for.
393+
"""
394+
endpoint = Path("moves") / "progress" / hostname
395+
return self.get(str(endpoint))
396+
397+
def start_move_batch(self, hostnames: list) -> dict:
398+
"""Start move tracking for a batch of hosts. Requires admin auth.
399+
400+
Args:
401+
hostnames: List of hostnames to start tracking.
402+
"""
403+
return self.post("moves/progress/batch", {"hostnames": hostnames})
404+
405+
def update_move_status(self, schedule_id: int, data: dict) -> dict:
406+
"""Update move status on a schedule. Requires admin auth.
407+
408+
Args:
409+
schedule_id: The schedule ID to update.
410+
data: Dict with any of status, message, error_message.
411+
"""
412+
endpoint = Path("moves") / "progress" / str(schedule_id)
413+
return self.patch(str(endpoint), data)
414+
370415
def get_version(self) -> dict:
371416
json_response = self.get("version")
372417
return json_response

tests/test_quads.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,6 +1763,100 @@ def test_get_moves_with_date(self, mock_get):
17631763
assert str(mock_get.call_args[0][1]).endswith(f"/moves?date={date}")
17641764
assert result == expected_response
17651765

1766+
# Move Status
1767+
@patch("requests.Session.request")
1768+
def test_get_all_move_status(self, mock_get):
1769+
expected_response = [
1770+
{"id": 1, "host": "host1", "status": "pending"},
1771+
{"id": 2, "host": "host2", "status": "ipmi_config"},
1772+
]
1773+
mock_response = Mock()
1774+
mock_response.json.return_value = expected_response
1775+
mock_get.return_value = mock_response
1776+
1777+
result = self.api.get_all_move_status()
1778+
1779+
mock_get.assert_called_once()
1780+
assert str(mock_get.call_args[0][1]).endswith("/moves/progress/")
1781+
assert result == expected_response
1782+
1783+
@patch("requests.Session.request")
1784+
def test_get_all_move_status_with_cloud(self, mock_get):
1785+
expected_response = [{"id": 1, "host": "host1", "status": "pending"}]
1786+
mock_response = Mock()
1787+
mock_response.json.return_value = expected_response
1788+
mock_get.return_value = mock_response
1789+
1790+
result = self.api.get_all_move_status(cloud="cloud02")
1791+
1792+
mock_get.assert_called_once()
1793+
assert "cloud=cloud02" in str(mock_get.call_args[0][1])
1794+
assert result == expected_response
1795+
1796+
@patch("requests.Session.request")
1797+
def test_get_all_move_status_with_status(self, mock_get):
1798+
expected_response = [{"id": 1, "host": "host1", "status": "provisioning"}]
1799+
mock_response = Mock()
1800+
mock_response.json.return_value = expected_response
1801+
mock_get.return_value = mock_response
1802+
1803+
result = self.api.get_all_move_status(status="provisioning")
1804+
1805+
mock_get.assert_called_once()
1806+
assert "status=provisioning" in str(mock_get.call_args[0][1])
1807+
assert result == expected_response
1808+
1809+
@patch("requests.Session.request")
1810+
def test_get_move_status(self, mock_get):
1811+
expected_response = {"id": 1, "host": "host1", "status": "provisioning"}
1812+
mock_response = Mock()
1813+
mock_response.json.return_value = expected_response
1814+
mock_get.return_value = mock_response
1815+
1816+
result = self.api.get_move_status("host1")
1817+
1818+
mock_get.assert_called_once()
1819+
assert str(mock_get.call_args[0][1]).endswith("/moves/progress/host1")
1820+
assert result == expected_response
1821+
1822+
@patch("requests.Session.request")
1823+
def test_start_move_batch(self, mock_post):
1824+
hostnames = ["host1", "host2"]
1825+
expected_response = {"host1": 1, "host2": 2}
1826+
mock_response = Mock()
1827+
mock_response.json.return_value = expected_response
1828+
mock_post.return_value = mock_response
1829+
1830+
result = self.api.start_move_batch(hostnames)
1831+
1832+
mock_post.assert_called_once()
1833+
assert str(mock_post.call_args[0][1]).endswith("/moves/progress/batch")
1834+
assert mock_post.call_args[1]["json"] == {"hostnames": hostnames}
1835+
assert result == expected_response
1836+
1837+
@patch("requests.Session.request")
1838+
def test_update_move_status(self, mock_patch):
1839+
data = {"status": "ipmi_config", "message": "IPMI configured"}
1840+
expected_response = {"id": 1, "host": "host1", "status": "ipmi_config"}
1841+
mock_response = Mock()
1842+
mock_response.json.return_value = expected_response
1843+
mock_patch.return_value = mock_response
1844+
1845+
result = self.api.update_move_status(1, data)
1846+
1847+
mock_patch.assert_called_once()
1848+
assert str(mock_patch.call_args[0][1]).endswith("/moves/progress/1")
1849+
assert result == expected_response
1850+
1851+
@patch("requests.Session.request")
1852+
def test_get_move_status_error(self, mock_get):
1853+
mock_response = Mock()
1854+
mock_response.status_code = 500
1855+
mock_get.return_value = mock_response
1856+
1857+
with pytest.raises(APIServerException, match="Check the flask server logs"):
1858+
self.api.get_move_status("host1")
1859+
17661860
@patch("requests.Session.request")
17671861
def test_get_version(self, mock_get):
17681862
expected_response = {"version": "1.0.0", "api_version": "2.0"}

0 commit comments

Comments
 (0)