From e3ea940dbaf968c9efce339bebda9d791606229a Mon Sep 17 00:00:00 2001 From: Tamir Date: Tue, 1 Apr 2025 13:25:23 +0300 Subject: [PATCH 01/10] use dataclasses and dataclasses_json for Instance --- datacrunch/instances/instances.py | 357 +++--------------------------- 1 file changed, 30 insertions(+), 327 deletions(-) diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index 4539abb..9fc05aa 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -1,4 +1,6 @@ from typing import List, Union, Optional, Dict, Literal +from dataclasses import dataclass +from dataclasses_json import dataclass_json from datacrunch.helpers import stringify_class_object_properties from datacrunch.constants import Locations @@ -7,283 +9,33 @@ Contract = Literal['LONG_TERM', 'PAY_AS_YOU_GO', 'SPOT'] Pricing = Literal['DYNAMIC_PRICE', 'FIXED_PRICE'] + +@dataclass_json +@dataclass class Instance: """An instance model class""" - def __init__(self, - id: str, - instance_type: str, - image: str, - price_per_hour: float, - hostname: str, - description: str, - ip: str, - status: str, - created_at: str, - ssh_key_ids: List[str], - cpu: dict, - gpu: dict, - memory: dict, - storage: dict, - os_volume_id: str, - gpu_memory: dict, - location: str = Locations.FIN_01, - startup_script_id: str = None, - is_spot: bool = False, - contract: Contract = None, - pricing: Pricing = None, - ) -> None: - """Initialize the instance object - - :param id: instance id - :type id: str - :param instance_type: instance type. e.g. '8V100.48M' - :type instance_type: str - :param image: instance image type. e.g. 'ubuntu-20.04-cuda-11.0' - :type image: str - :param price_per_hour: price per hour - :type price_per_hour: float - :param hostname: instance hostname - :type hostname: str - :param description: instance description - :type description: str - :param ip: instance ip address - :type ip: str - :param status: instance current status, might be out of date if changed - :type status: str - :param created_at: the time the instance was deployed (UTC) - :type created_at: str - :param ssh_key_ids: list of ssh keys ids - :type ssh_key_ids: List[str] - :param cpu: cpu details - :type cpu: dict - :param gpu: gpu details - :type gpu: dict - :param memory: memory details - :type memory: dict - :param storage: storate details - :type storage: dict - :param id: main OS volume id - :type id: str - :param memory: gpu memory details - :type memory: dict - :param location: datacenter location, defaults to "FIN-01" - :type location: str, optional - :param startup_script_id: startup script id, defaults to None - :type startup_script_id: str, optional - :param is_spot: is this a spot instance, defaults to None - :type is_spot: bool, optional - """ - self._id = id - self._instance_type = instance_type - self._image = image - self._price_per_hour = price_per_hour - self._location = location - self._hostname = hostname - self._description = description - self._ip = ip - self._status = status - self._created_at = created_at - self._ssh_key_ids = ssh_key_ids - self._startup_script_id = startup_script_id - self._cpu = cpu - self._gpu = gpu - self._memory = memory - self._storage = storage - self._os_volume_id = os_volume_id - self._gpu_memory = gpu_memory - self._is_spot = is_spot - self._contract = contract - self._pricing = pricing - - @property - def id(self) -> str: - """Get the instance id - - :return: instance id - :rtype: str - """ - return self._id - - @property - def instance_type(self) -> str: - """Get the instance type - - :return: instance type - :rtype: str - """ - return self._instance_type - - @property - def image(self) -> str: - """Get the instance image type - - :return: instance image type - :rtype: str - """ - return self._image - - @property - def price_per_hour(self) -> float: - """Get the instance price per hour - - :return: price per hour - :rtype: float - """ - return self._price_per_hour - - @property - def location(self) -> str: - """Get the instance datacenter location - - :return: datacenter location - :rtype: str - """ - return self._location - - @property - def hostname(self) -> str: - """Get the instance hostname - - :return: hostname - :rtype: str - """ - return self._hostname - - @property - def description(self) -> str: - """Get the instance description - - :return: instance description - :rtype: str - """ - return self._description - - @property - def ip(self) -> str: - """Get the instance ip address - - :return: ip address - :rtype: str - """ - return self._ip - - @property - def status(self) -> str: - """Get the current instance status. might be out of date if changed. - - :return: instance status - :rtype: str - """ - return self._status - - @property - def created_at(self) -> str: - """Get the time when the instance was deployed (UTC) - - :return: time - :rtype: str - """ - return self._created_at - - @property - def ssh_key_ids(self) -> List[str]: - """Get the SSH key IDs of the instance - - :return: SSH key IDs - :rtype: List[str] - """ - return self._ssh_key_ids - - @property - def startup_script_id(self) -> Union[str, None]: - """Get the startup script ID or None if the is no script - - :return: startup script ID or None - :rtype: Union[str, None] - """ - return self._startup_script_id - - @property - def cpu(self) -> dict: - """Get the instance cpu details - - :return: cpu details - :rtype: dict - """ - return self._cpu - - @property - def gpu(self) -> dict: - """Get the instance gpu details - - :return: gpu details - :rtype: dict - """ - return self._gpu - - @property - def memory(self) -> dict: - """Get the instance memory details - - :return: memory details - :rtype: dict - """ - return self._memory - - @property - def storage(self) -> dict: - """Get the instance storage details - - :return: storage details - :rtype: dict - """ - return self._storage - - @property - def os_volume_id(self) -> str: - """Get the main os volume id - - :return: main os volume id - :rtype: str - """ - return self._os_volume_id - - @property - def gpu_memory(self) -> dict: - """Get the instance gpu_memory details - - :return: gpu_memory details - :rtype: dict - """ - return self._gpu_memory - - @property - def is_spot(self) -> bool: - """Is this a spot instance - - :return: is spot details - :rtype: bool - """ - return self._is_spot - - @property - def contract(self) -> bool: - """Get contract type - - :return: contract type - :rtype: str - """ - return self._contract - - @property - def pricing(self) -> bool: - """Get pricing type - - :return: pricing type - :rtype: str - """ - return self._pricing + id: str + instance_type: str + price_per_hour: float + hostname: str + description: str + ip: str + status: str + created_at: str + ssh_key_ids: List[str] + cpu: dict + gpu: dict + memory: dict + storage: dict + os_volume_id: str + gpu_memory: dict + location: str = Locations.FIN_01 + image: Optional[str] = None + startup_script_id: Optional[str] = None + is_spot: bool = False + contract: Optional[Contract] = None + pricing: Optional[Pricing] = None def __str__(self) -> str: """Returns a string of the json representation of the instance @@ -310,31 +62,7 @@ def get(self, status: str = None) -> List[Instance]: """ instances_dict = self._http_client.get( INSTANCES_ENDPOINT, params={'status': status}).json() - instances = list(map(lambda instance_dict: Instance( - id=instance_dict['id'], - instance_type=instance_dict['instance_type'], - image=instance_dict['image'], - price_per_hour=instance_dict['price_per_hour'] if 'price_per_hour' in instance_dict else None, - location=instance_dict['location'], - hostname=instance_dict['hostname'], - description=instance_dict['description'], - ip=instance_dict['ip'], - status=instance_dict['status'], - created_at=instance_dict['created_at'], - ssh_key_ids=instance_dict['ssh_key_ids'] if 'ssh_key_ids' in instance_dict else [ - ], - startup_script_id=instance_dict['startup_script_id'] if 'startup_script_id' in instance_dict else None, - cpu=instance_dict['cpu'], - gpu=instance_dict['gpu'], - memory=instance_dict['memory'], - storage=instance_dict['storage'], - os_volume_id=instance_dict['os_volume_id'] if 'os_volume_id' in instance_dict else None, - gpu_memory=instance_dict['gpu_memory'] if 'gpu_memory' in instance_dict else None, - is_spot=instance_dict['is_spot'] if 'is_spot' in instance_dict else False, - contract=instance_dict['contract'] if 'contract' in instance_dict else False, - pricing=instance_dict['pricing'] if 'pricing' in instance_dict else False, - ), instances_dict)) - return instances + return [Instance.from_dict(instance_dict, infer_missing=True) for instance_dict in instances_dict] def get_by_id(self, id: str) -> Instance: """Get an instance with specified id. @@ -346,31 +74,7 @@ def get_by_id(self, id: str) -> Instance: """ instance_dict = self._http_client.get( INSTANCES_ENDPOINT + f'/{id}').json() - instance = Instance( - id=instance_dict['id'], - instance_type=instance_dict['instance_type'], - image=instance_dict['image'], - price_per_hour=instance_dict['price_per_hour'] if 'price_per_hour' in instance_dict else None, - location=instance_dict['location'], - hostname=instance_dict['hostname'], - description=instance_dict['description'], - ip=instance_dict['ip'], - status=instance_dict['status'], - created_at=instance_dict['created_at'], - ssh_key_ids=instance_dict['ssh_key_ids'] if 'ssh_key_ids' in instance_dict else [ - ], - startup_script_id=instance_dict['startup_script_id'] if 'startup_script_id' in instance_dict else None, - cpu=instance_dict['cpu'], - gpu=instance_dict['gpu'], - memory=instance_dict['memory'], - storage=instance_dict['storage'], - os_volume_id=instance_dict['os_volume_id'] if 'os_volume_id' in instance_dict else None, - gpu_memory=instance_dict['gpu_memory'] if 'gpu_memory' in instance_dict else None, - is_spot=instance_dict['is_spot'] if 'is_spot' in instance_dict else False, - contract=instance_dict['contract'] if 'contract' in instance_dict else False, - pricing=instance_dict['pricing'] if 'pricing' in instance_dict else False, - ) - return instance + return Instance.from_dict(instance_dict, infer_missing=True) def create(self, instance_type: str, @@ -418,7 +122,7 @@ def create(self, :param coupon: coupon code :type coupon: str, optional :return: the new instance object - :rtype: id + :rtype: Instance """ payload = { "instance_type": instance_type, @@ -439,8 +143,7 @@ def create(self, if pricing: payload['pricing'] = pricing id = self._http_client.post(INSTANCES_ENDPOINT, json=payload).text - instance = self.get_by_id(id) - return instance + return self.get_by_id(id) def action(self, id_list: Union[List[str], str], action: str, volume_ids: Optional[List[str]] = None) -> None: """Performs an action on a list of instances / single instance From 53256cbaa95d557e0a04bb59895fa3dc0e7464a5 Mon Sep 17 00:00:00 2001 From: Tamir Date: Wed, 2 Apr 2025 09:17:07 +0300 Subject: [PATCH 02/10] added instance status ordered --- datacrunch/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/datacrunch/constants.py b/datacrunch/constants.py index 15a4681..2de6667 100644 --- a/datacrunch/constants.py +++ b/datacrunch/constants.py @@ -22,6 +22,7 @@ def __init__(self): class InstanceStatus: + ORDERED = 'ordered' RUNNING = 'running' PROVISIONING = 'provisioning' OFFLINE = 'offline' From e28bc59e7dffe4abe35a432fcf460f0b53a2b4b6 Mon Sep 17 00:00:00 2001 From: Tamir Date: Thu, 3 Apr 2025 10:55:50 +0300 Subject: [PATCH 03/10] refactored docstrings --- datacrunch/instances/instances.py | 196 +++++++++++++++++------------- 1 file changed, 112 insertions(+), 84 deletions(-) diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index 9fc05aa..aebbe41 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -13,7 +13,31 @@ @dataclass_json @dataclass class Instance: - """An instance model class""" + """Represents a cloud instance with its configuration and state. + + Attributes: + id (str): Unique identifier for the instance. + instance_type (str): Type of the instance (e.g., '8V100.48V'). + price_per_hour (float): Cost per hour of running the instance. + hostname (str): Network hostname of the instance. + description (str): Human-readable description of the instance. + ip (str): IP address assigned to the instance. + status (str): Current operational status of the instance. + created_at (str): Timestamp of instance creation. + ssh_key_ids (List[str]): List of SSH key IDs associated with the instance. + cpu (dict): CPU configuration details. + gpu (dict): GPU configuration details. + memory (dict): Memory configuration details. + storage (dict): Storage configuration details. + os_volume_id (str): ID of the operating system volume. + gpu_memory (dict): GPU memory configuration details. + location (str): Datacenter location code (default: Locations.FIN_01). + image (Optional[str]): Image ID or type used for the instance. + startup_script_id (Optional[str]): ID of the startup script to run. + is_spot (bool): Whether the instance is a spot instance. + contract (Optional[Contract]): Contract type for the instance. (e.g. 'LONG_TERM', 'PAY_AS_YOU_GO', 'SPOT') + pricing (Optional[Pricing]): Pricing model for the instance. (e.g. 'DYNAMIC_PRICE', 'FIXED_PRICE') + """ id: str instance_type: str @@ -38,39 +62,54 @@ class Instance: pricing: Optional[Pricing] = None def __str__(self) -> str: - """Returns a string of the json representation of the instance + """Returns a JSON string representation of the instance. - :return: json representation of the instance - :rtype: str + Returns: + str: JSON string containing all instance properties. """ return stringify_class_object_properties(self) class InstancesService: - """A service for interacting with the instances endpoint""" + """Service for managing cloud instances through the API. + + This service provides methods to create, retrieve, and manage cloud instances + through the DataCrunch API. + """ def __init__(self, http_client) -> None: + """Initializes the InstancesService with an HTTP client. + + Args: + http_client: HTTP client for making API requests. + """ self._http_client = http_client - def get(self, status: str = None) -> List[Instance]: - """Get all of the client's non-deleted instances, or instances with specific status. + def get(self, status: Optional[str] = None) -> List[Instance]: + """Retrieves all non-deleted instances or instances with specific status. - :param status: optional, status of the instances, defaults to None - :type status: str, optional - :return: list of instance details objects - :rtype: List[Instance] + Args: + status: Optional status filter for instances. If None, returns all + non-deleted instances. + + Returns: + List[Instance]: List of instance objects matching the criteria. """ instances_dict = self._http_client.get( INSTANCES_ENDPOINT, params={'status': status}).json() return [Instance.from_dict(instance_dict, infer_missing=True) for instance_dict in instances_dict] def get_by_id(self, id: str) -> Instance: - """Get an instance with specified id. + """Retrieves a specific instance by its ID. + + Args: + id: Unique identifier of the instance to retrieve. + + Returns: + Instance: Instance object with the specified ID. - :param id: instance id - :type id: str - :return: instance details object - :rtype: Instance + Raises: + HTTPError: If the instance is not found or other API error occurs. """ instance_dict = self._http_client.get( INSTANCES_ENDPOINT + f'/{id}').json() @@ -83,46 +122,37 @@ def create(self, description: str, ssh_key_ids: list = [], location: str = Locations.FIN_01, - startup_script_id: str = None, - volumes: List[Dict] = None, - existing_volumes: List[str] = None, - os_volume: Dict = None, + startup_script_id: Optional[str] = None, + volumes: Optional[List[Dict]] = None, + existing_volumes: Optional[List[str]] = None, + os_volume: Optional[Dict] = None, is_spot: bool = False, - contract: Contract = None, - pricing: Pricing = None, - coupon: str = None) -> Instance: - """Creates (deploys) a new instance - - :param instance_type: instance type. e.g. '8V100.48M' - :type instance_type: str - :param image: instance image type. e.g. 'ubuntu-20.04-cuda-11.0', or existing OS volume id - :type image: str - :param ssh_key_ids: list of ssh key ids - :type ssh_key_ids: list - :param hostname: instance hostname - :type hostname: str - :param description: instance description - :type description: str - :param location: datacenter location, defaults to "FIN-01" - :type location: str, optional - :param startup_script_id: startup script id, defaults to None - :type startup_script_id: str, optional - :param volumes: List of volume data dictionaries to create alongside the instance - :type volumes: List[Dict], optional - :param existing_volumes: List of existing volume ids to attach to the instance - :type existing_volumes: List[str], optional - :param os_volume: OS volume details, defaults to None - :type os_volume: Dict, optional - :param is_spot: Is spot instance - :type is_spot: bool, optional - :param pricing: Pricing type - :type pricing: str, optional - :param contract: Contract type - :type contract: str, optional - :param coupon: coupon code - :type coupon: str, optional - :return: the new instance object - :rtype: Instance + contract: Optional[Contract] = None, + pricing: Optional[Pricing] = None, + coupon: Optional[str] = None) -> Instance: + """Creates and deploys a new cloud instance. + + Args: + instance_type: Type of instance to create (e.g., '8V100.48V'). + image: Image type or existing OS volume ID for the instance. + hostname: Network hostname for the instance. + description: Human-readable description of the instance. + ssh_key_ids: List of SSH key IDs to associate with the instance. + location: Datacenter location code (default: Locations.FIN_01). + startup_script_id: Optional ID of startup script to run. + volumes: Optional list of volume configurations to create. + existing_volumes: Optional list of existing volume IDs to attach. + os_volume: Optional OS volume configuration details. + is_spot: Whether to create a spot instance. + contract: Optional contract type for the instance. + pricing: Optional pricing model for the instance. + coupon: Optional coupon code for discounts. + + Returns: + Instance: The newly created instance object. + + Raises: + HTTPError: If instance creation fails or other API error occurs. """ payload = { "instance_type": instance_type, @@ -146,14 +176,15 @@ def create(self, return self.get_by_id(id) def action(self, id_list: Union[List[str], str], action: str, volume_ids: Optional[List[str]] = None) -> None: - """Performs an action on a list of instances / single instance - - :param id_list: list of instance ids, or an instance id - :type id_list: Union[List[str], str] - :param action: the action to perform - :type action: str - :param volume_ids: the volume ids to delete - :type volume_ids: Optional[List[str]] + """Performs an action on one or more instances. + + Args: + id_list: Single instance ID or list of instance IDs to act upon. + action: Action to perform on the instances. + volume_ids: Optional list of volume IDs to delete. + + Raises: + HTTPError: If the action fails or other API error occurs. """ if type(id_list) is str: id_list = [id_list] @@ -167,34 +198,31 @@ def action(self, id_list: Union[List[str], str], action: str, volume_ids: Option self._http_client.put(INSTANCES_ENDPOINT, json=payload) return - # 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 + def is_available(self, instance_type: str, is_spot: bool = False, location_code: Optional[str] = None) -> bool: + """Checks if a specific instance type is available for deployment. + + Args: + instance_type: Type of instance to check availability for. + is_spot: Whether to check spot instance availability. + location_code: Optional datacenter location code. + + Returns: + bool: True if the instance type is available, False otherwise. """ 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 + def get_availabilities(self, is_spot: Optional[bool] = None, location_code: Optional[str] = None) -> List[Dict]: + """Retrieves a list of available instance types across locations. + + Args: + is_spot: Optional flag to filter spot instance availability. + location_code: Optional datacenter location code to filter by. - :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 + Returns: + List[Dict]: List of available instance types and their details. """ is_spot = str(is_spot).lower() if is_spot is not None else None query_params = {'isSpot': is_spot, 'locationCode': location_code} From 1a6cfa888e97172a7cecd89a274ea7e30827cff5 Mon Sep 17 00:00:00 2001 From: Tamir Date: Mon, 7 Apr 2025 13:44:41 +0300 Subject: [PATCH 04/10] changelog --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2d10c71..c2a14f7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,8 @@ Changelog ========= +* Refactor: use dataclasses and google docstring style in instances.py + v1.9.0 (2025-04-04) ------------------- From 08ce649e1bacf7598fad95081ffdc04b7b8bf768 Mon Sep 17 00:00:00 2001 From: Tamir Date: Mon, 7 Apr 2025 15:51:44 +0300 Subject: [PATCH 05/10] removed redundant types from docstring --- datacrunch/instances/instances.py | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index aebbe41..8a33b83 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -16,27 +16,27 @@ class Instance: """Represents a cloud instance with its configuration and state. Attributes: - id (str): Unique identifier for the instance. - instance_type (str): Type of the instance (e.g., '8V100.48V'). - price_per_hour (float): Cost per hour of running the instance. - hostname (str): Network hostname of the instance. - description (str): Human-readable description of the instance. - ip (str): IP address assigned to the instance. - status (str): Current operational status of the instance. - created_at (str): Timestamp of instance creation. - ssh_key_ids (List[str]): List of SSH key IDs associated with the instance. - cpu (dict): CPU configuration details. - gpu (dict): GPU configuration details. - memory (dict): Memory configuration details. - storage (dict): Storage configuration details. - os_volume_id (str): ID of the operating system volume. - gpu_memory (dict): GPU memory configuration details. - location (str): Datacenter location code (default: Locations.FIN_01). - image (Optional[str]): Image ID or type used for the instance. - startup_script_id (Optional[str]): ID of the startup script to run. - is_spot (bool): Whether the instance is a spot instance. - contract (Optional[Contract]): Contract type for the instance. (e.g. 'LONG_TERM', 'PAY_AS_YOU_GO', 'SPOT') - pricing (Optional[Pricing]): Pricing model for the instance. (e.g. 'DYNAMIC_PRICE', 'FIXED_PRICE') + id: Unique identifier for the instance. + instance_type: Type of the instance (e.g., '8V100.48V'). + price_per_hour: Cost per hour of running the instance. + hostname: Network hostname of the instance. + description: Human-readable description of the instance. + ip: IP address assigned to the instance. + status: Current operational status of the instance. + created_at: Timestamp of instance creation. + ssh_key_ids: List of SSH key IDs associated with the instance. + cpu: CPU configuration details. + gpu: GPU configuration details. + memory: Memory configuration details. + storage: Storage configuration details. + os_volume_id: ID of the operating system volume. + gpu_memory: GPU memory configuration details. + location: Datacenter location code (default: Locations.FIN_01). + image: Image ID or type used for the instance. + startup_script_id: ID of the startup script to run. + is_spot: Whether the instance is a spot instance. + contract: Contract type for the instance. (e.g. 'LONG_TERM', 'PAY_AS_YOU_GO', 'SPOT') + pricing: Pricing model for the instance. (e.g. 'DYNAMIC_PRICE', 'FIXED_PRICE') """ id: str From 4b4ba016ec6cd3ea7db7e2fe99414098bb2c1dfd Mon Sep 17 00:00:00 2001 From: Tamir Date: Mon, 7 Apr 2025 15:54:16 +0300 Subject: [PATCH 06/10] removed more types from docstring --- datacrunch/instances/instances.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index 8a33b83..f361d61 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -65,7 +65,7 @@ def __str__(self) -> str: """Returns a JSON string representation of the instance. Returns: - str: JSON string containing all instance properties. + JSON string containing all instance properties. """ return stringify_class_object_properties(self) @@ -93,7 +93,7 @@ def get(self, status: Optional[str] = None) -> List[Instance]: non-deleted instances. Returns: - List[Instance]: List of instance objects matching the criteria. + List of instance objects matching the criteria. """ instances_dict = self._http_client.get( INSTANCES_ENDPOINT, params={'status': status}).json() @@ -106,7 +106,7 @@ def get_by_id(self, id: str) -> Instance: id: Unique identifier of the instance to retrieve. Returns: - Instance: Instance object with the specified ID. + Instance object with the specified ID. Raises: HTTPError: If the instance is not found or other API error occurs. @@ -149,7 +149,7 @@ def create(self, coupon: Optional coupon code for discounts. Returns: - Instance: The newly created instance object. + The newly created instance object. Raises: HTTPError: If instance creation fails or other API error occurs. @@ -207,7 +207,7 @@ def is_available(self, instance_type: str, is_spot: bool = False, location_code: location_code: Optional datacenter location code. Returns: - bool: True if the instance type is available, False otherwise. + True if the instance type is available, False otherwise. """ is_spot = str(is_spot).lower() query_params = {'isSpot': is_spot, 'location_code': location_code} @@ -222,7 +222,7 @@ def get_availabilities(self, is_spot: Optional[bool] = None, location_code: Opti location_code: Optional datacenter location code to filter by. Returns: - List[Dict]: List of available instance types and their details. + List of available instance types and their details. """ is_spot = str(is_spot).lower() if is_spot is not None else None query_params = {'isSpot': is_spot, 'locationCode': location_code} From 7f4843b2b447bbcad5d948a9a9f21f7c6ff8776e Mon Sep 17 00:00:00 2001 From: Tamir Date: Mon, 28 Apr 2025 08:51:00 +0300 Subject: [PATCH 07/10] upgrade actions ubuntu version --- .github/workflows/code_style.yml | 2 +- .github/workflows/unit_tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml index f0995ff..2d36207 100644 --- a/.github/workflows/code_style.yml +++ b/.github/workflows/code_style.yml @@ -8,7 +8,7 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index cdaf609..850e765 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -8,7 +8,7 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] From 5382d27484d76c8b9a8f4f1521b5255172716761 Mon Sep 17 00:00:00 2001 From: Tamir Date: Mon, 28 Apr 2025 09:57:22 +0300 Subject: [PATCH 08/10] removed custom instance str func, wait for provisioning status on create --- datacrunch/instances/instances.py | 39 ++++++++++++++++++------------ examples/simple_create_instance.py | 12 ++++++++- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index f361d61..dd3c7fe 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -1,8 +1,8 @@ +import time from typing import List, Union, Optional, Dict, Literal from dataclasses import dataclass from dataclasses_json import dataclass_json -from datacrunch.helpers import stringify_class_object_properties -from datacrunch.constants import Locations +from datacrunch.constants import Locations, InstanceStatus INSTANCES_ENDPOINT = '/instances' @@ -21,7 +21,6 @@ class Instance: price_per_hour: Cost per hour of running the instance. hostname: Network hostname of the instance. description: Human-readable description of the instance. - ip: IP address assigned to the instance. status: Current operational status of the instance. created_at: Timestamp of instance creation. ssh_key_ids: List of SSH key IDs associated with the instance. @@ -29,8 +28,9 @@ class Instance: gpu: GPU configuration details. memory: Memory configuration details. storage: Storage configuration details. - os_volume_id: ID of the operating system volume. gpu_memory: GPU memory configuration details. + ip: IP address assigned to the instance. + os_volume_id: ID of the operating system volume. location: Datacenter location code (default: Locations.FIN_01). image: Image ID or type used for the instance. startup_script_id: ID of the startup script to run. @@ -44,7 +44,6 @@ class Instance: price_per_hour: float hostname: str description: str - ip: str status: str created_at: str ssh_key_ids: List[str] @@ -52,8 +51,11 @@ class Instance: gpu: dict memory: dict storage: dict - os_volume_id: str gpu_memory: dict + # Can be None if instance is still not provisioned + ip: Optional[str] = None + # Can be None if instance is still not provisioned + os_volume_id: Optional[str] = None location: str = Locations.FIN_01 image: Optional[str] = None startup_script_id: Optional[str] = None @@ -61,14 +63,6 @@ class Instance: contract: Optional[Contract] = None pricing: Optional[Pricing] = None - def __str__(self) -> str: - """Returns a JSON string representation of the instance. - - Returns: - JSON string containing all instance properties. - """ - return stringify_class_object_properties(self) - class InstancesService: """Service for managing cloud instances through the API. @@ -173,7 +167,22 @@ def create(self, if pricing: payload['pricing'] = pricing id = self._http_client.post(INSTANCES_ENDPOINT, json=payload).text - return self.get_by_id(id) + + # Wait for instance to enter provisioning state with timeout + MAX_WAIT_TIME = 60 # Maximum wait time in seconds + POLL_INTERVAL = 0.5 # Time between status checks + + start_time = time.time() + while True: + instance = self.get_by_id(id) + if instance.status == InstanceStatus.PROVISIONING: + return instance + + if time.time() - start_time > MAX_WAIT_TIME: + raise TimeoutError( + f"Instance {id} did not enter provisioning state within {MAX_WAIT_TIME} seconds") + + time.sleep(POLL_INTERVAL) def action(self, id_list: Union[List[str], str], action: str, volume_ids: Optional[List[str]] = None) -> None: """Performs an action on one or more instances. diff --git a/examples/simple_create_instance.py b/examples/simple_create_instance.py index 6f4cb40..a52b18d 100644 --- a/examples/simple_create_instance.py +++ b/examples/simple_create_instance.py @@ -1,5 +1,7 @@ +import time import os from datacrunch import DataCrunchClient +from datacrunch.constants import Locations, InstanceStatus # Get client secret and id from environment variables DATACRUNCH_CLIENT_ID = os.environ.get('DATACRUNCH_CLIENT_ID') @@ -14,11 +16,19 @@ # Create a new instance instance = datacrunch.instances.create(instance_type='1V100.6V', - image='ubuntu-24.04-cuda-12.8-open-docker', + image='ubuntu-22.04-cuda-12.0-docker', + location=Locations.FIN_01, ssh_key_ids=ssh_keys_ids, hostname='example', description='example instance') +# Wait for instance to enter running state +while instance.status != InstanceStatus.RUNNING: + time.sleep(0.5) + instance = datacrunch.instances.get_by_id(instance.id) + +print(instance) + # Delete instance datacrunch.instances.action( instance.id, datacrunch.constants.instance_actions.DELETE) From 8f741d663a3fcd5a4b3714f344bcca868c1f5c94 Mon Sep 17 00:00:00 2001 From: Tamir Date: Mon, 28 Apr 2025 10:39:32 +0300 Subject: [PATCH 09/10] updated examples image --- examples/advanced_create_instance.py | 4 ++-- examples/instance_actions.py | 2 +- examples/instances_and_volumes.py | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/advanced_create_instance.py b/examples/advanced_create_instance.py index 6b46b36..b7a3af0 100644 --- a/examples/advanced_create_instance.py +++ b/examples/advanced_create_instance.py @@ -52,7 +52,7 @@ if price_per_hour * DURATION < balance.amount: # Deploy a new 8V instance instance = datacrunch.instances.create(instance_type=INSTANCE_TYPE_8V, - image='ubuntu-24.04-cuda-12.8-open-docker', + image='ubuntu-22.04-cuda-12.0-docker', ssh_key_ids=ssh_keys_ids, hostname='example', description='large instance', @@ -63,7 +63,7 @@ else: # Deploy a new 4V instance instance = datacrunch.instances.create(instance_type=INSTANCE_TYPE_4V, - image='ubuntu-24.04-cuda-12.8-open-docker', + image='ubuntu-22.04-cuda-12.0-docker', ssh_key_ids=ssh_keys_ids, hostname='example', description='medium instance') diff --git a/examples/instance_actions.py b/examples/instance_actions.py index 4021405..184cc99 100644 --- a/examples/instance_actions.py +++ b/examples/instance_actions.py @@ -17,7 +17,7 @@ # Create a new 1V100.6V instance instance = datacrunch.instances.create(instance_type='1V100.6V', - image='ubuntu-24.04-cuda-12.8-open-docker', + image='ubuntu-22.04-cuda-12.0-docker', ssh_key_ids=ssh_keys_ids, hostname='example', description='example instance') diff --git a/examples/instances_and_volumes.py b/examples/instances_and_volumes.py index db83d29..44b65d1 100644 --- a/examples/instances_and_volumes.py +++ b/examples/instances_and_volumes.py @@ -21,7 +21,7 @@ # Create instance with extra attached volumes instance_with_extra_volumes = datacrunch.instances.create(instance_type='1V100.6V', - image='ubuntu-24.04-cuda-12.8-open-docker', + image='ubuntu-22.04-cuda-12.0-docker', ssh_key_ids=ssh_keys, hostname='example', description='example instance', @@ -34,7 +34,7 @@ # Create instance with custom OS volume size and name instance_with_custom_os_volume = datacrunch.instances.create(instance_type='1V100.6V', - image='ubuntu-24.04-cuda-12.8-open-docker', + image='ubuntu-22.04-cuda-12.0-docker', ssh_key_ids=ssh_keys, hostname='example', description='example instance', @@ -59,7 +59,6 @@ action=datacrunch.constants.instance_actions.DELETE, volume_ids=[]) - # Delete instance and one of it's volumes (will delete one volume, detach the rest) datacrunch.instances.action(instance_id=EXAMPLE_INSTANCE_ID, action=datacrunch.constants.instance_actions.DELETE, From fea0768a963560ca3201f6465490296fc319011d Mon Sep 17 00:00:00 2001 From: Tamir Date: Mon, 28 Apr 2025 10:48:05 +0300 Subject: [PATCH 10/10] wait for status different than ordered instead of equals provisioning --- datacrunch/instances/instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index dd3c7fe..ce67c43 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -175,7 +175,7 @@ def create(self, start_time = time.time() while True: instance = self.get_by_id(id) - if instance.status == InstanceStatus.PROVISIONING: + if instance.status != InstanceStatus.ORDERED: return instance if time.time() - start_time > MAX_WAIT_TIME: