Skip to content

Commit 19ec59d

Browse files
committed
feat: sync SDK with latest OpenAPI spec 2026-03-25
- Add LongTermService with get_cluster_periods() and get_instance_periods() - Add VolumesService.delete_by_id() using DELETE /v1/volumes/{volume_id} - Wire LongTermService into VerdaClient - Add unit tests for all new methods
1 parent a18ec94 commit 19ec59d

8 files changed

Lines changed: 243 additions & 0 deletions

File tree

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- `LongTermService` with `get_cluster_periods()` and `get_instance_periods()` methods
13+
- `VolumesService.delete_by_id()` method using `DELETE /v1/volumes/{volume_id}` endpoint
14+
1015
## [1.22.0] - 2026-03-20
1116

1217
### Added

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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,63 @@ def test_delete_volume_failed(self, volumes_service, endpoint):
506506
assert excinfo.value.message == INVALID_REQUEST_MESSAGE
507507
assert responses.assert_call_count(endpoint, 1) is True
508508

509+
def test_delete_volume_by_id_successful(self, volumes_service, endpoint):
510+
# arrange
511+
url = endpoint + '/' + NVME_VOL_ID
512+
responses.add(
513+
responses.DELETE,
514+
url,
515+
status=202,
516+
match=[
517+
matchers.json_params_matcher({'is_permanent': False})
518+
],
519+
)
520+
521+
# act
522+
result = volumes_service.delete_by_id(NVME_VOL_ID)
523+
524+
# assert
525+
assert result is None
526+
assert responses.assert_call_count(url, 1) is True
527+
528+
def test_delete_volume_by_id_permanent_successful(self, volumes_service, endpoint):
529+
# arrange
530+
url = endpoint + '/' + NVME_VOL_ID
531+
responses.add(
532+
responses.DELETE,
533+
url,
534+
status=202,
535+
match=[
536+
matchers.json_params_matcher({'is_permanent': True})
537+
],
538+
)
539+
540+
# act
541+
result = volumes_service.delete_by_id(NVME_VOL_ID, is_permanent=True)
542+
543+
# assert
544+
assert result is None
545+
assert responses.assert_call_count(url, 1) is True
546+
547+
def test_delete_volume_by_id_failed(self, volumes_service, endpoint):
548+
# arrange
549+
url = endpoint + '/' + NVME_VOL_ID
550+
responses.add(
551+
responses.DELETE,
552+
url,
553+
json={'code': INVALID_REQUEST, 'message': INVALID_REQUEST_MESSAGE},
554+
status=400,
555+
)
556+
557+
# act
558+
with pytest.raises(APIException) as excinfo:
559+
volumes_service.delete_by_id(NVME_VOL_ID)
560+
561+
# assert
562+
assert excinfo.value.code == INVALID_REQUEST
563+
assert excinfo.value.message == INVALID_REQUEST_MESSAGE
564+
assert responses.assert_call_count(url, 1) is True
565+
509566
def test_clone_volume_with_input_name_successful(self, volumes_service, endpoint):
510567
# arrange
511568
CLONED_VOLUME_NAME = 'cloned-volume'

verda/_verda.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from verda.containers import ContainersService
99
from verda.http_client import HTTPClient
1010
from verda.images import ImagesService
11+
from verda.long_term import LongTermService
1112
from verda.instance_types import InstanceTypesService
1213
from verda.instances import InstancesService
1314
from verda.job_deployments import JobDeploymentsService
@@ -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
@@ -368,6 +368,18 @@ def increase_size(self, id_list: list[str] | str, size: int) -> None:
368368
self._http_client.put(VOLUMES_ENDPOINT, json=payload)
369369
return
370370

371+
def delete_by_id(self, volume_id: str, is_permanent: bool = False) -> None:
372+
"""Delete a single volume by id using the DELETE endpoint.
373+
374+
:param volume_id: volume id
375+
:type volume_id: str
376+
:param is_permanent: if True, volume is removed permanently; if False, moves to trash
377+
:type is_permanent: bool, optional
378+
"""
379+
payload = {'is_permanent': is_permanent}
380+
self._http_client.delete(VOLUMES_ENDPOINT + f'/{volume_id}', json=payload)
381+
return
382+
371383
def delete(self, id_list: list[str] | str, is_permanent: bool = False) -> None:
372384
"""Delete multiple volumes or single volume.
373385

0 commit comments

Comments
 (0)