datacrunch 1.15.0__py3-none-any.whl → 1.16.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.
Files changed (58) hide show
  1. datacrunch/InferenceClient/inference_client.py +200 -65
  2. datacrunch/__init__.py +2 -0
  3. datacrunch/_version.py +6 -0
  4. datacrunch/authentication/authentication.py +7 -14
  5. datacrunch/balance/balance.py +1 -3
  6. datacrunch/constants.py +19 -17
  7. datacrunch/containers/containers.py +151 -123
  8. datacrunch/datacrunch.py +18 -18
  9. datacrunch/helpers.py +7 -2
  10. datacrunch/http_client/http_client.py +14 -14
  11. datacrunch/images/images.py +9 -3
  12. datacrunch/instance_types/instance_types.py +42 -35
  13. datacrunch/instances/instances.py +62 -50
  14. datacrunch/locations/locations.py +1 -2
  15. datacrunch/ssh_keys/ssh_keys.py +3 -4
  16. datacrunch/startup_scripts/startup_scripts.py +10 -8
  17. datacrunch/volume_types/volume_types.py +10 -8
  18. datacrunch/volumes/volumes.py +60 -73
  19. {datacrunch-1.15.0.dist-info → datacrunch-1.16.0.dist-info}/METADATA +46 -72
  20. datacrunch-1.16.0.dist-info/RECORD +35 -0
  21. datacrunch-1.16.0.dist-info/WHEEL +4 -0
  22. datacrunch/__version__.py +0 -1
  23. datacrunch-1.15.0.dist-info/RECORD +0 -69
  24. datacrunch-1.15.0.dist-info/WHEEL +0 -5
  25. datacrunch-1.15.0.dist-info/licenses/LICENSE +0 -21
  26. datacrunch-1.15.0.dist-info/top_level.txt +0 -2
  27. tests/__init__.py +0 -0
  28. tests/integration_tests/__init__.py +0 -0
  29. tests/integration_tests/conftest.py +0 -20
  30. tests/integration_tests/test_instances.py +0 -36
  31. tests/integration_tests/test_locations.py +0 -65
  32. tests/integration_tests/test_volumes.py +0 -94
  33. tests/unit_tests/__init__.py +0 -0
  34. tests/unit_tests/authentication/__init__.py +0 -0
  35. tests/unit_tests/authentication/test_authentication.py +0 -202
  36. tests/unit_tests/balance/__init__.py +0 -0
  37. tests/unit_tests/balance/test_balance.py +0 -25
  38. tests/unit_tests/conftest.py +0 -21
  39. tests/unit_tests/containers/__init__.py +0 -1
  40. tests/unit_tests/containers/test_containers.py +0 -959
  41. tests/unit_tests/http_client/__init__.py +0 -0
  42. tests/unit_tests/http_client/test_http_client.py +0 -193
  43. tests/unit_tests/images/__init__.py +0 -0
  44. tests/unit_tests/images/test_images.py +0 -41
  45. tests/unit_tests/instance_types/__init__.py +0 -0
  46. tests/unit_tests/instance_types/test_instance_types.py +0 -87
  47. tests/unit_tests/instances/__init__.py +0 -0
  48. tests/unit_tests/instances/test_instances.py +0 -483
  49. tests/unit_tests/ssh_keys/__init__.py +0 -0
  50. tests/unit_tests/ssh_keys/test_ssh_keys.py +0 -198
  51. tests/unit_tests/startup_scripts/__init__.py +0 -0
  52. tests/unit_tests/startup_scripts/test_startup_scripts.py +0 -196
  53. tests/unit_tests/test_datacrunch.py +0 -65
  54. tests/unit_tests/test_exceptions.py +0 -33
  55. tests/unit_tests/volume_types/__init__.py +0 -0
  56. tests/unit_tests/volume_types/test_volume_types.py +0 -50
  57. tests/unit_tests/volumes/__init__.py +0 -0
  58. tests/unit_tests/volumes/test_volumes.py +0 -641
@@ -2,7 +2,7 @@ import requests
2
2
  import json
3
3
 
4
4
  from datacrunch.exceptions import APIException
5
- from datacrunch.__version__ import VERSION
5
+ from datacrunch._version import __version__
6
6
 
7
7
 
8
8
  def handle_error(response: requests.Response) -> None:
@@ -27,7 +27,7 @@ class HTTPClient:
27
27
  """
28
28
 
29
29
  def __init__(self, auth_service, base_url: str) -> None:
30
- self._version = VERSION
30
+ self._version = __version__
31
31
  self._base_url = base_url
32
32
  self._auth_service = auth_service
33
33
  self._auth_service.authenticate()
@@ -56,8 +56,7 @@ class HTTPClient:
56
56
  url = self._add_base_url(url)
57
57
  headers = self._generate_headers()
58
58
 
59
- response = requests.post(
60
- url, json=json, headers=headers, params=params, **kwargs)
59
+ response = requests.post(url, json=json, headers=headers, params=params, **kwargs)
61
60
  handle_error(response)
62
61
 
63
62
  return response
@@ -86,8 +85,7 @@ class HTTPClient:
86
85
  url = self._add_base_url(url)
87
86
  headers = self._generate_headers()
88
87
 
89
- response = requests.put(
90
- url, json=json, headers=headers, params=params, **kwargs)
88
+ response = requests.put(url, json=json, headers=headers, params=params, **kwargs)
91
89
  handle_error(response)
92
90
 
93
91
  return response
@@ -119,7 +117,9 @@ class HTTPClient:
119
117
 
120
118
  return response
121
119
 
122
- def patch(self, url: str, json: dict = None, params: dict = None, **kwargs) -> requests.Response:
120
+ def patch(
121
+ self, url: str, json: dict = None, params: dict = None, **kwargs
122
+ ) -> requests.Response:
123
123
  """Sends a PATCH request.
124
124
 
125
125
  A wrapper for the requests.patch method.
@@ -143,13 +143,14 @@ class HTTPClient:
143
143
  url = self._add_base_url(url)
144
144
  headers = self._generate_headers()
145
145
 
146
- response = requests.patch(
147
- url, json=json, headers=headers, params=params, **kwargs)
146
+ response = requests.patch(url, json=json, headers=headers, params=params, **kwargs)
148
147
  handle_error(response)
149
148
 
150
149
  return response
151
150
 
152
- def delete(self, url: str, json: dict = None, params: dict = None, **kwargs) -> requests.Response:
151
+ def delete(
152
+ self, url: str, json: dict = None, params: dict = None, **kwargs
153
+ ) -> requests.Response:
153
154
  """Sends a DELETE request.
154
155
 
155
156
  A wrapper for the requests.delete method.
@@ -173,8 +174,7 @@ class HTTPClient:
173
174
  url = self._add_base_url(url)
174
175
  headers = self._generate_headers()
175
176
 
176
- response = requests.delete(
177
- url, headers=headers, json=json, params=params, **kwargs)
177
+ response = requests.delete(url, headers=headers, json=json, params=params, **kwargs)
178
178
  handle_error(response)
179
179
 
180
180
  return response
@@ -186,7 +186,7 @@ class HTTPClient:
186
186
 
187
187
  :raises APIException: an api exception with message and error type code
188
188
  """
189
- if (self._auth_service.is_expired()):
189
+ if self._auth_service.is_expired():
190
190
  # try to refresh. if refresh token has expired, reauthenticate
191
191
  try:
192
192
  self._auth_service.refresh()
@@ -202,7 +202,7 @@ class HTTPClient:
202
202
  headers = {
203
203
  'Authorization': self._generate_bearer_header(),
204
204
  'User-Agent': self._generate_user_agent(),
205
- 'Content-Type': 'application/json'
205
+ 'Content-Type': 'application/json',
206
206
  }
207
207
  return headers
208
208
 
@@ -76,12 +76,18 @@ class ImagesService:
76
76
  self._http_client = http_client
77
77
 
78
78
  def get(self) -> List[Image]:
79
- """Get the available instance images
79
+ """Get the available instance images
80
80
 
81
81
  :return: list of images objects
82
82
  :rtype: List[Image]
83
83
  """
84
84
  images = self._http_client.get(IMAGES_ENDPOINT).json()
85
- image_objects = list(map(lambda image: Image(
86
- image['id'], image['name'], image['image_type'], image['details']), images))
85
+ image_objects = list(
86
+ map(
87
+ lambda image: Image(
88
+ image['id'], image['name'], image['image_type'], image['details']
89
+ ),
90
+ images,
91
+ )
92
+ )
87
93
  return image_objects
@@ -4,18 +4,19 @@ INSTANCE_TYPES_ENDPOINT = '/instance-types'
4
4
 
5
5
 
6
6
  class InstanceType:
7
-
8
- def __init__(self,
9
- id: str,
10
- instance_type: str,
11
- price_per_hour: float,
12
- spot_price_per_hour: float,
13
- description: str,
14
- cpu: dict,
15
- gpu: dict,
16
- memory: dict,
17
- gpu_memory: dict,
18
- storage: dict) -> None:
7
+ def __init__(
8
+ self,
9
+ id: str,
10
+ instance_type: str,
11
+ price_per_hour: float,
12
+ spot_price_per_hour: float,
13
+ description: str,
14
+ cpu: dict,
15
+ gpu: dict,
16
+ memory: dict,
17
+ gpu_memory: dict,
18
+ storage: dict,
19
+ ) -> None:
19
20
  """Initialize an instance type object
20
21
 
21
22
  :param id: instance type id
@@ -146,17 +147,18 @@ class InstanceType:
146
147
  :return: instance type string representation
147
148
  :rtype: str
148
149
  """
149
- return (f'id: {self._id}\n'
150
- f'instance type: {self._instance_type}\n'
151
- f'price_per_hour: ${self._price_per_hour}\n'
152
- f'spot_price_per_hour: ${self._spot_price_per_hour}\n'
153
- f'description: {self._description}\n'
154
- f'cpu: {self._cpu}\n'
155
- f'gpu: {self._gpu}\n'
156
- f'memory :{self._memory}\n'
157
- f'gpu_memory :{self._gpu_memory}\n'
158
- f'storage :{self._storage}\n'
159
- )
150
+ return (
151
+ f'id: {self._id}\n'
152
+ f'instance type: {self._instance_type}\n'
153
+ f'price_per_hour: ${self._price_per_hour}\n'
154
+ f'spot_price_per_hour: ${self._spot_price_per_hour}\n'
155
+ f'description: {self._description}\n'
156
+ f'cpu: {self._cpu}\n'
157
+ f'gpu: {self._gpu}\n'
158
+ f'memory :{self._memory}\n'
159
+ f'gpu_memory :{self._gpu_memory}\n'
160
+ f'storage :{self._storage}\n'
161
+ )
160
162
 
161
163
 
162
164
  class InstanceTypesService:
@@ -172,17 +174,22 @@ class InstanceTypesService:
172
174
  :rtype: List[InstanceType]
173
175
  """
174
176
  instance_types = self._http_client.get(INSTANCE_TYPES_ENDPOINT).json()
175
- instance_type_objects = list(map(lambda instance_type: InstanceType(
176
- id=instance_type['id'],
177
- instance_type=instance_type['instance_type'],
178
- price_per_hour=instance_type['price_per_hour'],
179
- spot_price_per_hour=instance_type['spot_price'],
180
- description=instance_type['description'],
181
- cpu=instance_type['cpu'],
182
- gpu=instance_type['gpu'],
183
- memory=instance_type['memory'],
184
- gpu_memory=instance_type['gpu_memory'],
185
- storage=instance_type['storage']
186
- ), instance_types))
177
+ instance_type_objects = list(
178
+ map(
179
+ lambda instance_type: InstanceType(
180
+ id=instance_type['id'],
181
+ instance_type=instance_type['instance_type'],
182
+ price_per_hour=instance_type['price_per_hour'],
183
+ spot_price_per_hour=instance_type['spot_price'],
184
+ description=instance_type['description'],
185
+ cpu=instance_type['cpu'],
186
+ gpu=instance_type['gpu'],
187
+ memory=instance_type['memory'],
188
+ gpu_memory=instance_type['gpu_memory'],
189
+ storage=instance_type['storage'],
190
+ ),
191
+ instance_types,
192
+ )
193
+ )
187
194
 
188
195
  return instance_type_objects
@@ -32,7 +32,7 @@ class Instance:
32
32
  gpu_memory: GPU memory configuration details.
33
33
  ip: IP address assigned to the instance.
34
34
  os_volume_id: ID of the operating system volume.
35
- location: Datacenter location code (default: Locations.FIN_01).
35
+ location: Datacenter location code (default: Locations.FIN_03).
36
36
  image: Image ID or type used for the instance.
37
37
  startup_script_id: ID of the startup script to run.
38
38
  is_spot: Whether the instance is a spot instance.
@@ -57,7 +57,7 @@ class Instance:
57
57
  ip: Optional[str] = None
58
58
  # Can be None if instance is still not provisioned
59
59
  os_volume_id: Optional[str] = None
60
- location: str = Locations.FIN_01
60
+ location: str = Locations.FIN_03
61
61
  image: Optional[str] = None
62
62
  startup_script_id: Optional[str] = None
63
63
  is_spot: bool = False
@@ -90,9 +90,11 @@ class InstancesService:
90
90
  Returns:
91
91
  List of instance objects matching the criteria.
92
92
  """
93
- instances_dict = self._http_client.get(
94
- INSTANCES_ENDPOINT, params={'status': status}).json()
95
- return [Instance.from_dict(instance_dict, infer_missing=True) for instance_dict in instances_dict]
93
+ instances_dict = self._http_client.get(INSTANCES_ENDPOINT, params={'status': status}).json()
94
+ return [
95
+ Instance.from_dict(instance_dict, infer_missing=True)
96
+ for instance_dict in instances_dict
97
+ ]
96
98
 
97
99
  def get_by_id(self, id: str) -> Instance:
98
100
  """Retrieves a specific instance by its ID.
@@ -106,30 +108,31 @@ class InstancesService:
106
108
  Raises:
107
109
  HTTPError: If the instance is not found or other API error occurs.
108
110
  """
109
- instance_dict = self._http_client.get(
110
- INSTANCES_ENDPOINT + f'/{id}').json()
111
+ instance_dict = self._http_client.get(INSTANCES_ENDPOINT + f'/{id}').json()
111
112
  return Instance.from_dict(instance_dict, infer_missing=True)
112
113
 
113
- def create(self,
114
- instance_type: str,
115
- image: str,
116
- hostname: str,
117
- description: str,
118
- ssh_key_ids: list = [],
119
- location: str = Locations.FIN_01,
120
- startup_script_id: Optional[str] = None,
121
- volumes: Optional[List[Dict]] = None,
122
- existing_volumes: Optional[List[str]] = None,
123
- os_volume: Optional[Dict] = None,
124
- is_spot: bool = False,
125
- contract: Optional[Contract] = None,
126
- pricing: Optional[Pricing] = None,
127
- coupon: Optional[str] = None,
128
- *,
129
- max_wait_time: float = 180,
130
- initial_interval: float = 0.5,
131
- max_interval: float = 5,
132
- backoff_coefficient: float = 2.0) -> Instance:
114
+ def create(
115
+ self,
116
+ instance_type: str,
117
+ image: str,
118
+ hostname: str,
119
+ description: str,
120
+ ssh_key_ids: list = [],
121
+ location: str = Locations.FIN_03,
122
+ startup_script_id: Optional[str] = None,
123
+ volumes: Optional[List[Dict]] = None,
124
+ existing_volumes: Optional[List[str]] = None,
125
+ os_volume: Optional[Dict] = None,
126
+ is_spot: bool = False,
127
+ contract: Optional[Contract] = None,
128
+ pricing: Optional[Pricing] = None,
129
+ coupon: Optional[str] = None,
130
+ *,
131
+ max_wait_time: float = 180,
132
+ initial_interval: float = 0.5,
133
+ max_interval: float = 5,
134
+ backoff_coefficient: float = 2.0,
135
+ ) -> Instance:
133
136
  """Creates and deploys a new cloud instance.
134
137
 
135
138
  Args:
@@ -138,7 +141,7 @@ class InstancesService:
138
141
  hostname: Network hostname for the instance.
139
142
  description: Human-readable description of the instance.
140
143
  ssh_key_ids: List of SSH key IDs to associate with the instance.
141
- location: Datacenter location code (default: Locations.FIN_01).
144
+ location: Datacenter location code (default: Locations.FIN_03).
142
145
  startup_script_id: Optional ID of startup script to run.
143
146
  volumes: Optional list of volume configurations to create.
144
147
  existing_volumes: Optional list of existing volume IDs to attach.
@@ -159,18 +162,18 @@ class InstancesService:
159
162
  HTTPError: If instance creation fails or other API error occurs.
160
163
  """
161
164
  payload = {
162
- "instance_type": instance_type,
163
- "image": image,
164
- "ssh_key_ids": ssh_key_ids,
165
- "startup_script_id": startup_script_id,
166
- "hostname": hostname,
167
- "description": description,
168
- "location_code": location,
169
- "os_volume": os_volume,
170
- "volumes": volumes,
171
- "existing_volumes": existing_volumes,
172
- "is_spot": is_spot,
173
- "coupon": coupon,
165
+ 'instance_type': instance_type,
166
+ 'image': image,
167
+ 'ssh_key_ids': ssh_key_ids,
168
+ 'startup_script_id': startup_script_id,
169
+ 'hostname': hostname,
170
+ 'description': description,
171
+ 'location_code': location,
172
+ 'os_volume': os_volume,
173
+ 'volumes': volumes,
174
+ 'existing_volumes': existing_volumes,
175
+ 'is_spot': is_spot,
176
+ 'coupon': coupon,
174
177
  }
175
178
  if contract:
176
179
  payload['contract'] = contract
@@ -188,12 +191,18 @@ class InstancesService:
188
191
  now = time.monotonic()
189
192
  if now >= deadline:
190
193
  raise TimeoutError(
191
- f"Instance {id} did not enter provisioning state within {max_wait_time:.1f} seconds")
194
+ f'Instance {id} did not enter provisioning state within {max_wait_time:.1f} seconds'
195
+ )
192
196
 
193
- interval = min(initial_interval * backoff_coefficient ** i, max_interval, deadline - now)
197
+ interval = min(initial_interval * backoff_coefficient**i, max_interval, deadline - now)
194
198
  time.sleep(interval)
195
199
 
196
- def action(self, id_list: Union[List[str], str], action: str, volume_ids: Optional[List[str]] = None) -> None:
200
+ def action(
201
+ self,
202
+ id_list: Union[List[str], str],
203
+ action: str,
204
+ volume_ids: Optional[List[str]] = None,
205
+ ) -> None:
197
206
  """Performs an action on one or more instances.
198
207
 
199
208
  Args:
@@ -207,16 +216,17 @@ class InstancesService:
207
216
  if type(id_list) is str:
208
217
  id_list = [id_list]
209
218
 
210
- payload = {
211
- "id": id_list,
212
- "action": action,
213
- "volume_ids": volume_ids
214
- }
219
+ payload = {'id': id_list, 'action': action, 'volume_ids': volume_ids}
215
220
 
216
221
  self._http_client.put(INSTANCES_ENDPOINT, json=payload)
217
222
  return
218
223
 
219
- def is_available(self, instance_type: str, is_spot: bool = False, location_code: Optional[str] = None) -> bool:
224
+ def is_available(
225
+ self,
226
+ instance_type: str,
227
+ is_spot: bool = False,
228
+ location_code: Optional[str] = None,
229
+ ) -> bool:
220
230
  """Checks if a specific instance type is available for deployment.
221
231
 
222
232
  Args:
@@ -232,7 +242,9 @@ class InstancesService:
232
242
  url = f'/instance-availability/{instance_type}'
233
243
  return self._http_client.get(url, query_params).json()
234
244
 
235
- def get_availabilities(self, is_spot: Optional[bool] = None, location_code: Optional[str] = None) -> List[Dict]:
245
+ def get_availabilities(
246
+ self, is_spot: Optional[bool] = None, location_code: Optional[str] = None
247
+ ) -> List[Dict]:
236
248
  """Retrieves a list of available instance types across locations.
237
249
 
238
250
  Args:
@@ -10,7 +10,6 @@ class LocationsService:
10
10
  self._http_client = http_client
11
11
 
12
12
  def get(self) -> List[dict]:
13
- """Get all locations
14
- """
13
+ """Get all locations"""
15
14
  locations = self._http_client.get(LOCATIONS_ENDPOINT).json()
16
15
  return locations
@@ -61,8 +61,7 @@ class SSHKeysService:
61
61
  :rtype: List[SSHKey]
62
62
  """
63
63
  keys = self._http_client.get(SSHKEYS_ENDPOINT).json()
64
- keys_object_list = list(map(lambda key: SSHKey(
65
- key['id'], key['name'], key['key']), keys))
64
+ keys_object_list = list(map(lambda key: SSHKey(key['id'], key['name'], key['key']), keys))
66
65
 
67
66
  return keys_object_list
68
67
 
@@ -84,7 +83,7 @@ class SSHKeysService:
84
83
  :param id_list: list of SSH keys ids
85
84
  :type id_list: List[str]
86
85
  """
87
- payload = {"keys": id_list}
86
+ payload = {'keys': id_list}
88
87
  self._http_client.delete(SSHKEYS_ENDPOINT, json=payload)
89
88
  return
90
89
 
@@ -107,6 +106,6 @@ class SSHKeysService:
107
106
  :return: new SSH key object
108
107
  :rtype: SSHKey
109
108
  """
110
- payload = {"name": name, "key": key}
109
+ payload = {'name': name, 'key': key}
111
110
  id = self._http_client.post(SSHKEYS_ENDPOINT, json=payload).text
112
111
  return SSHKey(id, name, key)
@@ -61,8 +61,12 @@ class StartupScriptsService:
61
61
  :rtype: List[StartupScript]
62
62
  """
63
63
  scripts = self._http_client.get(STARTUP_SCRIPTS_ENDPOINT).json()
64
- scripts_objects = list(map(lambda script: StartupScript(
65
- script['id'], script['name'], script['script']), scripts))
64
+ scripts_objects = list(
65
+ map(
66
+ lambda script: StartupScript(script['id'], script['name'], script['script']),
67
+ scripts,
68
+ )
69
+ )
66
70
  return scripts_objects
67
71
 
68
72
  def get_by_id(self, id) -> StartupScript:
@@ -73,8 +77,7 @@ class StartupScriptsService:
73
77
  :return: startup script object
74
78
  :rtype: StartupScript
75
79
  """
76
- script = self._http_client.get(
77
- STARTUP_SCRIPTS_ENDPOINT + f'/{id}').json()[0]
80
+ script = self._http_client.get(STARTUP_SCRIPTS_ENDPOINT + f'/{id}').json()[0]
78
81
 
79
82
  return StartupScript(script['id'], script['name'], script['script'])
80
83
 
@@ -84,7 +87,7 @@ class StartupScriptsService:
84
87
  :param id_list: list of startup scripts ids
85
88
  :type id_list: List[str]
86
89
  """
87
- payload = {"scripts": id_list}
90
+ payload = {'scripts': id_list}
88
91
  self._http_client.delete(STARTUP_SCRIPTS_ENDPOINT, json=payload)
89
92
  return
90
93
 
@@ -107,7 +110,6 @@ class StartupScriptsService:
107
110
  :return: the new startup script's id
108
111
  :rtype: str
109
112
  """
110
- payload = {"name": name, "script": script}
111
- id = self._http_client.post(
112
- STARTUP_SCRIPTS_ENDPOINT, json=payload).text
113
+ payload = {'name': name, 'script': script}
114
+ id = self._http_client.post(STARTUP_SCRIPTS_ENDPOINT, json=payload).text
113
115
  return StartupScript(id, name, script)
@@ -4,10 +4,7 @@ VOLUME_TYPES_ENDPOINT = '/volume-types'
4
4
 
5
5
 
6
6
  class VolumeType:
7
-
8
- def __init__(self,
9
- type: str,
10
- price_per_month_per_gb: float) -> None:
7
+ def __init__(self, type: str, price_per_month_per_gb: float) -> None:
11
8
  """Initialize a volume type object
12
9
 
13
10
  :param type: volume type name
@@ -58,9 +55,14 @@ class VolumeTypesService:
58
55
  :rtype: List[VolumesType]
59
56
  """
60
57
  volume_types = self._http_client.get(VOLUME_TYPES_ENDPOINT).json()
61
- volume_type_objects = list(map(lambda volume_type: VolumeType(
62
- type=volume_type['type'],
63
- price_per_month_per_gb=volume_type['price']['price_per_month_per_gb'],
64
- ), volume_types))
58
+ volume_type_objects = list(
59
+ map(
60
+ lambda volume_type: VolumeType(
61
+ type=volume_type['type'],
62
+ price_per_month_per_gb=volume_type['price']['price_per_month_per_gb'],
63
+ ),
64
+ volume_types,
65
+ )
66
+ )
65
67
 
66
68
  return volume_type_objects