Skip to content

Commit d9d6b1c

Browse files
fix(api): address instance type and helper review comments
Remove obsolete dynamic pricing fields from instance types. Move shared payload cleanup into verda.helpers, reuse the shared Currency alias, update client service docstrings, and add helper coverage requested during review. Co-authored-by: imagene-shahar <imagene-shahar@users.noreply.github.com>
1 parent 702ac99 commit d9d6b1c

9 files changed

Lines changed: 64 additions & 63 deletions

File tree

tests/unit_tests/instance_types/test_instance_types.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
P2P = '300 GB/s'
2121
PRICE_PER_HOUR = 5.0
2222
SPOT_PRICE_PER_HOUR = 2.5
23-
MAX_DYNAMIC_PRICE = 7.5
2423
SERVERLESS_PRICE = 1.25
2524
SERVERLESS_SPOT_PRICE = 0.75
2625
INSTANCE_TYPE = '8V100.48M'
@@ -29,8 +28,6 @@
2928
DISPLAY_NAME = 'NVIDIA Tesla V100'
3029
SUPPORTED_OS = ['ubuntu-24.04-cuda-12.8-open-docker']
3130

32-
PRICE_HISTORY_PAYLOAD = [{'date': '2024-01-01', 'price_per_hour': '2.00'}]
33-
3431

3532
@responses.activate
3633
def test_instance_types(http_client):
@@ -69,7 +66,6 @@ def test_instance_types(http_client):
6966
'p2p': P2P,
7067
'price_per_hour': '5.00',
7168
'spot_price': '2.50',
72-
'max_dynamic_price': '7.50',
7369
'serverless_price': '1.25',
7470
'serverless_spot_price': '0.75',
7571
'instance_type': INSTANCE_TYPE,
@@ -106,7 +102,6 @@ def test_instance_types(http_client):
106102
assert instance_type.display_name == DISPLAY_NAME
107103
assert instance_type.supported_os == SUPPORTED_OS
108104
assert instance_type.deploy_warning == 'Use updated drivers'
109-
assert instance_type.max_dynamic_price == MAX_DYNAMIC_PRICE
110105
assert instance_type.serverless_price == SERVERLESS_PRICE
111106
assert instance_type.serverless_spot_price == SERVERLESS_SPOT_PRICE
112107
assert isinstance(instance_type.cpu, dict)
@@ -123,22 +118,3 @@ def test_instance_types(http_client):
123118
assert instance_type.memory['size_in_gigabytes'] == MEMORY_SIZE
124119
assert instance_type.gpu_memory['size_in_gigabytes'] == GPU_MEMORY_SIZE
125120
assert instance_type.storage['size_in_gigabytes'] == STORAGE_SIZE
126-
127-
128-
@responses.activate
129-
def test_instance_type_price_history(http_client):
130-
# arrange - add response mock
131-
responses.add(
132-
responses.GET,
133-
http_client._base_url + '/instance-types/price-history',
134-
json=PRICE_HISTORY_PAYLOAD,
135-
status=200,
136-
)
137-
138-
instance_types_service = InstanceTypesService(http_client)
139-
140-
# act
141-
price_history = instance_types_service.get_price_history()
142-
143-
# assert
144-
assert price_history == PRICE_HISTORY_PAYLOAD

tests/unit_tests/test_helpers.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from verda.helpers import strip_none_values
2+
3+
4+
def test_strip_none_values_removes_none_recursively():
5+
data = {
6+
'name': 'job',
7+
'optional': None,
8+
'nested': {
9+
'keep': 'value',
10+
'drop': None,
11+
},
12+
'items': [
13+
{'keep': 1, 'drop': None},
14+
None,
15+
['value', None],
16+
],
17+
}
18+
19+
assert strip_none_values(data) == {
20+
'name': 'job',
21+
'nested': {'keep': 'value'},
22+
'items': [
23+
{'keep': 1},
24+
None,
25+
['value', None],
26+
],
27+
}

verda/_verda.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,13 @@ def __init__(
8888
"""Job deployments service. Deploy and manage serverless jobs"""
8989

9090
self.container_types: ContainerTypesService = ContainerTypesService(self._http_client)
91-
"""Container types service. Get available serverless container SKUs"""
91+
"""Container types service. Get available serverless container info"""
9292

9393
self.clusters: ClustersService = ClustersService(self._http_client)
9494
"""Clusters service. Create and manage compute clusters"""
9595

9696
self.cluster_types: ClusterTypesService = ClusterTypesService(self._http_client)
97-
"""Cluster types service. Get available cluster SKUs"""
97+
"""Cluster types service. Get available cluster info"""
9898

9999
self.long_term: LongTermService = LongTermService(self._http_client)
100100
"""Long-term service. Get available long-term pricing periods"""

verda/cluster_types/_cluster_types.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from dataclasses import dataclass
2-
from typing import Literal
32

43
from dataclasses_json import dataclass_json
54

6-
CLUSTER_TYPES_ENDPOINT = '/cluster-types'
5+
from verda.constants import Currency
76

8-
Currency = Literal['usd', 'eur']
7+
CLUSTER_TYPES_ENDPOINT = '/cluster-types'
98

109

1110
@dataclass_json

verda/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
from typing import Literal
2+
3+
Currency = Literal['usd', 'eur']
4+
5+
16
class Actions:
27
"""Instance actions."""
38

verda/container_types/_container_types.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from dataclasses import dataclass
2-
from typing import Literal
32

43
from dataclasses_json import dataclass_json
54

6-
CONTAINER_TYPES_ENDPOINT = '/container-types'
5+
from verda.constants import Currency
76

8-
Currency = Literal['usd', 'eur']
7+
CONTAINER_TYPES_ENDPOINT = '/container-types'
98

109

1110
@dataclass_json

verda/helpers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from typing import Any
23

34

45
def stringify_class_object_properties(class_object: type) -> str:
@@ -15,3 +16,12 @@ def stringify_class_object_properties(class_object: type) -> str:
1516
if property[:1] != '_' and type(getattr(class_object, property, '')).__name__ != 'method'
1617
}
1718
return json.dumps(class_properties, indent=2)
19+
20+
21+
def strip_none_values(data: Any) -> Any:
22+
"""Recursively remove ``None`` values from JSON-serializable data."""
23+
if isinstance(data, dict):
24+
return {key: strip_none_values(value) for key, value in data.items() if value is not None}
25+
if isinstance(data, list):
26+
return [strip_none_values(item) for item in data]
27+
return data

verda/instance_types/_instance_types.py

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from dataclasses import dataclass
2-
from typing import Literal
32

43
from dataclasses_json import dataclass_json
54

6-
INSTANCE_TYPES_ENDPOINT = '/instance-types'
5+
from verda.constants import Currency
76

8-
Currency = Literal['usd', 'eur']
7+
INSTANCE_TYPES_ENDPOINT = '/instance-types'
98

109

1110
@dataclass_json
@@ -24,6 +23,17 @@ class InstanceType:
2423
memory: Instance type memory details.
2524
gpu_memory: Instance type GPU memory details.
2625
storage: Instance type storage details.
26+
best_for: Suggested use cases for the instance type.
27+
model: GPU model.
28+
name: Human-readable instance type name.
29+
p2p: Peer-to-peer interconnect bandwidth details.
30+
currency: Currency used for pricing.
31+
manufacturer: Hardware manufacturer.
32+
display_name: Display name shown to users.
33+
supported_os: Supported operating system images.
34+
deploy_warning: Optional deployment warning returned by the API.
35+
serverless_price: Optional serverless price for the same hardware profile.
36+
serverless_spot_price: Optional serverless spot price for the same hardware profile.
2737
"""
2838

2939
id: str
@@ -45,8 +55,6 @@ class InstanceType:
4555
display_name: str
4656
supported_os: list[str]
4757
deploy_warning: str | None = None
48-
dynamic_price: float | None = None
49-
max_dynamic_price: float | None = None
5058
serverless_price: float | None = None
5159
serverless_spot_price: float | None = None
5260

@@ -88,16 +96,6 @@ def get(self, currency: Currency = 'usd') -> list[InstanceType]:
8896
display_name=instance_type['display_name'],
8997
supported_os=instance_type['supported_os'],
9098
deploy_warning=instance_type.get('deploy_warning'),
91-
dynamic_price=(
92-
float(instance_type['dynamic_price'])
93-
if instance_type.get('dynamic_price') is not None
94-
else None
95-
),
96-
max_dynamic_price=(
97-
float(instance_type['max_dynamic_price'])
98-
if instance_type.get('max_dynamic_price') is not None
99-
else None
100-
),
10199
serverless_price=(
102100
float(instance_type['serverless_price'])
103101
if instance_type.get('serverless_price') is not None
@@ -113,7 +111,3 @@ def get(self, currency: Currency = 'usd') -> list[InstanceType]:
113111
]
114112

115113
return instance_type_objects
116-
117-
def get_price_history(self):
118-
"""Get the deprecated dynamic price history endpoint as raw JSON."""
119-
return self._http_client.get(f'{INSTANCE_TYPES_ENDPOINT}/price-history').json()

verda/job_deployments/_job_deployments.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,16 @@
22

33
from dataclasses import dataclass, field
44
from enum import Enum
5-
from typing import Any
65

76
from dataclasses_json import Undefined, dataclass_json
87

98
from verda.containers import ComputeResource, Container, ContainerRegistrySettings
9+
from verda.helpers import strip_none_values
1010
from verda.http_client import HTTPClient
1111

1212
JOB_DEPLOYMENTS_ENDPOINT = '/job-deployments'
1313

1414

15-
def _strip_none(data: Any) -> Any:
16-
"""Recursively remove None values from JSON-serializable data."""
17-
if isinstance(data, dict):
18-
return {key: _strip_none(value) for key, value in data.items() if value is not None}
19-
if isinstance(data, list):
20-
return [_strip_none(item) for item in data]
21-
return data
22-
23-
2415
class JobDeploymentStatus(str, Enum):
2516
"""Possible states of a job deployment."""
2617

@@ -85,15 +76,15 @@ def create(self, deployment: JobDeployment) -> JobDeployment:
8576
"""Create a new job deployment."""
8677
response = self._http_client.post(
8778
JOB_DEPLOYMENTS_ENDPOINT,
88-
json=_strip_none(deployment.to_dict()),
79+
json=strip_none_values(deployment.to_dict()),
8980
)
9081
return JobDeployment.from_dict(response.json(), infer_missing=True)
9182

9283
def update(self, job_name: str, deployment: JobDeployment) -> JobDeployment:
9384
"""Update an existing job deployment."""
9485
response = self._http_client.patch(
9586
f'{JOB_DEPLOYMENTS_ENDPOINT}/{job_name}',
96-
json=_strip_none(deployment.to_dict()),
87+
json=strip_none_values(deployment.to_dict()),
9788
)
9889
return JobDeployment.from_dict(response.json(), infer_missing=True)
9990

0 commit comments

Comments
 (0)