Skip to content

Commit 29c2560

Browse files
committed
Merge branch 'release/v1.6.0'
2 parents fe6b8b1 + 8fe89af commit 29c2560

File tree

11 files changed

+144
-17
lines changed

11 files changed

+144
-17
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
Changelog
22
=========
33

4-
v1.5.10 (2023-06-28)
4+
v1.6.0 (2023-09-15)
5+
-------------------
6+
7+
* Added locations endpoint and location code parameter to the availability endpoints
8+
9+
v1.5.0 (2023-06-28)
510
-------------------
611

712
* Added location constants

datacrunch/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = '1.5.0'
1+
VERSION = '1.6.0'

datacrunch/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ def __init__(self):
5656

5757

5858
class Locations:
59-
FIN_01 = "FIN-01"
60-
ICE_01 = "ICE-01"
59+
FIN_01: str = "FIN-01"
60+
ICE_01: str = "ICE-01"
6161

6262
def __init__(self):
6363
return

datacrunch/datacrunch.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from datacrunch.volume_types.volume_types import VolumeTypesService
1010
from datacrunch.volumes.volumes import VolumesService
1111
from datacrunch.constants import Constants
12+
from datacrunch.locations.locations import LocationsService
1213
from datacrunch.__version__ import VERSION
1314

1415

@@ -62,3 +63,7 @@ def __init__(self, client_id: str, client_secret: str, base_url: str = "https://
6263

6364
self.volumes: VolumesService = VolumesService(self._http_client)
6465
"""Volume service. Create, attach, detach, get, rename, delete volumes"""
66+
67+
self.locations: LocationsService = LocationsService(
68+
self._http_client)
69+
"""Locations service. Get locations"""

datacrunch/http_client/http_client.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self, auth_service, base_url: str) -> None:
3232
self._auth_service = auth_service
3333
self._auth_service.authenticate()
3434

35-
def post(self, url: str, json: dict = None, **kwargs) -> requests.Response:
35+
def post(self, url: str, json: dict = None, params: dict = None, **kwargs) -> requests.Response:
3636
"""Sends a POST request.
3737
3838
A wrapper for the requests.post method.
@@ -43,6 +43,8 @@ def post(self, url: str, json: dict = None, **kwargs) -> requests.Response:
4343
:type url: str
4444
:param json: A JSON serializable Python object to send in the body of the Request, defaults to None
4545
:type json: dict, optional
46+
:param params: Dictionary of querystring data to attach to the Request, defaults to None
47+
:type params: dict, optional
4648
4749
:raises APIException: an api exception with message and error type code
4850
@@ -54,12 +56,13 @@ def post(self, url: str, json: dict = None, **kwargs) -> requests.Response:
5456
url = self._add_base_url(url)
5557
headers = self._generate_headers()
5658

57-
response = requests.post(url, json=json, headers=headers, **kwargs)
59+
response = requests.post(
60+
url, json=json, headers=headers, params=params, **kwargs)
5861
handle_error(response)
5962

6063
return response
6164

62-
def put(self, url: str, json: dict = None, **kwargs) -> requests.Response:
65+
def put(self, url: str, json: dict = None, params: dict = None, **kwargs) -> requests.Response:
6366
"""Sends a PUT request.
6467
6568
A wrapper for the requests.put method.
@@ -70,6 +73,8 @@ def put(self, url: str, json: dict = None, **kwargs) -> requests.Response:
7073
:type url: str
7174
:param json: A JSON serializable Python object to send in the body of the Request, defaults to None
7275
:type json: dict, optional
76+
:param params: Dictionary of querystring data to attach to the Request, defaults to None
77+
:type params: dict, optional
7378
7479
:raises APIException: an api exception with message and error type code
7580
@@ -81,7 +86,8 @@ def put(self, url: str, json: dict = None, **kwargs) -> requests.Response:
8186
url = self._add_base_url(url)
8287
headers = self._generate_headers()
8388

84-
response = requests.put(url, json=json, headers=headers, **kwargs)
89+
response = requests.put(
90+
url, json=json, headers=headers, params=params, **kwargs)
8591
handle_error(response)
8692

8793
return response
@@ -95,7 +101,7 @@ def get(self, url: str, params: dict = None, **kwargs) -> requests.Response:
95101
96102
:param url: relative url of the API endpoint
97103
:type url: str
98-
:param params: Dictionary, list of tuples or bytes to send in the query string for the Request. defaults to None
104+
:param params: Dictionary of querystring data to attach to the Request, defaults to None
99105
:type params: dict, optional
100106
101107
:raises APIException: an api exception with message and error type code
@@ -113,7 +119,7 @@ def get(self, url: str, params: dict = None, **kwargs) -> requests.Response:
113119

114120
return response
115121

116-
def delete(self, url: str, json: dict = None, **kwargs) -> requests.Response:
122+
def delete(self, url: str, json: dict = None, params: dict = None, **kwargs) -> requests.Response:
117123
"""Sends a DELETE request.
118124
119125
A wrapper for the requests.delete method.
@@ -124,6 +130,8 @@ def delete(self, url: str, json: dict = None, **kwargs) -> requests.Response:
124130
:type url: str
125131
:param json: A JSON serializable Python object to send in the body of the Request, defaults to None
126132
:type json: dict, optional
133+
:param params: Dictionary of querystring data to attach to the Request, defaults to None
134+
:type params: dict, optional
127135
128136
:raises APIException: an api exception with message and error type code
129137
@@ -135,7 +143,8 @@ def delete(self, url: str, json: dict = None, **kwargs) -> requests.Response:
135143
url = self._add_base_url(url)
136144
headers = self._generate_headers()
137145

138-
response = requests.delete(url, headers=headers, json=json, **kwargs)
146+
response = requests.delete(
147+
url, headers=headers, json=json, params=params, **kwargs)
139148
handle_error(response)
140149

141150
return response

datacrunch/instances/instances.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -426,16 +426,35 @@ def action(self, id_list: Union[List[str], str], action: str, volume_ids: Option
426426
self._http_client.put(INSTANCES_ENDPOINT, json=payload)
427427
return
428428

429-
def is_available(self, instance_type: str, is_spot: bool = None) -> bool:
429+
# TODO: use enum/const for location_code
430+
def is_available(self, instance_type: str, is_spot: bool = False, location_code: str = None) -> bool:
430431
"""Returns True if a specific instance type is now available for deployment
431432
432433
:param instance_type: instance type
433434
:type instance_type: str
434435
:param is_spot: Is spot instance
435436
:type is_spot: bool, optional
437+
:param location_code: datacenter location, defaults to "FIN-01"
438+
:type location_code: str, optional
436439
:return: True if available to deploy, False otherwise
437440
:rtype: bool
438441
"""
439-
query_param = '?isSpot=true' if is_spot else ''
440-
url = f'/instance-availability/{instance_type}{query_param}'
441-
return self._http_client.get(url).json()
442+
is_spot = str(is_spot).lower()
443+
query_params = {'isSpot': is_spot, 'location_code': location_code}
444+
url = f'/instance-availability/{instance_type}'
445+
return self._http_client.get(url, query_params).json()
446+
447+
# TODO: use enum/const for location_code
448+
def get_availabilities(self, is_spot: bool = None, location_code: str = None) -> bool:
449+
"""Returns a list of available instance types
450+
451+
:param is_spot: Is spot instance
452+
:type is_spot: bool, optional
453+
:param location_code: datacenter location, defaults to "FIN-01"
454+
:type location_code: str, optional
455+
:return: list of available instance types in every location
456+
:rtype: list
457+
"""
458+
is_spot = str(is_spot).lower() if is_spot is not None else None
459+
query_params = {'isSpot': is_spot, 'locationCode': location_code}
460+
return self._http_client.get('/instance-availability', params=query_params).json()

datacrunch/locations/__init__.py

Whitespace-only changes.

datacrunch/locations/locations.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from typing import List
2+
3+
LOCATIONS_ENDPOINT = '/locations'
4+
5+
6+
class LocationsService:
7+
"""A service for interacting with the locations endpoint"""
8+
9+
def __init__(self, http_client) -> None:
10+
self._http_client = http_client
11+
12+
def get(self) -> List[dict]:
13+
"""Get all locations
14+
"""
15+
locations = self._http_client.get(LOCATIONS_ENDPOINT).json()
16+
return locations
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import os
2+
import pytest
3+
from datacrunch.datacrunch import DataCrunchClient
4+
from datacrunch.constants import Locations
5+
6+
IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true"
7+
8+
location_codes = [Locations.FIN_01, Locations.ICE_01]
9+
10+
11+
@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions.")
12+
@pytest.mark.withoutresponses
13+
class TestLocations():
14+
15+
def test_specific_instance_availability_in_specific_location(self, datacrunch_client: DataCrunchClient):
16+
# call the instance availability endpoint, for a specific location
17+
availability = datacrunch_client.instances.is_available(
18+
'CPU.4V', location_code=Locations.FIN_01)
19+
20+
assert availability is not None
21+
assert isinstance(availability, bool)
22+
23+
def test_all_availabilies_in_specific_location(self, datacrunch_client: DataCrunchClient):
24+
25+
# call the instance availability endpoint, for a specific location
26+
availabilities = datacrunch_client.instances.get_availabilities(
27+
location_code=Locations.FIN_01)
28+
29+
assert availabilities is not None
30+
assert isinstance(availabilities, list)
31+
assert len(availabilities) == 1
32+
assert availabilities[0]['location_code'] in location_codes
33+
assert isinstance(availabilities[0]['availabilities'], list)
34+
assert len(availabilities[0]['availabilities']) > 0
35+
36+
def test_all_availabilites(self, datacrunch_client: DataCrunchClient):
37+
# call the instance availability endpoint, for all locations
38+
all_availabilities = datacrunch_client.instances.get_availabilities()
39+
40+
assert all_availabilities is not None
41+
assert isinstance(all_availabilities, list)
42+
assert len(all_availabilities) > 1
43+
assert all_availabilities[0]['location_code'] in location_codes
44+
assert all_availabilities[1]['location_code'] in location_codes
45+
assert isinstance(all_availabilities[0]['availabilities'], list)
46+
assert len(all_availabilities[0]['availabilities']) > 0
47+
48+
def test_get_all_locations(self, datacrunch_client: DataCrunchClient):
49+
# call the locations endpoint
50+
locations = datacrunch_client.locations.get()
51+
52+
assert locations is not None
53+
assert isinstance(locations, list)
54+
55+
assert locations[0]['code'] in location_codes
56+
assert locations[1]['code'] in location_codes
57+
assert locations[0]['code'] != locations[1]['code']
58+
59+
assert locations[0]['name'] is not None
60+
assert locations[1]['name'] is not None
61+
assert locations[0]['name'] != locations[1]['name']
62+
63+
assert locations[0]['country_code'] is not None
64+
assert locations[1]['country_code'] is not None
65+
assert locations[0]['country_code'] != locations[1]['country_code']

tests/integration_tests/test_volumes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import time
23
import pytest
34
from datacrunch.datacrunch import DataCrunchClient
45
from datacrunch.constants import Locations, VolumeTypes, VolumeStatus
@@ -38,6 +39,9 @@ def test_permanently_delete_detached_volumes(seld, datacrunch_client):
3839
# permanently delete the detached volume
3940
datacrunch_client.volumes.delete(volume.id, is_permanent=True)
4041

42+
# sleep for 2 seconds
43+
time.sleep(2)
44+
4145
# make sure the volume is not in trash
4246
volumes = datacrunch_client.volumes.get_in_trash()
4347

@@ -58,6 +62,9 @@ def test_permanently_delete_a_deleted_volume_from_trash(self, datacrunch_client)
5862
# delete volume
5963
datacrunch_client.volumes.delete(volume.id)
6064

65+
# sleep for 2 seconds
66+
time.sleep(2)
67+
6168
# permanently delete the volume
6269
datacrunch_client.volumes.delete(volume.id, is_permanent=True)
6370

0 commit comments

Comments
 (0)