verda 0.1.0__py3-none-any.whl → 0.2.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.
verda/datacrunch.py ADDED
@@ -0,0 +1,43 @@
1
+ # Frozen, minimal compatibility layer for old DataCrunch API
2
+
3
+ from verda import DataCrunchClient
4
+ from verda._version import __version__
5
+ from verda.authentication.authentication import AuthenticationService
6
+ from verda.balance.balance import BalanceService
7
+ from verda.constants import Constants
8
+ from verda.containers.containers import ContainersService
9
+ from verda.http_client.http_client import HTTPClient
10
+ from verda.images.images import ImagesService
11
+ from verda.instance_types.instance_types import InstanceTypesService
12
+ from verda.instances.instances import InstancesService
13
+ from verda.locations.locations import LocationsService
14
+ from verda.ssh_keys.ssh_keys import SSHKeysService
15
+ from verda.startup_scripts.startup_scripts import StartupScriptsService
16
+ from verda.volume_types.volume_types import VolumeTypesService
17
+ from verda.volumes.volumes import VolumesService
18
+
19
+ __all__ = [
20
+ 'AuthenticationService',
21
+ 'BalanceService',
22
+ 'Constants',
23
+ 'ContainersService',
24
+ 'DataCrunchClient',
25
+ 'HTTPClient',
26
+ 'ImagesService',
27
+ 'InstanceTypesService',
28
+ 'InstancesService',
29
+ 'LocationsService',
30
+ 'SSHKeysService',
31
+ 'StartupScriptsService',
32
+ 'VolumeTypesService',
33
+ 'VolumesService',
34
+ '__version__',
35
+ ]
36
+
37
+ import warnings
38
+
39
+ warnings.warn(
40
+ 'datacrunch.datacrunch is deprecated; use `from verda` instead.',
41
+ DeprecationWarning,
42
+ stacklevel=2,
43
+ )
verda/exceptions.py ADDED
@@ -0,0 +1,30 @@
1
+ class APIException(Exception):
2
+ """This exception is raised if there was an error from verda's API.
3
+
4
+ Could be an invalid input, token etc.
5
+
6
+ Raised when an API HTTP call response has a status code >= 400
7
+ """
8
+
9
+ def __init__(self, code: str, message: str) -> None:
10
+ """API Exception.
11
+
12
+ :param code: error code
13
+ :type code: str
14
+ :param message: error message
15
+ :type message: str
16
+ """
17
+ self.code = code
18
+ """Error code. should be available in VerdaClient.error_codes"""
19
+
20
+ self.message = message
21
+ """Error message
22
+ """
23
+
24
+ def __str__(self) -> str:
25
+ msg = ''
26
+ if self.code:
27
+ msg = f'error code: {self.code}\n'
28
+
29
+ msg += f'message: {self.message}'
30
+ return msg
verda/helpers.py ADDED
@@ -0,0 +1,17 @@
1
+ import json
2
+
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 = {
13
+ property: getattr(class_object, property, '')
14
+ for property in class_object.__dir__() # noqa: A001
15
+ if property[:1] != '_' and type(getattr(class_object, property, '')).__name__ != 'method'
16
+ }
17
+ return json.dumps(class_properties, indent=2)
File without changes
@@ -0,0 +1,246 @@
1
+ import json
2
+
3
+ import requests
4
+
5
+ from verda._version import __version__
6
+ from verda.exceptions import APIException
7
+
8
+
9
+ def handle_error(response: requests.Response) -> None:
10
+ """Checks for the response status code and raises an exception if it's 400 or higher.
11
+
12
+ :param response: the API call response
13
+ :raises APIException: an api exception with message and error type code
14
+ """
15
+ if not response.ok:
16
+ data = json.loads(response.text)
17
+ code = data['code'] if 'code' in data else None
18
+ message = data['message'] if 'message' in data else None
19
+ raise APIException(code, message)
20
+
21
+
22
+ class HTTPClient:
23
+ """An http client, a wrapper for the requests library.
24
+
25
+ For each request, it adds the authentication header with an access token.
26
+ If the access token is expired it refreshes it before calling the specified API endpoint.
27
+ Also checks the response status code and raises an exception if needed.
28
+ """
29
+
30
+ def __init__(self, auth_service, base_url: str) -> None:
31
+ self._version = __version__
32
+ self._base_url = base_url
33
+ self._auth_service = auth_service
34
+ self._auth_service.authenticate()
35
+
36
+ def post(
37
+ self, url: str, json: dict | None = None, params: dict | None = None, **kwargs
38
+ ) -> requests.Response:
39
+ """Sends a POST request.
40
+
41
+ A wrapper for the requests.post method.
42
+
43
+ Builds the url, uses custom headers, refresh tokens if needed.
44
+
45
+ :param url: relative url of the API endpoint
46
+ :type url: str
47
+ :param json: A JSON serializable Python object to send in the body of the Request, defaults to None
48
+ :type json: dict, optional
49
+ :param params: Dictionary of querystring data to attach to the Request, defaults to None
50
+ :type params: dict, optional
51
+
52
+ :raises APIException: an api exception with message and error type code
53
+
54
+ :return: Response object
55
+ :rtype: requests.Response
56
+ """
57
+ self._refresh_token_if_expired()
58
+
59
+ url = self._add_base_url(url)
60
+ headers = self._generate_headers()
61
+
62
+ response = requests.post(url, json=json, headers=headers, params=params, **kwargs)
63
+ handle_error(response)
64
+
65
+ return response
66
+
67
+ def put(
68
+ self, url: str, json: dict | None = None, params: dict | None = None, **kwargs
69
+ ) -> requests.Response:
70
+ """Sends a PUT request.
71
+
72
+ A wrapper for the requests.put method.
73
+
74
+ Builds the url, uses custom headers, refresh tokens if needed.
75
+
76
+ :param url: relative url of the API endpoint
77
+ :type url: str
78
+ :param json: A JSON serializable Python object to send in the body of the Request, defaults to None
79
+ :type json: dict, optional
80
+ :param params: Dictionary of querystring data to attach to the Request, defaults to None
81
+ :type params: dict, optional
82
+
83
+ :raises APIException: an api exception with message and error type code
84
+
85
+ :return: Response object
86
+ :rtype: requests.Response
87
+ """
88
+ self._refresh_token_if_expired()
89
+
90
+ url = self._add_base_url(url)
91
+ headers = self._generate_headers()
92
+
93
+ response = requests.put(url, json=json, headers=headers, params=params, **kwargs)
94
+ handle_error(response)
95
+
96
+ return response
97
+
98
+ def get(self, url: str, params: dict | None = None, **kwargs) -> requests.Response:
99
+ """Sends a GET request.
100
+
101
+ A wrapper for the requests.get method.
102
+
103
+ Builds the url, uses custom headers, refresh tokens if needed.
104
+
105
+ :param url: relative url of the API endpoint
106
+ :type url: str
107
+ :param params: Dictionary of querystring data to attach to the Request, defaults to None
108
+ :type params: dict, optional
109
+
110
+ :raises APIException: an api exception with message and error type code
111
+
112
+ :return: Response object
113
+ :rtype: requests.Response
114
+ """
115
+ self._refresh_token_if_expired()
116
+
117
+ url = self._add_base_url(url)
118
+ headers = self._generate_headers()
119
+
120
+ response = requests.get(url, params=params, headers=headers, **kwargs)
121
+ handle_error(response)
122
+
123
+ return response
124
+
125
+ def patch(
126
+ self, url: str, json: dict | None = None, params: dict | None = None, **kwargs
127
+ ) -> requests.Response:
128
+ """Sends a PATCH request.
129
+
130
+ A wrapper for the requests.patch method.
131
+
132
+ Builds the url, uses custom headers, refresh tokens if needed.
133
+
134
+ :param url: relative url of the API endpoint
135
+ :type url: str
136
+ :param json: A JSON serializable Python object to send in the body of the Request, defaults to None
137
+ :type json: dict, optional
138
+ :param params: Dictionary of querystring data to attach to the Request, defaults to None
139
+ :type params: dict, optional
140
+
141
+ :raises APIException: an api exception with message and error type code
142
+
143
+ :return: Response object
144
+ :rtype: requests.Response
145
+ """
146
+ self._refresh_token_if_expired()
147
+
148
+ url = self._add_base_url(url)
149
+ headers = self._generate_headers()
150
+
151
+ response = requests.patch(url, json=json, headers=headers, params=params, **kwargs)
152
+ handle_error(response)
153
+
154
+ return response
155
+
156
+ def delete(
157
+ self, url: str, json: dict | None = None, params: dict | None = None, **kwargs
158
+ ) -> requests.Response:
159
+ """Sends a DELETE request.
160
+
161
+ A wrapper for the requests.delete method.
162
+
163
+ Builds the url, uses custom headers, refresh tokens if needed.
164
+
165
+ :param url: relative url of the API endpoint
166
+ :type url: str
167
+ :param json: A JSON serializable Python object to send in the body of the Request, defaults to None
168
+ :type json: dict, optional
169
+ :param params: Dictionary of querystring data to attach to the Request, defaults to None
170
+ :type params: dict, optional
171
+
172
+ :raises APIException: an api exception with message and error type code
173
+
174
+ :return: Response object
175
+ :rtype: requests.Response
176
+ """
177
+ self._refresh_token_if_expired()
178
+
179
+ url = self._add_base_url(url)
180
+ headers = self._generate_headers()
181
+
182
+ response = requests.delete(url, headers=headers, json=json, params=params, **kwargs)
183
+ handle_error(response)
184
+
185
+ return response
186
+
187
+ def _refresh_token_if_expired(self) -> None:
188
+ """Refreshes the access token if it expired.
189
+
190
+ Uses the refresh token to refresh, and if the refresh token is also expired, uses the client credentials.
191
+
192
+ :raises APIException: an api exception with message and error type code
193
+ """
194
+ if self._auth_service.is_expired():
195
+ # try to refresh. if refresh token has expired, reauthenticate
196
+ try:
197
+ self._auth_service.refresh()
198
+ except Exception:
199
+ self._auth_service.authenticate()
200
+
201
+ def _generate_headers(self) -> dict:
202
+ """Generate the default headers for every request.
203
+
204
+ :return: dict with request headers
205
+ :rtype: dict
206
+ """
207
+ headers = {
208
+ 'Authorization': self._generate_bearer_header(),
209
+ 'User-Agent': self._generate_user_agent(),
210
+ 'Content-Type': 'application/json',
211
+ }
212
+ return headers
213
+
214
+ def _generate_bearer_header(self) -> str:
215
+ """Generate the authorization header Bearer string.
216
+
217
+ :return: Authorization header Bearer string
218
+ :rtype: str
219
+ """
220
+ return f'Bearer {self._auth_service._access_token}'
221
+
222
+ def _generate_user_agent(self) -> str:
223
+ """Generate the user agent string.
224
+
225
+ :return: user agent string
226
+ :rtype: str
227
+ """
228
+ # get the first 10 chars of the client id
229
+ client_id_truncated = self._auth_service._client_id[:10]
230
+
231
+ return f'datacrunch-python-v{self._version}-{client_id_truncated}'
232
+
233
+ def _add_base_url(self, url: str) -> str:
234
+ """Adds the base url to the relative url.
235
+
236
+ Example:
237
+ if the relative url is '/balance'
238
+ and the base url is 'https://api.datacrunch.io/v1'
239
+ then this method will return 'https://api.datacrunch.io/v1/balance'
240
+
241
+ :param url: a relative url path
242
+ :type url: str
243
+ :return: the full url path
244
+ :rtype: str
245
+ """
246
+ return self._base_url + url
File without changes
verda/images/images.py ADDED
@@ -0,0 +1,88 @@
1
+ from verda.helpers import stringify_class_object_properties
2
+
3
+ IMAGES_ENDPOINT = '/images'
4
+
5
+
6
+ class Image:
7
+ """An image model class."""
8
+
9
+ def __init__(self, id: str, name: str, image_type: str, details: list[str]) -> None:
10
+ """Initialize an image object.
11
+
12
+ :param id: image id
13
+ :type id: str
14
+ :param name: image name
15
+ :type name: str
16
+ :param image_type: image type, e.g. 'ubuntu-20.04-cuda-11.0'
17
+ :type image_type: str
18
+ :param details: image details
19
+ :type details: list[str]
20
+ """
21
+ self._id = id
22
+ self._name = name
23
+ self._image_type = image_type
24
+ self._details = details
25
+
26
+ @property
27
+ def id(self) -> str:
28
+ """Get the image id.
29
+
30
+ :return: image id
31
+ :rtype: str
32
+ """
33
+ return self._id
34
+
35
+ @property
36
+ def name(self) -> str:
37
+ """Get the image name.
38
+
39
+ :return: image name
40
+ :rtype: str
41
+ """
42
+ return self._name
43
+
44
+ @property
45
+ def image_type(self) -> str:
46
+ """Get the image type.
47
+
48
+ :return: image type
49
+ :rtype: str
50
+ """
51
+ return self._image_type
52
+
53
+ @property
54
+ def details(self) -> list[str]:
55
+ """Get the image details.
56
+
57
+ :return: image details
58
+ :rtype: list[str]
59
+ """
60
+ return self._details
61
+
62
+ def __str__(self) -> str:
63
+ """Returns a string of the json representation of the image.
64
+
65
+ :return: json representation of the image
66
+ :rtype: str
67
+ """
68
+ return stringify_class_object_properties(self)
69
+
70
+
71
+ class ImagesService:
72
+ """A service for interacting with the images endpoint."""
73
+
74
+ def __init__(self, http_client) -> None:
75
+ self._http_client = http_client
76
+
77
+ def get(self) -> list[Image]:
78
+ """Get the available instance images.
79
+
80
+ :return: list of images objects
81
+ :rtype: list[Image]
82
+ """
83
+ images = self._http_client.get(IMAGES_ENDPOINT).json()
84
+ image_objects = [
85
+ Image(image['id'], image['name'], image['image_type'], image['details'])
86
+ for image in images
87
+ ]
88
+ return image_objects
File without changes
@@ -0,0 +1,67 @@
1
+ from dataclasses import dataclass
2
+
3
+ from dataclasses_json import dataclass_json
4
+
5
+ INSTANCE_TYPES_ENDPOINT = '/instance-types'
6
+
7
+
8
+ @dataclass_json
9
+ @dataclass
10
+ class InstanceType:
11
+ """Instance type.
12
+
13
+ Attributes:
14
+ id: instance type id.
15
+ instance_type: instance type, e.g. '8V100.48M'.
16
+ price_per_hour: instance type price per hour.
17
+ spot_price_per_hour: instance type spot price per hour.
18
+ description: instance type description.
19
+ cpu: instance type cpu details.
20
+ gpu: instance type gpu details.
21
+ memory: instance type memory details.
22
+ gpu_memory: instance type gpu memory details.
23
+ storage: instance type storage details.
24
+ """
25
+
26
+ id: str
27
+ instance_type: str
28
+ price_per_hour: float
29
+ spot_price_per_hour: float
30
+ description: str
31
+ cpu: dict
32
+ gpu: dict
33
+ memory: dict
34
+ gpu_memory: dict
35
+ storage: dict
36
+
37
+
38
+ class InstanceTypesService:
39
+ """A service for interacting with the instance-types endpoint."""
40
+
41
+ def __init__(self, http_client) -> None:
42
+ self._http_client = http_client
43
+
44
+ def get(self) -> list[InstanceType]:
45
+ """Get all instance types.
46
+
47
+ :return: list of instance type objects
48
+ :rtype: list[InstanceType]
49
+ """
50
+ instance_types = self._http_client.get(INSTANCE_TYPES_ENDPOINT).json()
51
+ instance_type_objects = [
52
+ InstanceType(
53
+ id=instance_type['id'],
54
+ instance_type=instance_type['instance_type'],
55
+ price_per_hour=float(instance_type['price_per_hour']),
56
+ spot_price_per_hour=float(instance_type['spot_price']),
57
+ description=instance_type['description'],
58
+ cpu=instance_type['cpu'],
59
+ gpu=instance_type['gpu'],
60
+ memory=instance_type['memory'],
61
+ gpu_memory=instance_type['gpu_memory'],
62
+ storage=instance_type['storage'],
63
+ )
64
+ for instance_type in instance_types
65
+ ]
66
+
67
+ return instance_type_objects
File without changes