Skip to content

Commit 0831b8f

Browse files
authored
Merge pull request #87 from verda-cloud/poc-2026-03-25-sync
feat: sync SDK with latest OpenAPI spec 2026-03-25
2 parents 5bd137e + 7967e38 commit 0831b8f

8 files changed

Lines changed: 236 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `LongTermService` with `get_cluster_periods()` and `get_instance_periods()` methods
13+
- `VolumesService.delete_by_id()` method using `DELETE /v1/volumes/{volume_id}` endpoint
1214
- Support for querying OS images by instance type via `verda.images.get(instance_type=...)`
1315

1416
### Changed

tests/unit_tests/long_term/__init__.py

Whitespace-only changes.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import pytest
2+
import responses
3+
4+
from verda.exceptions import APIException
5+
from verda.long_term import LongTermPeriod, LongTermService
6+
7+
INVALID_REQUEST = 'invalid_request'
8+
INVALID_REQUEST_MESSAGE = 'Bad request'
9+
10+
PERIOD_1 = {
11+
'code': '3_MONTHS',
12+
'name': '3 months',
13+
'is_enabled': True,
14+
'unit_name': 'month',
15+
'unit_value': 3.0,
16+
'discount_percentage': 14.0,
17+
}
18+
19+
PERIOD_2 = {
20+
'code': '6_MONTHS',
21+
'name': '6 months',
22+
'is_enabled': True,
23+
'unit_name': 'month',
24+
'unit_value': 6.0,
25+
'discount_percentage': 20.0,
26+
}
27+
28+
PAYLOAD = [PERIOD_1, PERIOD_2]
29+
30+
31+
class TestLongTermService:
32+
@pytest.fixture
33+
def long_term_service(self, http_client):
34+
return LongTermService(http_client)
35+
36+
@pytest.fixture
37+
def endpoint(self, http_client):
38+
return http_client._base_url + '/long-term/periods'
39+
40+
def test_get_cluster_periods(self, long_term_service, endpoint):
41+
# arrange
42+
responses.add(responses.GET, endpoint + '/clusters', json=PAYLOAD, status=200)
43+
44+
# act
45+
periods = long_term_service.get_cluster_periods()
46+
47+
# assert
48+
assert isinstance(periods, list)
49+
assert len(periods) == 2
50+
assert isinstance(periods[0], LongTermPeriod)
51+
assert periods[0].code == '3_MONTHS'
52+
assert periods[0].name == '3 months'
53+
assert periods[0].is_enabled is True
54+
assert periods[0].unit_name == 'month'
55+
assert periods[0].unit_value == 3.0
56+
assert periods[0].discount_percentage == 14.0
57+
assert periods[1].code == '6_MONTHS'
58+
assert responses.assert_call_count(endpoint + '/clusters', 1) is True
59+
60+
def test_get_cluster_periods_failed(self, long_term_service, endpoint):
61+
# arrange
62+
url = endpoint + '/clusters'
63+
responses.add(
64+
responses.GET,
65+
url,
66+
json={'code': INVALID_REQUEST, 'message': INVALID_REQUEST_MESSAGE},
67+
status=400,
68+
)
69+
70+
# act + assert
71+
with pytest.raises(APIException) as excinfo:
72+
long_term_service.get_cluster_periods()
73+
74+
assert excinfo.value.code == INVALID_REQUEST
75+
assert excinfo.value.message == INVALID_REQUEST_MESSAGE
76+
assert responses.assert_call_count(url, 1) is True
77+
78+
def test_get_instance_periods(self, long_term_service, endpoint):
79+
# arrange
80+
responses.add(responses.GET, endpoint + '/instances', json=PAYLOAD, status=200)
81+
82+
# act
83+
periods = long_term_service.get_instance_periods()
84+
85+
# assert
86+
assert isinstance(periods, list)
87+
assert len(periods) == 2
88+
assert isinstance(periods[0], LongTermPeriod)
89+
assert periods[0].code == '3_MONTHS'
90+
assert periods[0].discount_percentage == 14.0
91+
assert periods[1].unit_value == 6.0
92+
assert responses.assert_call_count(endpoint + '/instances', 1) is True
93+
94+
def test_get_instance_periods_failed(self, long_term_service, endpoint):
95+
# arrange
96+
url = endpoint + '/instances'
97+
responses.add(
98+
responses.GET,
99+
url,
100+
json={'code': INVALID_REQUEST, 'message': INVALID_REQUEST_MESSAGE},
101+
status=400,
102+
)
103+
104+
# act + assert
105+
with pytest.raises(APIException) as excinfo:
106+
long_term_service.get_instance_periods()
107+
108+
assert excinfo.value.code == INVALID_REQUEST
109+
assert excinfo.value.message == INVALID_REQUEST_MESSAGE
110+
assert responses.assert_call_count(url, 1) is True
111+
112+
def test_get_cluster_periods_empty_list(self, long_term_service, endpoint):
113+
# arrange
114+
responses.add(responses.GET, endpoint + '/clusters', json=[], status=200)
115+
116+
# act
117+
periods = long_term_service.get_cluster_periods()
118+
119+
# assert
120+
assert isinstance(periods, list)
121+
assert len(periods) == 0

tests/unit_tests/volumes/test_volumes.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,59 @@ def test_delete_volume_failed(self, volumes_service, endpoint):
572572
assert excinfo.value.message == INVALID_REQUEST_MESSAGE
573573
assert responses.assert_call_count(endpoint, 1) is True
574574

575+
def test_delete_volume_by_id_successful(self, volumes_service, endpoint):
576+
# arrange
577+
url = endpoint + '/' + NVME_VOL_ID
578+
responses.add(
579+
responses.DELETE,
580+
url,
581+
status=202,
582+
match=[matchers.json_params_matcher({'is_permanent': False})],
583+
)
584+
585+
# act
586+
result = volumes_service.delete_by_id(NVME_VOL_ID)
587+
588+
# assert
589+
assert result is None
590+
assert responses.assert_call_count(url, 1) is True
591+
592+
def test_delete_volume_by_id_permanent_successful(self, volumes_service, endpoint):
593+
# arrange
594+
url = endpoint + '/' + NVME_VOL_ID
595+
responses.add(
596+
responses.DELETE,
597+
url,
598+
status=202,
599+
match=[matchers.json_params_matcher({'is_permanent': True})],
600+
)
601+
602+
# act
603+
result = volumes_service.delete_by_id(NVME_VOL_ID, is_permanent=True)
604+
605+
# assert
606+
assert result is None
607+
assert responses.assert_call_count(url, 1) is True
608+
609+
def test_delete_volume_by_id_failed(self, volumes_service, endpoint):
610+
# arrange
611+
url = endpoint + '/' + NVME_VOL_ID
612+
responses.add(
613+
responses.DELETE,
614+
url,
615+
json={'code': INVALID_REQUEST, 'message': INVALID_REQUEST_MESSAGE},
616+
status=400,
617+
)
618+
619+
# act
620+
with pytest.raises(APIException) as excinfo:
621+
volumes_service.delete_by_id(NVME_VOL_ID)
622+
623+
# assert
624+
assert excinfo.value.code == INVALID_REQUEST
625+
assert excinfo.value.message == INVALID_REQUEST_MESSAGE
626+
assert responses.assert_call_count(url, 1) is True
627+
575628
def test_clone_volume_with_input_name_successful(self, volumes_service, endpoint):
576629
# arrange
577630
CLONED_VOLUME_NAME = 'cloned-volume'

verda/_verda.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from verda.instances import InstancesService
1313
from verda.job_deployments import JobDeploymentsService
1414
from verda.locations import LocationsService
15+
from verda.long_term import LongTermService
1516
from verda.ssh_keys import SSHKeysService
1617
from verda.startup_scripts import StartupScriptsService
1718
from verda.volume_types import VolumeTypesService
@@ -95,5 +96,8 @@ def __init__(
9596
self.cluster_types: ClusterTypesService = ClusterTypesService(self._http_client)
9697
"""Cluster types service. Get available cluster info"""
9798

99+
self.long_term: LongTermService = LongTermService(self._http_client)
100+
"""Long-term service. Get available commitment periods"""
101+
98102

99103
__all__ = ['VerdaClient']

verda/long_term/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from ._long_term import LongTermPeriod, LongTermService

verda/long_term/_long_term.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from dataclasses import dataclass
2+
3+
from dataclasses_json import Undefined, dataclass_json
4+
5+
LONG_TERM_PERIODS_ENDPOINT = '/long-term/periods'
6+
7+
8+
@dataclass_json(undefined=Undefined.EXCLUDE)
9+
@dataclass
10+
class LongTermPeriod:
11+
"""A long-term commitment period."""
12+
13+
code: str
14+
name: str
15+
is_enabled: bool
16+
unit_name: str
17+
unit_value: float
18+
discount_percentage: float
19+
20+
21+
class LongTermService:
22+
"""A service for interacting with the long-term periods endpoints."""
23+
24+
def __init__(self, http_client) -> None:
25+
self._http_client = http_client
26+
27+
def get_cluster_periods(self) -> list[LongTermPeriod]:
28+
"""Get available long-term commitment periods for clusters.
29+
30+
:return: list of long-term period objects
31+
:rtype: list[LongTermPeriod]
32+
"""
33+
periods = self._http_client.get(LONG_TERM_PERIODS_ENDPOINT + '/clusters').json()
34+
return [LongTermPeriod.from_dict(p, infer_missing=True) for p in periods]
35+
36+
def get_instance_periods(self) -> list[LongTermPeriod]:
37+
"""Get available long-term commitment periods for instances.
38+
39+
:return: list of long-term period objects
40+
:rtype: list[LongTermPeriod]
41+
"""
42+
periods = self._http_client.get(LONG_TERM_PERIODS_ENDPOINT + '/instances').json()
43+
return [LongTermPeriod.from_dict(p, infer_missing=True) for p in periods]

verda/volumes/_volumes.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,18 @@ def increase_size(self, id_list: list[str] | str, size: int) -> None:
234234
self._http_client.put(VOLUMES_ENDPOINT, json=payload)
235235
return
236236

237+
def delete_by_id(self, volume_id: str, is_permanent: bool = False) -> None:
238+
"""Delete a single volume by id using the DELETE endpoint.
239+
240+
:param volume_id: volume id
241+
:type volume_id: str
242+
:param is_permanent: if True, volume is removed permanently; if False, moves to trash
243+
:type is_permanent: bool, optional
244+
"""
245+
payload = {'is_permanent': is_permanent}
246+
self._http_client.delete(VOLUMES_ENDPOINT + f'/{volume_id}', json=payload)
247+
return
248+
237249
def delete(self, id_list: list[str] | str, is_permanent: bool = False) -> None:
238250
"""Delete multiple volumes or single volume.
239251

0 commit comments

Comments
 (0)