Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Changelog
=========

* Added locations endpoint and location code parameter to the availability endpoints

v1.5.10 (2023-06-28)
-------------------

Expand Down
4 changes: 2 additions & 2 deletions datacrunch/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ def __init__(self):


class Locations:
FIN_01 = "FIN-01"
ICE_01 = "ICE-01"
FIN_01: str = "FIN-01"
ICE_01: str = "ICE-01"

def __init__(self):
return
Expand Down
5 changes: 5 additions & 0 deletions datacrunch/datacrunch.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from datacrunch.volume_types.volume_types import VolumeTypesService
from datacrunch.volumes.volumes import VolumesService
from datacrunch.constants import Constants
from datacrunch.locations.locations import LocationsService
from datacrunch.__version__ import VERSION


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

self.volumes: VolumesService = VolumesService(self._http_client)
"""Volume service. Create, attach, detach, get, rename, delete volumes"""

self.locations: LocationsService = LocationsService(
self._http_client)
"""Locations service. Get locations"""
23 changes: 16 additions & 7 deletions datacrunch/http_client/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self, auth_service, base_url: str) -> None:
self._auth_service = auth_service
self._auth_service.authenticate()

def post(self, url: str, json: dict = None, **kwargs) -> requests.Response:
def post(self, url: str, json: dict = None, params: dict = None, **kwargs) -> requests.Response:
"""Sends a POST request.

A wrapper for the requests.post method.
Expand All @@ -43,6 +43,8 @@ def post(self, url: str, json: dict = None, **kwargs) -> requests.Response:
:type url: str
:param json: A JSON serializable Python object to send in the body of the Request, defaults to None
:type json: dict, optional
:param params: Dictionary of querystring data to attach to the Request, defaults to None
:type params: dict, optional

:raises APIException: an api exception with message and error type code

Expand All @@ -54,12 +56,13 @@ def post(self, url: str, json: dict = None, **kwargs) -> requests.Response:
url = self._add_base_url(url)
headers = self._generate_headers()

response = requests.post(url, json=json, headers=headers, **kwargs)
response = requests.post(
url, json=json, headers=headers, params=params, **kwargs)
handle_error(response)

return response

def put(self, url: str, json: dict = None, **kwargs) -> requests.Response:
def put(self, url: str, json: dict = None, params: dict = None, **kwargs) -> requests.Response:
"""Sends a PUT request.

A wrapper for the requests.put method.
Expand All @@ -70,6 +73,8 @@ def put(self, url: str, json: dict = None, **kwargs) -> requests.Response:
:type url: str
:param json: A JSON serializable Python object to send in the body of the Request, defaults to None
:type json: dict, optional
:param params: Dictionary of querystring data to attach to the Request, defaults to None
:type params: dict, optional

:raises APIException: an api exception with message and error type code

Expand All @@ -81,7 +86,8 @@ def put(self, url: str, json: dict = None, **kwargs) -> requests.Response:
url = self._add_base_url(url)
headers = self._generate_headers()

response = requests.put(url, json=json, headers=headers, **kwargs)
response = requests.put(
url, json=json, headers=headers, params=params, **kwargs)
handle_error(response)

return response
Expand All @@ -95,7 +101,7 @@ def get(self, url: str, params: dict = None, **kwargs) -> requests.Response:

:param url: relative url of the API endpoint
:type url: str
:param params: Dictionary, list of tuples or bytes to send in the query string for the Request. defaults to None
:param params: Dictionary of querystring data to attach to the Request, defaults to None
:type params: dict, optional

:raises APIException: an api exception with message and error type code
Expand All @@ -113,7 +119,7 @@ def get(self, url: str, params: dict = None, **kwargs) -> requests.Response:

return response

def delete(self, url: str, json: dict = None, **kwargs) -> requests.Response:
def delete(self, url: str, json: dict = None, params: dict = None, **kwargs) -> requests.Response:
"""Sends a DELETE request.

A wrapper for the requests.delete method.
Expand All @@ -124,6 +130,8 @@ def delete(self, url: str, json: dict = None, **kwargs) -> requests.Response:
:type url: str
:param json: A JSON serializable Python object to send in the body of the Request, defaults to None
:type json: dict, optional
:param params: Dictionary of querystring data to attach to the Request, defaults to None
:type params: dict, optional

:raises APIException: an api exception with message and error type code

Expand All @@ -135,7 +143,8 @@ def delete(self, url: str, json: dict = None, **kwargs) -> requests.Response:
url = self._add_base_url(url)
headers = self._generate_headers()

response = requests.delete(url, headers=headers, json=json, **kwargs)
response = requests.delete(
url, headers=headers, json=json, params=params, **kwargs)
handle_error(response)

return response
Expand Down
27 changes: 23 additions & 4 deletions datacrunch/instances/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,16 +426,35 @@ def action(self, id_list: Union[List[str], str], action: str, volume_ids: Option
self._http_client.put(INSTANCES_ENDPOINT, json=payload)
return

def is_available(self, instance_type: str, is_spot: bool = None) -> bool:
# TODO: use enum/const for location_code
def is_available(self, instance_type: str, is_spot: bool = False, location_code: str = None) -> bool:
"""Returns True if a specific instance type is now available for deployment

:param instance_type: instance type
:type instance_type: str
:param is_spot: Is spot instance
:type is_spot: bool, optional
:param location_code: datacenter location, defaults to "FIN-01"
:type location_code: str, optional
:return: True if available to deploy, False otherwise
:rtype: bool
"""
query_param = '?isSpot=true' if is_spot else ''
url = f'/instance-availability/{instance_type}{query_param}'
return self._http_client.get(url).json()
is_spot = str(is_spot).lower()
query_params = {'isSpot': is_spot, 'location_code': location_code}
url = f'/instance-availability/{instance_type}'
return self._http_client.get(url, query_params).json()

# TODO: use enum/const for location_code
def get_availabilities(self, is_spot: bool = None, location_code: str = None) -> bool:
"""Returns a list of available instance types

:param is_spot: Is spot instance
:type is_spot: bool, optional
:param location_code: datacenter location, defaults to "FIN-01"
:type location_code: str, optional
:return: list of available instance types in every location
:rtype: list
"""
is_spot = str(is_spot).lower() if is_spot is not None else None
query_params = {'isSpot': is_spot, 'locationCode': location_code}
return self._http_client.get('/instance-availability', params=query_params).json()
Empty file.
16 changes: 16 additions & 0 deletions datacrunch/locations/locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from typing import List

LOCATIONS_ENDPOINT = '/locations'


class LocationsService:
"""A service for interacting with the locations endpoint"""

def __init__(self, http_client) -> None:
self._http_client = http_client

def get(self) -> List[dict]:
"""Get all locations
"""
locations = self._http_client.get(LOCATIONS_ENDPOINT).json()
return locations
65 changes: 65 additions & 0 deletions tests/integration_tests/test_locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import os
import pytest
from datacrunch.datacrunch import DataCrunchClient
from datacrunch.constants import Locations

IN_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true"

location_codes = [Locations.FIN_01, Locations.ICE_01]


@pytest.mark.skipif(IN_GITHUB_ACTIONS, reason="Test doesn't work in Github Actions.")
@pytest.mark.withoutresponses
class TestLocations():

def test_specific_instance_availability_in_specific_location(self, datacrunch_client: DataCrunchClient):
# call the instance availability endpoint, for a specific location
availability = datacrunch_client.instances.is_available(
'CPU.4V', location_code=Locations.FIN_01)

assert availability is not None
assert isinstance(availability, bool)

def test_all_availabilies_in_specific_location(self, datacrunch_client: DataCrunchClient):

# call the instance availability endpoint, for a specific location
availabilities = datacrunch_client.instances.get_availabilities(
location_code=Locations.FIN_01)

assert availabilities is not None
assert isinstance(availabilities, list)
assert len(availabilities) == 1
assert availabilities[0]['location_code'] in location_codes
assert isinstance(availabilities[0]['availabilities'], list)
assert len(availabilities[0]['availabilities']) > 0

def test_all_availabilites(self, datacrunch_client: DataCrunchClient):
# call the instance availability endpoint, for all locations
all_availabilities = datacrunch_client.instances.get_availabilities()

assert all_availabilities is not None
assert isinstance(all_availabilities, list)
assert len(all_availabilities) > 1
assert all_availabilities[0]['location_code'] in location_codes
assert all_availabilities[1]['location_code'] in location_codes
assert isinstance(all_availabilities[0]['availabilities'], list)
assert len(all_availabilities[0]['availabilities']) > 0

def test_get_all_locations(self, datacrunch_client: DataCrunchClient):
# call the locations endpoint
locations = datacrunch_client.locations.get()

assert locations is not None
assert isinstance(locations, list)

assert locations[0]['code'] in location_codes
assert locations[1]['code'] in location_codes
assert locations[0]['code'] != locations[1]['code']

assert locations[0]['name'] is not None
assert locations[1]['name'] is not None
assert locations[0]['name'] != locations[1]['name']

assert locations[0]['country_code'] is not None
assert locations[1]['country_code'] is not None
assert locations[0]['country_code'] != locations[1]['country_code']
7 changes: 7 additions & 0 deletions tests/integration_tests/test_volumes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import time
import pytest
from datacrunch.datacrunch import DataCrunchClient
from datacrunch.constants import Locations, VolumeTypes, VolumeStatus
Expand Down Expand Up @@ -38,6 +39,9 @@ def test_permanently_delete_detached_volumes(seld, datacrunch_client):
# permanently delete the detached volume
datacrunch_client.volumes.delete(volume.id, is_permanent=True)

# sleep for 2 seconds
time.sleep(2)

# make sure the volume is not in trash
volumes = datacrunch_client.volumes.get_in_trash()

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

# sleep for 2 seconds
time.sleep(2)

# permanently delete the volume
datacrunch_client.volumes.delete(volume.id, is_permanent=True)

Expand Down
5 changes: 3 additions & 2 deletions tests/unit_tests/instances/test_instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ def test_action_failed(self, instances_service, endpoint):
def test_is_available_successful(self, instances_service):
# arrange - add response mock
url = instances_service._http_client._base_url + \
'/instance-availability/' + INSTANCE_TYPE
'/instance-availability/' + INSTANCE_TYPE + "?isSpot=false"
responses.add(
responses.GET,
url,
Expand Down Expand Up @@ -464,7 +464,8 @@ def test_is_spot_available_successful(self, instances_service):

def test_is_available_failed(self, instances_service):
# arrange - add response mock
url = instances_service._http_client._base_url + '/instance-availability/x'
url = instances_service._http_client._base_url + \
'/instance-availability/x' + "?isSpot=false"
responses.add(
responses.GET,
url,
Expand Down