Skip to content

Commit 090a926

Browse files
authored
Merge pull request #6 from DataCrunch-io/feature/add_spot_instance_support
Feature/add spot instance support
2 parents b0959fb + 17d524a commit 090a926

8 files changed

Lines changed: 126 additions & 10 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ We use pytest for testing.
134134

135135
### Local Manual Testing
136136

137+
Create this file in the root directory of the project:
138+
137139
```python
138140
from datacrunch.datacrunch import DataCrunchClient
139141

datacrunch/helpers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Type
2+
import json
3+
4+
def stringify_class_object_properties(class_object: Type) -> str:
5+
"""Generates a json string representation of a class object's properties and values
6+
7+
:param class_object: An instance of a class
8+
:type class_object: Type
9+
:return: _description_
10+
:rtype: json string representation of a class object's properties and values
11+
"""
12+
class_properties = {property: getattr(class_object, property, '') for property in class_object.__dir__() if property[:1] != '_' and type(getattr(class_object, property, '')).__name__ != 'method'}
13+
return json.dumps(class_properties, indent=2)

datacrunch/images/images.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import List
2+
from datacrunch.helpers import stringify_class_object_properties
23

34
IMAGES_ENDPOINT = '/images'
45

@@ -59,6 +60,14 @@ def details(self) -> List[str]:
5960
"""
6061
return self._details
6162

63+
def __str__(self) -> str:
64+
"""Returns a string of the json representation of the image
65+
66+
:return: json representation of the image
67+
:rtype: str
68+
"""
69+
return stringify_class_object_properties(self)
70+
6271

6372
class ImagesService:
6473
"""A service for interacting with the images endpoint"""

datacrunch/instances/instances.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import List, Union, Optional, Dict
2+
from datacrunch.helpers import stringify_class_object_properties
23

34
INSTANCES_ENDPOINT = '/instances'
45

@@ -24,7 +25,8 @@ def __init__(self,
2425
os_volume_id: str,
2526
gpu_memory: dict,
2627
location: str = "FIN1",
27-
startup_script_id: str = None
28+
startup_script_id: str = None,
29+
is_spot: bool = False
2830
) -> None:
2931
"""Initialize the instance object
3032
@@ -64,6 +66,8 @@ def __init__(self,
6466
:type location: str, optional
6567
:param startup_script_id: startup script id, defaults to None
6668
:type startup_script_id: str, optional
69+
:param is_spot: is this a spot instance, defaults to None
70+
:type is_spot: bool, optional
6771
"""
6872
self._id = id
6973
self._instance_type = instance_type
@@ -83,6 +87,7 @@ def __init__(self,
8387
self._storage = storage
8488
self._os_volume_id = os_volume_id
8589
self._gpu_memory = gpu_memory
90+
self._is_spot = is_spot
8691

8792
@property
8893
def id(self) -> str:
@@ -246,6 +251,22 @@ def gpu_memory(self) -> dict:
246251
"""
247252
return self._gpu_memory
248253

254+
@property
255+
def is_spot(self) -> bool:
256+
"""Is this a spot instance
257+
258+
:return: is spot details
259+
:rtype: bool
260+
"""
261+
return self._is_spot
262+
263+
def __str__(self) -> str:
264+
"""Returns a string of the json representation of the instance
265+
266+
:return: json representation of the instance
267+
:rtype: str
268+
"""
269+
return stringify_class_object_properties(self)
249270

250271
class InstancesService:
251272
"""A service for interacting with the instances endpoint"""
@@ -281,7 +302,8 @@ def get(self, status: str = None) -> List[Instance]:
281302
memory=instance_dict['memory'],
282303
storage=instance_dict['storage'],
283304
os_volume_id=instance_dict['os_volume_id'] if 'os_volume_id' in instance_dict else None,
284-
gpu_memory=instance_dict['gpu_memory'] if 'gpu_memory' in instance_dict else None
305+
gpu_memory=instance_dict['gpu_memory'] if 'gpu_memory' in instance_dict else None,
306+
is_spot=instance_dict['is_spot'] if 'is_spot' in instance_dict else False
285307
), instances_dict))
286308
return instances
287309

@@ -313,8 +335,8 @@ def get_by_id(self, id: str) -> Instance:
313335
memory=instance_dict['memory'],
314336
storage=instance_dict['storage'],
315337
os_volume_id=instance_dict['os_volume_id'] if 'os_volume_id' in instance_dict else None,
316-
gpu_memory=instance_dict['gpu_memory'] if 'gpu_memory' in instance_dict else None
317-
338+
gpu_memory=instance_dict['gpu_memory'] if 'gpu_memory' in instance_dict else None,
339+
is_spot=instance_dict['is_spot'] if 'is_spot' in instance_dict else False
318340
)
319341
return instance
320342

@@ -327,7 +349,8 @@ def create(self,
327349
location: str = "FIN1",
328350
startup_script_id: str = None,
329351
volumes: List[Dict] = None,
330-
os_volume: Dict = None) -> Instance:
352+
os_volume: Dict = None,
353+
is_spot: bool = False) -> Instance:
331354
"""Creates (deploys) a new instance
332355
333356
:param instance_type: instance type. e.g. '8V100.48M'
@@ -348,6 +371,8 @@ def create(self,
348371
:type volumes: List[Dict], optional
349372
:param os_volume: OS volume details, defaults to None
350373
:type os_volume: Dict, optional
374+
:param is_spot: Is spot instance
375+
:type is_spot: bool, optional
351376
:return: the new instance object
352377
:rtype: id
353378
"""
@@ -360,7 +385,8 @@ def create(self,
360385
"description": description,
361386
"location": location,
362387
"os_volume": os_volume,
363-
"volumes": volumes
388+
"volumes": volumes,
389+
"is_spot": is_spot
364390
}
365391
id = self._http_client.post(INSTANCES_ENDPOINT, json=payload).text
366392
instance = self.get_by_id(id)

datacrunch/volumes/volumes.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import List, Union, Optional
22
from datacrunch.constants import VolumeActions
3+
from datacrunch.helpers import stringify_class_object_properties
34

45
VOLUMES_ENDPOINT = '/volumes'
56

@@ -157,6 +158,14 @@ def ssh_key_ids(self) -> List[str]:
157158
"""
158159
return self._ssh_key_ids
159160

161+
def __str__(self) -> str:
162+
"""Returns a string of the json representation of the volume
163+
164+
:return: json representation of the volume
165+
:rtype: str
166+
"""
167+
return stringify_class_object_properties(self)
168+
160169
class VolumesService:
161170
"""A service for interacting with the volumes endpoint"""
162171

tests/unit_tests/images/test_images.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ def test_images(http_client):
3636
assert images[0].image_type == 'ubuntu-20.04-cuda-11.0'
3737
assert type(images[0].details) == list
3838
assert images[0].details[0] == "Ubuntu 20.04"
39-
assert images[0].details[1] == "CUDA 11.0"
39+
assert images[0].details[1] == "CUDA 11.0"
40+
assert type(images[0].__str__()) == str
41+

tests/unit_tests/instances/test_instances.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
}
6161
]
6262

63+
PAYLOAD_SPOT = PAYLOAD
64+
PAYLOAD_SPOT[0]["is_spot"] = True
6365

6466
class TestInstancesService:
6567
@pytest.fixture
@@ -260,6 +262,58 @@ def test_create_instance_successful(self, instances_service, endpoint):
260262
assert type(instance.storage) == dict
261263
assert responses.assert_call_count(endpoint, 1) is True
262264
assert responses.assert_call_count(url, 1) is True
265+
assert type(instance.__str__()) == str
266+
267+
def test_create_spot_instance_successful(self, instances_service, endpoint):
268+
# arrange - add response mock
269+
# add response mock for the create instance endpoint
270+
responses.add(
271+
responses.POST,
272+
endpoint,
273+
body=INSTANCE_ID,
274+
status=200
275+
)
276+
# add response mock for the get instance by id endpoint
277+
url = endpoint + '/' + INSTANCE_ID
278+
responses.add(
279+
responses.GET,
280+
url,
281+
json=PAYLOAD_SPOT[0],
282+
status=200
283+
)
284+
285+
# act
286+
instance = instances_service.create(
287+
instance_type=INSTANCE_TYPE,
288+
image=INSTANCE_IMAGE,
289+
ssh_key_ids=[SSH_KEY_ID],
290+
hostname=INSTANCE_HOSTNAME,
291+
description=INSTANCE_DESCRIPTION,
292+
os_volume=INSTANCE_OS_VOLUME
293+
)
294+
295+
# assert
296+
assert type(instance) == Instance
297+
assert instance.id == INSTANCE_ID
298+
assert instance.ssh_key_ids == [SSH_KEY_ID]
299+
assert instance.status == INSTANCE_STATUS
300+
assert instance.image == INSTANCE_IMAGE
301+
assert instance.instance_type == INSTANCE_TYPE
302+
assert instance.price_per_hour == INSTANCE_PRICE_PER_HOUR
303+
assert instance.location == INSTANCE_LOCATION
304+
assert instance.description == INSTANCE_DESCRIPTION
305+
assert instance.hostname == INSTANCE_HOSTNAME
306+
assert instance.ip == INSTANCE_IP
307+
assert instance.created_at == INSTANCE_CREATED_AT
308+
assert instance.os_volume_id == OS_VOLUME_ID
309+
assert instance.is_spot == True
310+
assert type(instance.cpu) == dict
311+
assert type(instance.gpu) == dict
312+
assert type(instance.memory) == dict
313+
assert type(instance.gpu_memory) == dict
314+
assert type(instance.storage) == dict
315+
assert responses.assert_call_count(endpoint, 1) is True
316+
assert responses.assert_call_count(url, 1) is True
263317

264318
def test_create_instance_attached_os_volume_successful(self, instances_service, endpoint):
265319
# arrange - add response mock
@@ -368,7 +422,7 @@ def test_action_failed(self, instances_service, endpoint):
368422
assert excinfo.value.message == INVALID_REQUEST_MESSAGE
369423
assert responses.assert_call_count(url, 1) is True
370424

371-
def test_is_available_successful(self, instances_service, endpoint):
425+
def test_is_available_successful(self, instances_service):
372426
# arrange - add response mock
373427
url = instances_service._http_client._base_url + '/instance-availability/' + INSTANCE_TYPE
374428
responses.add(
@@ -385,7 +439,7 @@ def test_is_available_successful(self, instances_service, endpoint):
385439
assert is_available is True
386440
assert responses.assert_call_count(url, 1) is True
387441

388-
def test_is_available_failed(self, instances_service, endpoint):
442+
def test_is_available_failed(self, instances_service):
389443
# arrange - add response mock
390444
url = instances_service._http_client._base_url + '/instance-availability/x'
391445
responses.add(
@@ -402,4 +456,4 @@ def test_is_available_failed(self, instances_service, endpoint):
402456
# assert
403457
assert excinfo.value.code == INVALID_REQUEST
404458
assert excinfo.value.message == INVALID_REQUEST_MESSAGE
405-
assert responses.assert_call_count(url, 1) is True
459+
assert responses.assert_call_count(url, 1) is True

tests/unit_tests/volumes/test_volumes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ def test_create_volume_successful(self, volumes_service, endpoint):
223223

224224
# assert
225225
assert volume.id == NVME_VOL_ID
226+
assert type(volume.__str__()) == str
226227

227228
def test_create_volume_failed(self, volumes_service, endpoint):
228229
# arrange - add response mock

0 commit comments

Comments
 (0)