verda 0.1.0__py3-none-any.whl → 1.17.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,260 @@
1
+ import itertools
2
+ import time
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+ from dataclasses_json import dataclass_json
7
+
8
+ from verda.constants import InstanceStatus, Locations
9
+
10
+ INSTANCES_ENDPOINT = '/instances'
11
+
12
+ Contract = Literal['LONG_TERM', 'PAY_AS_YOU_GO', 'SPOT']
13
+ Pricing = Literal['DYNAMIC_PRICE', 'FIXED_PRICE']
14
+
15
+
16
+ @dataclass_json
17
+ @dataclass
18
+ class Instance:
19
+ """Represents a cloud instance with its configuration and state.
20
+
21
+ Attributes:
22
+ id: Unique identifier for the instance.
23
+ instance_type: Type of the instance (e.g., '8V100.48V').
24
+ price_per_hour: Cost per hour of running the instance.
25
+ hostname: Network hostname of the instance.
26
+ description: Human-readable description of the instance.
27
+ status: Current operational status of the instance.
28
+ created_at: Timestamp of instance creation.
29
+ ssh_key_ids: List of SSH key IDs associated with the instance.
30
+ cpu: CPU configuration details.
31
+ gpu: GPU configuration details.
32
+ memory: Memory configuration details.
33
+ storage: Storage configuration details.
34
+ gpu_memory: GPU memory configuration details.
35
+ ip: IP address assigned to the instance.
36
+ os_volume_id: ID of the operating system volume.
37
+ location: Datacenter location code (default: Locations.FIN_03).
38
+ image: Image ID or type used for the instance.
39
+ startup_script_id: ID of the startup script to run.
40
+ is_spot: Whether the instance is a spot instance.
41
+ contract: Contract type for the instance. (e.g. 'LONG_TERM', 'PAY_AS_YOU_GO', 'SPOT')
42
+ pricing: Pricing model for the instance. (e.g. 'DYNAMIC_PRICE', 'FIXED_PRICE')
43
+ """
44
+
45
+ id: str
46
+ instance_type: str
47
+ price_per_hour: float
48
+ hostname: str
49
+ description: str
50
+ status: str
51
+ created_at: str
52
+ ssh_key_ids: list[str]
53
+ cpu: dict
54
+ gpu: dict
55
+ memory: dict
56
+ storage: dict
57
+ gpu_memory: dict
58
+ # Can be None if instance is still not provisioned
59
+ ip: str | None = None
60
+ # Can be None if instance is still not provisioned
61
+ os_volume_id: str | None = None
62
+ location: str = Locations.FIN_03
63
+ image: str | None = None
64
+ startup_script_id: str | None = None
65
+ is_spot: bool = False
66
+ contract: Contract | None = None
67
+ pricing: Pricing | None = None
68
+
69
+
70
+ class InstancesService:
71
+ """Service for managing cloud instances through the API.
72
+
73
+ This service provides methods to create, retrieve, and manage cloud instances.
74
+ """
75
+
76
+ def __init__(self, http_client) -> None:
77
+ """Initializes the InstancesService with an HTTP client.
78
+
79
+ Args:
80
+ http_client: HTTP client for making API requests.
81
+ """
82
+ self._http_client = http_client
83
+
84
+ def get(self, status: str | None = None) -> list[Instance]:
85
+ """Retrieves all non-deleted instances or instances with specific status.
86
+
87
+ Args:
88
+ status: Optional status filter for instances. If None, returns all
89
+ non-deleted instances.
90
+
91
+ Returns:
92
+ List of instance objects matching the criteria.
93
+ """
94
+ instances_dict = self._http_client.get(INSTANCES_ENDPOINT, params={'status': status}).json()
95
+ return [
96
+ Instance.from_dict(instance_dict, infer_missing=True)
97
+ for instance_dict in instances_dict
98
+ ]
99
+
100
+ def get_by_id(self, id: str) -> Instance:
101
+ """Retrieves a specific instance by its ID.
102
+
103
+ Args:
104
+ id: Unique identifier of the instance to retrieve.
105
+
106
+ Returns:
107
+ Instance object with the specified ID.
108
+
109
+ Raises:
110
+ HTTPError: If the instance is not found or other API error occurs.
111
+ """
112
+ instance_dict = self._http_client.get(INSTANCES_ENDPOINT + f'/{id}').json()
113
+ return Instance.from_dict(instance_dict, infer_missing=True)
114
+
115
+ def create(
116
+ self,
117
+ instance_type: str,
118
+ image: str,
119
+ hostname: str,
120
+ description: str,
121
+ ssh_key_ids: list = [],
122
+ location: str = Locations.FIN_03,
123
+ startup_script_id: str | None = None,
124
+ volumes: list[dict] | None = None,
125
+ existing_volumes: list[str] | None = None,
126
+ os_volume: dict | None = None,
127
+ is_spot: bool = False,
128
+ contract: Contract | None = None,
129
+ pricing: Pricing | None = None,
130
+ coupon: str | None = None,
131
+ *,
132
+ max_wait_time: float = 180,
133
+ initial_interval: float = 0.5,
134
+ max_interval: float = 5,
135
+ backoff_coefficient: float = 2.0,
136
+ ) -> Instance:
137
+ """Creates and deploys a new cloud instance.
138
+
139
+ Args:
140
+ instance_type: Type of instance to create (e.g., '8V100.48V').
141
+ image: Image type or existing OS volume ID for the instance.
142
+ hostname: Network hostname for the instance.
143
+ description: Human-readable description of the instance.
144
+ ssh_key_ids: List of SSH key IDs to associate with the instance.
145
+ location: Datacenter location code (default: Locations.FIN_03).
146
+ startup_script_id: Optional ID of startup script to run.
147
+ volumes: Optional list of volume configurations to create.
148
+ existing_volumes: Optional list of existing volume IDs to attach.
149
+ os_volume: Optional OS volume configuration details.
150
+ is_spot: Whether to create a spot instance.
151
+ contract: Optional contract type for the instance.
152
+ pricing: Optional pricing model for the instance.
153
+ coupon: Optional coupon code for discounts.
154
+ max_wait_time: Maximum total wait for the instance to start provisioning, in seconds (default: 180)
155
+ initial_interval: Initial interval, in seconds (default: 0.5)
156
+ max_interval: The longest single delay allowed between retries, in seconds (default: 5)
157
+ backoff_coefficient: Coefficient to calculate the next retry interval (default 2.0)
158
+
159
+ Returns:
160
+ The newly created instance object.
161
+
162
+ Raises:
163
+ HTTPError: If instance creation fails or other API error occurs.
164
+ """
165
+ payload = {
166
+ 'instance_type': instance_type,
167
+ 'image': image,
168
+ 'ssh_key_ids': ssh_key_ids,
169
+ 'startup_script_id': startup_script_id,
170
+ 'hostname': hostname,
171
+ 'description': description,
172
+ 'location_code': location,
173
+ 'os_volume': os_volume,
174
+ 'volumes': volumes,
175
+ 'existing_volumes': existing_volumes,
176
+ 'is_spot': is_spot,
177
+ 'coupon': coupon,
178
+ }
179
+ if contract:
180
+ payload['contract'] = contract
181
+ if pricing:
182
+ payload['pricing'] = pricing
183
+ id = self._http_client.post(INSTANCES_ENDPOINT, json=payload).text
184
+
185
+ # Wait for instance to enter provisioning state with timeout
186
+ deadline = time.monotonic() + max_wait_time
187
+ for i in itertools.count():
188
+ instance = self.get_by_id(id)
189
+ if instance.status != InstanceStatus.ORDERED:
190
+ return instance
191
+
192
+ now = time.monotonic()
193
+ if now >= deadline:
194
+ raise TimeoutError(
195
+ f'Instance {id} did not enter provisioning state within {max_wait_time:.1f} seconds'
196
+ )
197
+
198
+ interval = min(initial_interval * backoff_coefficient**i, max_interval, deadline - now)
199
+ time.sleep(interval)
200
+
201
+ def action(
202
+ self,
203
+ id_list: list[str] | str,
204
+ action: str,
205
+ volume_ids: list[str] | None = None,
206
+ ) -> None:
207
+ """Performs an action on one or more instances.
208
+
209
+ Args:
210
+ id_list: Single instance ID or list of instance IDs to act upon.
211
+ action: Action to perform on the instances.
212
+ volume_ids: Optional list of volume IDs to delete.
213
+
214
+ Raises:
215
+ HTTPError: If the action fails or other API error occurs.
216
+ """
217
+ if type(id_list) is str:
218
+ id_list = [id_list]
219
+
220
+ payload = {'id': id_list, 'action': action, 'volume_ids': volume_ids}
221
+
222
+ self._http_client.put(INSTANCES_ENDPOINT, json=payload)
223
+ return
224
+
225
+ def is_available(
226
+ self,
227
+ instance_type: str,
228
+ is_spot: bool = False,
229
+ location_code: str | None = None,
230
+ ) -> bool:
231
+ """Checks if a specific instance type is available for deployment.
232
+
233
+ Args:
234
+ instance_type: Type of instance to check availability for.
235
+ is_spot: Whether to check spot instance availability.
236
+ location_code: Optional datacenter location code.
237
+
238
+ Returns:
239
+ True if the instance type is available, False otherwise.
240
+ """
241
+ is_spot = str(is_spot).lower()
242
+ query_params = {'isSpot': is_spot, 'location_code': location_code}
243
+ url = f'/instance-availability/{instance_type}'
244
+ return self._http_client.get(url, query_params).json()
245
+
246
+ def get_availabilities(
247
+ self, is_spot: bool | None = None, location_code: str | None = None
248
+ ) -> list[dict]:
249
+ """Retrieves a list of available instance types across locations.
250
+
251
+ Args:
252
+ is_spot: Optional flag to filter spot instance availability.
253
+ location_code: Optional datacenter location code to filter by.
254
+
255
+ Returns:
256
+ List of available instance types and their details.
257
+ """
258
+ is_spot = str(is_spot).lower() if is_spot is not None else None
259
+ query_params = {'isSpot': is_spot, 'locationCode': location_code}
260
+ return self._http_client.get('/instance-availability', params=query_params).json()
File without changes
@@ -0,0 +1,13 @@
1
+ LOCATIONS_ENDPOINT = '/locations'
2
+
3
+
4
+ class LocationsService:
5
+ """A service for interacting with the locations endpoint."""
6
+
7
+ def __init__(self, http_client) -> None:
8
+ self._http_client = http_client
9
+
10
+ def get(self) -> list[dict]:
11
+ """Get all locations."""
12
+ locations = self._http_client.get(LOCATIONS_ENDPOINT).json()
13
+ return locations
File without changes
@@ -0,0 +1,109 @@
1
+ SSHKEYS_ENDPOINT = '/sshkeys'
2
+
3
+
4
+ class SSHKey:
5
+ """An SSH key model class."""
6
+
7
+ def __init__(self, id: str, name: str, public_key: str) -> None:
8
+ """Initialize a new SSH key object.
9
+
10
+ :param id: SSH key id
11
+ :type id: str
12
+ :param name: SSH key name
13
+ :type name: str
14
+ :param public_key: SSH key public key
15
+ :type public_key: str
16
+ """
17
+ self._id = id
18
+ self._name = name
19
+ self._public_key = public_key
20
+
21
+ @property
22
+ def id(self) -> str:
23
+ """Get the SSH key id.
24
+
25
+ :return: SSH key id
26
+ :rtype: str
27
+ """
28
+ return self._id
29
+
30
+ @property
31
+ def name(self) -> str:
32
+ """Get the SSH key name.
33
+
34
+ :return: SSH key name
35
+ :rtype: str
36
+ """
37
+ return self._name
38
+
39
+ @property
40
+ def public_key(self) -> str:
41
+ """Get the SSH key public key value.
42
+
43
+ :return: public SSH key
44
+ :rtype: str
45
+ """
46
+ return self._public_key
47
+
48
+
49
+ class SSHKeysService:
50
+ """A service for interacting with the SSH keys endpoint."""
51
+
52
+ def __init__(self, http_client) -> None:
53
+ self._http_client = http_client
54
+
55
+ def get(self) -> list[SSHKey]:
56
+ """Get all of the client's SSH keys.
57
+
58
+ :return: list of SSH keys objects
59
+ :rtype: list[SSHKey]
60
+ """
61
+ keys = self._http_client.get(SSHKEYS_ENDPOINT).json()
62
+ keys_object_list = [SSHKey(key['id'], key['name'], key['key']) for key in keys]
63
+
64
+ return keys_object_list
65
+
66
+ def get_by_id(self, id: str) -> SSHKey:
67
+ """Get a specific SSH key by id.
68
+
69
+ :param id: SSH key id
70
+ :type id: str
71
+ :return: SSHKey object
72
+ :rtype: SSHKey
73
+ """
74
+ key_dict = self._http_client.get(SSHKEYS_ENDPOINT + f'/{id}').json()[0]
75
+ key_object = SSHKey(key_dict['id'], key_dict['name'], key_dict['key'])
76
+ return key_object
77
+
78
+ def delete(self, id_list: list[str]) -> None:
79
+ """Delete multiple SSH keys by id.
80
+
81
+ :param id_list: list of SSH keys ids
82
+ :type id_list: list[str]
83
+ """
84
+ payload = {'keys': id_list}
85
+ self._http_client.delete(SSHKEYS_ENDPOINT, json=payload)
86
+ return
87
+
88
+ def delete_by_id(self, id: str) -> None:
89
+ """Delete a single SSH key by id.
90
+
91
+ :param id: SSH key id
92
+ :type id: str
93
+ """
94
+ self._http_client.delete(SSHKEYS_ENDPOINT + f'/{id}')
95
+ return
96
+
97
+ def create(self, name: str, key: str) -> SSHKey:
98
+ """Create a new SSH key.
99
+
100
+ :param name: SSH key name
101
+ :type name: str
102
+ :param key: public SSH key value
103
+ :type key: str
104
+ :return: new SSH key object
105
+ :rtype: SSHKey
106
+ """
107
+ payload = {'name': name, 'key': key}
108
+ id = self._http_client.post(SSHKEYS_ENDPOINT, json=payload).text
109
+ return SSHKey(id, name, key)
File without changes
@@ -0,0 +1,110 @@
1
+ STARTUP_SCRIPTS_ENDPOINT = '/scripts'
2
+
3
+
4
+ class StartupScript:
5
+ """A startup script model class."""
6
+
7
+ def __init__(self, id: str, name: str, script: str) -> None:
8
+ """Initialize a new startup script object.
9
+
10
+ :param id: startup script id
11
+ :type id: str
12
+ :param name: startup script name
13
+ :type name: str
14
+ :param script: the actual script
15
+ :type script: str
16
+ """
17
+ self._id = id
18
+ self._name = name
19
+ self._script = script
20
+
21
+ @property
22
+ def id(self) -> str:
23
+ """Get the startup script id.
24
+
25
+ :return: startup script id
26
+ :rtype: str
27
+ """
28
+ return self._id
29
+
30
+ @property
31
+ def name(self) -> str:
32
+ """Get the startup script name.
33
+
34
+ :return: startup script name
35
+ :rtype: str
36
+ """
37
+ return self._name
38
+
39
+ @property
40
+ def script(self) -> str:
41
+ """Get the actual startup script code.
42
+
43
+ :return: startup script text
44
+ :rtype: str
45
+ """
46
+ return self._script
47
+
48
+
49
+ class StartupScriptsService:
50
+ """A service for interacting with the startup scripts endpoint."""
51
+
52
+ def __init__(self, http_client) -> None:
53
+ self._http_client = http_client
54
+
55
+ def get(self) -> list[StartupScript]:
56
+ """Get all of the client's startup scripts.
57
+
58
+ :return: list of startup script objects
59
+ :rtype: list[StartupScript]
60
+ """
61
+ scripts = self._http_client.get(STARTUP_SCRIPTS_ENDPOINT).json()
62
+ scripts_objects = [
63
+ StartupScript(script['id'], script['name'], script['script']) for script in scripts
64
+ ]
65
+ return scripts_objects
66
+
67
+ def get_by_id(self, id) -> StartupScript:
68
+ """Get a specific startup script by id.
69
+
70
+ :param id: startup script id
71
+ :type id: str
72
+ :return: startup script object
73
+ :rtype: StartupScript
74
+ """
75
+ script = self._http_client.get(STARTUP_SCRIPTS_ENDPOINT + f'/{id}').json()[0]
76
+
77
+ return StartupScript(script['id'], script['name'], script['script'])
78
+
79
+ def delete(self, id_list: list[str]) -> None:
80
+ """Delete multiple startup scripts by id.
81
+
82
+ :param id_list: list of startup scripts ids
83
+ :type id_list: list[str]
84
+ """
85
+ payload = {'scripts': id_list}
86
+ self._http_client.delete(STARTUP_SCRIPTS_ENDPOINT, json=payload)
87
+ return
88
+
89
+ def delete_by_id(self, id: str) -> None:
90
+ """Delete a single startup script by id.
91
+
92
+ :param id: startup script id
93
+ :type id: str
94
+ """
95
+ self._http_client.delete(STARTUP_SCRIPTS_ENDPOINT + f'/{id}')
96
+ return
97
+
98
+ def create(self, name: str, script: str) -> StartupScript:
99
+ """Create a new startup script.
100
+
101
+ :param name: startup script name
102
+ :type name: str
103
+ :param script: startup script value
104
+ :type script: str
105
+ :return: the new startup script's id
106
+ :rtype: str
107
+ """
108
+ payload = {'name': name, 'script': script}
109
+ id = self._http_client.post(STARTUP_SCRIPTS_ENDPOINT, json=payload).text
110
+ return StartupScript(id, name, script)
verda/verda.py ADDED
@@ -0,0 +1,83 @@
1
+ from verda._version import __version__
2
+ from verda.authentication.authentication import AuthenticationService
3
+ from verda.balance.balance import BalanceService
4
+ from verda.constants import Constants
5
+ from verda.containers.containers import ContainersService
6
+ from verda.http_client.http_client import HTTPClient
7
+ from verda.images.images import ImagesService
8
+ from verda.instance_types.instance_types import InstanceTypesService
9
+ from verda.instances.instances import InstancesService
10
+ from verda.locations.locations import LocationsService
11
+ from verda.ssh_keys.ssh_keys import SSHKeysService
12
+ from verda.startup_scripts.startup_scripts import StartupScriptsService
13
+ from verda.volume_types.volume_types import VolumeTypesService
14
+ from verda.volumes.volumes import VolumesService
15
+
16
+
17
+ class VerdaClient:
18
+ """Client for interacting with Verda public API."""
19
+
20
+ def __init__(
21
+ self,
22
+ client_id: str,
23
+ client_secret: str,
24
+ base_url: str = 'https://api.verda.com/v1',
25
+ inference_key: str | None = None,
26
+ ) -> None:
27
+ """Verda client.
28
+
29
+ :param client_id: client id
30
+ :type client_id: str
31
+ :param client_secret: client secret
32
+ :type client_secret: str
33
+ :param base_url: base url for all the endpoints, optional, defaults to "https://api.verda.com/v1"
34
+ :type base_url: str, optional
35
+ :param inference_key: inference key, optional
36
+ :type inference_key: str, optional
37
+ """
38
+ # Validate that client_id and client_secret are not empty
39
+ if not client_id or not client_secret:
40
+ raise ValueError('client_id and client_secret must be provided')
41
+
42
+ # Constants
43
+ self.constants: Constants = Constants(base_url, __version__)
44
+ """Constants"""
45
+
46
+ # Services
47
+ self._authentication: AuthenticationService = AuthenticationService(
48
+ client_id, client_secret, self.constants.base_url
49
+ )
50
+ self._http_client: HTTPClient = HTTPClient(self._authentication, self.constants.base_url)
51
+
52
+ self.balance: BalanceService = BalanceService(self._http_client)
53
+ """Balance service. Get client balance"""
54
+
55
+ self.images: ImagesService = ImagesService(self._http_client)
56
+ """Image service"""
57
+
58
+ self.instance_types: InstanceTypesService = InstanceTypesService(self._http_client)
59
+ """Instance type service"""
60
+
61
+ self.instances: InstancesService = InstancesService(self._http_client)
62
+ """Instances service. Deploy, delete, hibernate (etc) instances"""
63
+
64
+ self.ssh_keys: SSHKeysService = SSHKeysService(self._http_client)
65
+ """SSH keys service"""
66
+
67
+ self.startup_scripts: StartupScriptsService = StartupScriptsService(self._http_client)
68
+ """Startup Scripts service"""
69
+
70
+ self.volume_types: VolumeTypesService = VolumeTypesService(self._http_client)
71
+ """Volume type service"""
72
+
73
+ self.volumes: VolumesService = VolumesService(self._http_client)
74
+ """Volume service. Create, attach, detach, get, rename, delete volumes"""
75
+
76
+ self.locations: LocationsService = LocationsService(self._http_client)
77
+ """Locations service. Get locations"""
78
+
79
+ self.containers: ContainersService = ContainersService(self._http_client, inference_key)
80
+ """Containers service. Deploy, manage, and monitor container deployments"""
81
+
82
+
83
+ __all__ = ['VerdaClient']
File without changes
@@ -0,0 +1,66 @@
1
+ VOLUME_TYPES_ENDPOINT = '/volume-types'
2
+
3
+
4
+ class VolumeType:
5
+ """Volume type."""
6
+
7
+ def __init__(self, type: str, price_per_month_per_gb: float) -> None:
8
+ """Initialize a volume type object.
9
+
10
+ :param type: volume type name
11
+ :type type: str
12
+ :param price_per_month_per_gb: price per month per gb of storage
13
+ :type price_per_month_per_gb: float
14
+ """
15
+ self._type = type
16
+ self._price_per_month_per_gb = price_per_month_per_gb
17
+
18
+ @property
19
+ def type(self) -> str:
20
+ """Get the volume type.
21
+
22
+ :return: volume type
23
+ :rtype: str
24
+ """
25
+ return self._type
26
+
27
+ @property
28
+ def price_per_month_per_gb(self) -> str:
29
+ """Get the volume price_per_month_per_gb.
30
+
31
+ :return: volume price_per_month_per_gb
32
+ :rtype: str
33
+ """
34
+ return self._price_per_month_per_gb
35
+
36
+ def __str__(self) -> str:
37
+ """Prints the volume type.
38
+
39
+ :return: volume type string representation
40
+ :rtype: str
41
+ """
42
+ return f'type: {self._type}\nprice_per_month_per_gb: ${self._price_per_month_per_gb}'
43
+
44
+
45
+ class VolumeTypesService:
46
+ """A service for interacting with the volume-types endpoint."""
47
+
48
+ def __init__(self, http_client) -> None:
49
+ self._http_client = http_client
50
+
51
+ def get(self) -> list[VolumeType]:
52
+ """Get all volume types.
53
+
54
+ :return: list of volume type objects
55
+ :rtype: list[VolumesType]
56
+ """
57
+ volume_types = self._http_client.get(VOLUME_TYPES_ENDPOINT).json()
58
+ volume_type_objects = [
59
+ VolumeType(
60
+ type=volume_type['type'],
61
+ price_per_month_per_gb=volume_type['price']['price_per_month_per_gb'],
62
+ )
63
+ for volume_type in volume_types
64
+ ]
65
+
66
+ return volume_type_objects
File without changes