datacrunch 1.10.0__py3-none-any.whl → 1.12.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.
- datacrunch/InferenceClient/inference_client.py +36 -0
- datacrunch/__version__.py +1 -1
- datacrunch/constants.py +1 -0
- datacrunch/containers/containers.py +52 -0
- datacrunch/instances/instances.py +157 -417
- {datacrunch-1.10.0.dist-info → datacrunch-1.12.0.dist-info}/METADATA +1 -1
- {datacrunch-1.10.0.dist-info → datacrunch-1.12.0.dist-info}/RECORD +11 -11
- {datacrunch-1.10.0.dist-info → datacrunch-1.12.0.dist-info}/WHEEL +1 -1
- tests/unit_tests/containers/test_containers.py +2 -1
- {datacrunch-1.10.0.dist-info → datacrunch-1.12.0.dist-info}/licenses/LICENSE +0 -0
- {datacrunch-1.10.0.dist-info → datacrunch-1.12.0.dist-info}/top_level.txt +0 -0
|
@@ -6,16 +6,19 @@ from typing import Optional, Dict, Any, Union, Generator
|
|
|
6
6
|
from urllib.parse import urlparse
|
|
7
7
|
from enum import Enum
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
class InferenceClientError(Exception):
|
|
10
11
|
"""Base exception for InferenceClient errors."""
|
|
11
12
|
pass
|
|
12
13
|
|
|
14
|
+
|
|
13
15
|
class AsyncStatus(int, Enum):
|
|
14
16
|
Initialized = 0
|
|
15
17
|
Queue = 1
|
|
16
18
|
Inference = 2
|
|
17
19
|
Completed = 3
|
|
18
20
|
|
|
21
|
+
|
|
19
22
|
@dataclass_json(undefined=Undefined.EXCLUDE)
|
|
20
23
|
@dataclass
|
|
21
24
|
class InferenceResponse:
|
|
@@ -222,6 +225,22 @@ class InferenceClient:
|
|
|
222
225
|
raise InferenceClientError(f"Request to {path} failed: {str(e)}")
|
|
223
226
|
|
|
224
227
|
def run_sync(self, data: Dict[str, Any], path: str = "", timeout_seconds: int = 60 * 5, headers: Optional[Dict[str, str]] = None, http_method: str = "POST", stream: bool = False):
|
|
228
|
+
"""Make a synchronous request to the inference endpoint.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
data: The data payload to send with the request
|
|
232
|
+
path: API endpoint path. Defaults to empty string.
|
|
233
|
+
timeout_seconds: Request timeout in seconds. Defaults to 5 minutes.
|
|
234
|
+
headers: Optional headers to include in the request
|
|
235
|
+
http_method: HTTP method to use. Defaults to "POST".
|
|
236
|
+
stream: Whether to stream the response. Defaults to False.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
InferenceResponse: Object containing the response data.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
InferenceClientError: If the request fails
|
|
243
|
+
"""
|
|
225
244
|
response = self._make_request(
|
|
226
245
|
http_method, path, json=data, timeout_seconds=timeout_seconds, headers=headers, stream=stream)
|
|
227
246
|
|
|
@@ -233,6 +252,23 @@ class InferenceClient:
|
|
|
233
252
|
)
|
|
234
253
|
|
|
235
254
|
def run(self, data: Dict[str, Any], path: str = "", timeout_seconds: int = 60 * 5, headers: Optional[Dict[str, str]] = None, http_method: str = "POST", no_response: bool = False):
|
|
255
|
+
"""Make an asynchronous request to the inference endpoint.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
data: The data payload to send with the request
|
|
259
|
+
path: API endpoint path. Defaults to empty string.
|
|
260
|
+
timeout_seconds: Request timeout in seconds. Defaults to 5 minutes.
|
|
261
|
+
headers: Optional headers to include in the request
|
|
262
|
+
http_method: HTTP method to use. Defaults to "POST".
|
|
263
|
+
no_response: If True, don't wait for response. Defaults to False.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
AsyncInferenceExecution: Object to track the async execution status.
|
|
267
|
+
If no_response is True, returns None.
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
InferenceClientError: If the request fails
|
|
271
|
+
"""
|
|
236
272
|
# Add relevant headers to the request, to indicate that the request is async
|
|
237
273
|
headers = headers or {}
|
|
238
274
|
if no_response:
|
datacrunch/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
VERSION = '1.
|
|
1
|
+
VERSION = '1.12.0'
|
datacrunch/constants.py
CHANGED
|
@@ -4,6 +4,8 @@ This module provides functionality for managing container deployments, including
|
|
|
4
4
|
creation, updates, deletion, and monitoring of containerized applications.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import base64
|
|
8
|
+
import os
|
|
7
9
|
from dataclasses import dataclass, field
|
|
8
10
|
from dataclasses_json import dataclass_json, Undefined # type: ignore
|
|
9
11
|
from typing import List, Optional, Dict, Any
|
|
@@ -18,6 +20,7 @@ CONTAINER_DEPLOYMENTS_ENDPOINT = '/container-deployments'
|
|
|
18
20
|
SERVERLESS_COMPUTE_RESOURCES_ENDPOINT = '/serverless-compute-resources'
|
|
19
21
|
CONTAINER_REGISTRY_CREDENTIALS_ENDPOINT = '/container-registry-credentials'
|
|
20
22
|
SECRETS_ENDPOINT = '/secrets'
|
|
23
|
+
FILESET_SECRETS_ENDPOINT = '/file-secrets'
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
class EnvVarType(str, Enum):
|
|
@@ -27,6 +30,13 @@ class EnvVarType(str, Enum):
|
|
|
27
30
|
SECRET = "secret"
|
|
28
31
|
|
|
29
32
|
|
|
33
|
+
class SecretType(str, Enum):
|
|
34
|
+
"""Types of secrets that can be set in containers."""
|
|
35
|
+
|
|
36
|
+
GENERIC = "generic" # Regular secret, can be used in env vars
|
|
37
|
+
FILESET = "file-secret" # A file secret that can be mounted into the container
|
|
38
|
+
|
|
39
|
+
|
|
30
40
|
class VolumeMountType(str, Enum):
|
|
31
41
|
"""Types of volume mounts that can be configured for containers."""
|
|
32
42
|
|
|
@@ -446,10 +456,12 @@ class Secret:
|
|
|
446
456
|
Attributes:
|
|
447
457
|
name: Name of the secret.
|
|
448
458
|
created_at: Timestamp when the secret was created.
|
|
459
|
+
secret_type: Type of the secret.
|
|
449
460
|
"""
|
|
450
461
|
|
|
451
462
|
name: str
|
|
452
463
|
created_at: str
|
|
464
|
+
secret_type: SecretType
|
|
453
465
|
|
|
454
466
|
|
|
455
467
|
@dataclass_json
|
|
@@ -909,6 +921,7 @@ class ContainersService:
|
|
|
909
921
|
List[Secret]: List of all secrets.
|
|
910
922
|
"""
|
|
911
923
|
response = self.client.get(SECRETS_ENDPOINT)
|
|
924
|
+
print(response.json())
|
|
912
925
|
return [Secret.from_dict(secret) for secret in response.json()]
|
|
913
926
|
|
|
914
927
|
def create_secret(self, name: str, value: str) -> None:
|
|
@@ -956,3 +969,42 @@ class ContainersService:
|
|
|
956
969
|
"""
|
|
957
970
|
self.client.delete(
|
|
958
971
|
f"{CONTAINER_REGISTRY_CREDENTIALS_ENDPOINT}/{credentials_name}")
|
|
972
|
+
|
|
973
|
+
def get_fileset_secrets(self) -> List[Secret]:
|
|
974
|
+
"""Retrieves all fileset secrets.
|
|
975
|
+
|
|
976
|
+
Returns:
|
|
977
|
+
List of all fileset secrets.
|
|
978
|
+
"""
|
|
979
|
+
response = self.client.get(FILESET_SECRETS_ENDPOINT)
|
|
980
|
+
return [Secret.from_dict(secret) for secret in response.json()]
|
|
981
|
+
|
|
982
|
+
def delete_fileset_secret(self, secret_name: str) -> None:
|
|
983
|
+
"""Deletes a fileset secret.
|
|
984
|
+
|
|
985
|
+
Args:
|
|
986
|
+
secret_name: Name of the secret to delete.
|
|
987
|
+
"""
|
|
988
|
+
self.client.delete(f"{FILESET_SECRETS_ENDPOINT}/{secret_name}")
|
|
989
|
+
|
|
990
|
+
def create_fileset_secret_from_file_paths(self, secret_name: str, file_paths: List[str]) -> None:
|
|
991
|
+
"""Creates a new fileset secret.
|
|
992
|
+
A fileset secret is a secret that contains several files,
|
|
993
|
+
and can be used to mount a directory with the files in a container.
|
|
994
|
+
|
|
995
|
+
Args:
|
|
996
|
+
secret_name: Name of the secret.
|
|
997
|
+
file_paths: List of file paths to include in the secret.
|
|
998
|
+
"""
|
|
999
|
+
processed_files = []
|
|
1000
|
+
for file_path in file_paths:
|
|
1001
|
+
with open(file_path, "rb") as f:
|
|
1002
|
+
base64_content = base64.b64encode(f.read()).decode("utf-8")
|
|
1003
|
+
processed_files.append({
|
|
1004
|
+
"file_name": os.path.basename(file_path),
|
|
1005
|
+
"base64_content": base64_content
|
|
1006
|
+
})
|
|
1007
|
+
self.client.post(FILESET_SECRETS_ENDPOINT, {
|
|
1008
|
+
"name": secret_name,
|
|
1009
|
+
"files": processed_files
|
|
1010
|
+
})
|
|
@@ -1,376 +1,113 @@
|
|
|
1
|
+
import time
|
|
1
2
|
from typing import List, Union, Optional, Dict, Literal
|
|
2
|
-
from
|
|
3
|
-
from
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from dataclasses_json import dataclass_json
|
|
5
|
+
from datacrunch.constants import Locations, InstanceStatus
|
|
4
6
|
|
|
5
7
|
INSTANCES_ENDPOINT = '/instances'
|
|
6
8
|
|
|
7
9
|
Contract = Literal['LONG_TERM', 'PAY_AS_YOU_GO', 'SPOT']
|
|
8
10
|
Pricing = Literal['DYNAMIC_PRICE', 'FIXED_PRICE']
|
|
9
11
|
|
|
10
|
-
class Instance:
|
|
11
|
-
"""An instance model class"""
|
|
12
|
-
|
|
13
|
-
def __init__(self,
|
|
14
|
-
id: str,
|
|
15
|
-
instance_type: str,
|
|
16
|
-
image: str,
|
|
17
|
-
price_per_hour: float,
|
|
18
|
-
hostname: str,
|
|
19
|
-
description: str,
|
|
20
|
-
ip: str,
|
|
21
|
-
status: str,
|
|
22
|
-
created_at: str,
|
|
23
|
-
ssh_key_ids: List[str],
|
|
24
|
-
cpu: dict,
|
|
25
|
-
gpu: dict,
|
|
26
|
-
memory: dict,
|
|
27
|
-
storage: dict,
|
|
28
|
-
os_volume_id: str,
|
|
29
|
-
gpu_memory: dict,
|
|
30
|
-
location: str = Locations.FIN_01,
|
|
31
|
-
startup_script_id: str = None,
|
|
32
|
-
is_spot: bool = False,
|
|
33
|
-
contract: Contract = None,
|
|
34
|
-
pricing: Pricing = None,
|
|
35
|
-
) -> None:
|
|
36
|
-
"""Initialize the instance object
|
|
37
|
-
|
|
38
|
-
:param id: instance id
|
|
39
|
-
:type id: str
|
|
40
|
-
:param instance_type: instance type. e.g. '8V100.48M'
|
|
41
|
-
:type instance_type: str
|
|
42
|
-
:param image: instance image type. e.g. 'ubuntu-20.04-cuda-11.0'
|
|
43
|
-
:type image: str
|
|
44
|
-
:param price_per_hour: price per hour
|
|
45
|
-
:type price_per_hour: float
|
|
46
|
-
:param hostname: instance hostname
|
|
47
|
-
:type hostname: str
|
|
48
|
-
:param description: instance description
|
|
49
|
-
:type description: str
|
|
50
|
-
:param ip: instance ip address
|
|
51
|
-
:type ip: str
|
|
52
|
-
:param status: instance current status, might be out of date if changed
|
|
53
|
-
:type status: str
|
|
54
|
-
:param created_at: the time the instance was deployed (UTC)
|
|
55
|
-
:type created_at: str
|
|
56
|
-
:param ssh_key_ids: list of ssh keys ids
|
|
57
|
-
:type ssh_key_ids: List[str]
|
|
58
|
-
:param cpu: cpu details
|
|
59
|
-
:type cpu: dict
|
|
60
|
-
:param gpu: gpu details
|
|
61
|
-
:type gpu: dict
|
|
62
|
-
:param memory: memory details
|
|
63
|
-
:type memory: dict
|
|
64
|
-
:param storage: storate details
|
|
65
|
-
:type storage: dict
|
|
66
|
-
:param id: main OS volume id
|
|
67
|
-
:type id: str
|
|
68
|
-
:param memory: gpu memory details
|
|
69
|
-
:type memory: dict
|
|
70
|
-
:param location: datacenter location, defaults to "FIN-01"
|
|
71
|
-
:type location: str, optional
|
|
72
|
-
:param startup_script_id: startup script id, defaults to None
|
|
73
|
-
:type startup_script_id: str, optional
|
|
74
|
-
:param is_spot: is this a spot instance, defaults to None
|
|
75
|
-
:type is_spot: bool, optional
|
|
76
|
-
"""
|
|
77
|
-
self._id = id
|
|
78
|
-
self._instance_type = instance_type
|
|
79
|
-
self._image = image
|
|
80
|
-
self._price_per_hour = price_per_hour
|
|
81
|
-
self._location = location
|
|
82
|
-
self._hostname = hostname
|
|
83
|
-
self._description = description
|
|
84
|
-
self._ip = ip
|
|
85
|
-
self._status = status
|
|
86
|
-
self._created_at = created_at
|
|
87
|
-
self._ssh_key_ids = ssh_key_ids
|
|
88
|
-
self._startup_script_id = startup_script_id
|
|
89
|
-
self._cpu = cpu
|
|
90
|
-
self._gpu = gpu
|
|
91
|
-
self._memory = memory
|
|
92
|
-
self._storage = storage
|
|
93
|
-
self._os_volume_id = os_volume_id
|
|
94
|
-
self._gpu_memory = gpu_memory
|
|
95
|
-
self._is_spot = is_spot
|
|
96
|
-
self._contract = contract
|
|
97
|
-
self._pricing = pricing
|
|
98
|
-
|
|
99
|
-
@property
|
|
100
|
-
def id(self) -> str:
|
|
101
|
-
"""Get the instance id
|
|
102
|
-
|
|
103
|
-
:return: instance id
|
|
104
|
-
:rtype: str
|
|
105
|
-
"""
|
|
106
|
-
return self._id
|
|
107
|
-
|
|
108
|
-
@property
|
|
109
|
-
def instance_type(self) -> str:
|
|
110
|
-
"""Get the instance type
|
|
111
|
-
|
|
112
|
-
:return: instance type
|
|
113
|
-
:rtype: str
|
|
114
|
-
"""
|
|
115
|
-
return self._instance_type
|
|
116
|
-
|
|
117
|
-
@property
|
|
118
|
-
def image(self) -> str:
|
|
119
|
-
"""Get the instance image type
|
|
120
|
-
|
|
121
|
-
:return: instance image type
|
|
122
|
-
:rtype: str
|
|
123
|
-
"""
|
|
124
|
-
return self._image
|
|
125
|
-
|
|
126
|
-
@property
|
|
127
|
-
def price_per_hour(self) -> float:
|
|
128
|
-
"""Get the instance price per hour
|
|
129
|
-
|
|
130
|
-
:return: price per hour
|
|
131
|
-
:rtype: float
|
|
132
|
-
"""
|
|
133
|
-
return self._price_per_hour
|
|
134
|
-
|
|
135
|
-
@property
|
|
136
|
-
def location(self) -> str:
|
|
137
|
-
"""Get the instance datacenter location
|
|
138
|
-
|
|
139
|
-
:return: datacenter location
|
|
140
|
-
:rtype: str
|
|
141
|
-
"""
|
|
142
|
-
return self._location
|
|
143
|
-
|
|
144
|
-
@property
|
|
145
|
-
def hostname(self) -> str:
|
|
146
|
-
"""Get the instance hostname
|
|
147
|
-
|
|
148
|
-
:return: hostname
|
|
149
|
-
:rtype: str
|
|
150
|
-
"""
|
|
151
|
-
return self._hostname
|
|
152
|
-
|
|
153
|
-
@property
|
|
154
|
-
def description(self) -> str:
|
|
155
|
-
"""Get the instance description
|
|
156
|
-
|
|
157
|
-
:return: instance description
|
|
158
|
-
:rtype: str
|
|
159
|
-
"""
|
|
160
|
-
return self._description
|
|
161
|
-
|
|
162
|
-
@property
|
|
163
|
-
def ip(self) -> str:
|
|
164
|
-
"""Get the instance ip address
|
|
165
|
-
|
|
166
|
-
:return: ip address
|
|
167
|
-
:rtype: str
|
|
168
|
-
"""
|
|
169
|
-
return self._ip
|
|
170
|
-
|
|
171
|
-
@property
|
|
172
|
-
def status(self) -> str:
|
|
173
|
-
"""Get the current instance status. might be out of date if changed.
|
|
174
|
-
|
|
175
|
-
:return: instance status
|
|
176
|
-
:rtype: str
|
|
177
|
-
"""
|
|
178
|
-
return self._status
|
|
179
|
-
|
|
180
|
-
@property
|
|
181
|
-
def created_at(self) -> str:
|
|
182
|
-
"""Get the time when the instance was deployed (UTC)
|
|
183
|
-
|
|
184
|
-
:return: time
|
|
185
|
-
:rtype: str
|
|
186
|
-
"""
|
|
187
|
-
return self._created_at
|
|
188
|
-
|
|
189
|
-
@property
|
|
190
|
-
def ssh_key_ids(self) -> List[str]:
|
|
191
|
-
"""Get the SSH key IDs of the instance
|
|
192
|
-
|
|
193
|
-
:return: SSH key IDs
|
|
194
|
-
:rtype: List[str]
|
|
195
|
-
"""
|
|
196
|
-
return self._ssh_key_ids
|
|
197
|
-
|
|
198
|
-
@property
|
|
199
|
-
def startup_script_id(self) -> Union[str, None]:
|
|
200
|
-
"""Get the startup script ID or None if the is no script
|
|
201
|
-
|
|
202
|
-
:return: startup script ID or None
|
|
203
|
-
:rtype: Union[str, None]
|
|
204
|
-
"""
|
|
205
|
-
return self._startup_script_id
|
|
206
|
-
|
|
207
|
-
@property
|
|
208
|
-
def cpu(self) -> dict:
|
|
209
|
-
"""Get the instance cpu details
|
|
210
|
-
|
|
211
|
-
:return: cpu details
|
|
212
|
-
:rtype: dict
|
|
213
|
-
"""
|
|
214
|
-
return self._cpu
|
|
215
|
-
|
|
216
|
-
@property
|
|
217
|
-
def gpu(self) -> dict:
|
|
218
|
-
"""Get the instance gpu details
|
|
219
12
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
:
|
|
230
|
-
:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
:
|
|
239
|
-
:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
13
|
+
@dataclass_json
|
|
14
|
+
@dataclass
|
|
15
|
+
class Instance:
|
|
16
|
+
"""Represents a cloud instance with its configuration and state.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
id: Unique identifier for the instance.
|
|
20
|
+
instance_type: Type of the instance (e.g., '8V100.48V').
|
|
21
|
+
price_per_hour: Cost per hour of running the instance.
|
|
22
|
+
hostname: Network hostname of the instance.
|
|
23
|
+
description: Human-readable description of the instance.
|
|
24
|
+
status: Current operational status of the instance.
|
|
25
|
+
created_at: Timestamp of instance creation.
|
|
26
|
+
ssh_key_ids: List of SSH key IDs associated with the instance.
|
|
27
|
+
cpu: CPU configuration details.
|
|
28
|
+
gpu: GPU configuration details.
|
|
29
|
+
memory: Memory configuration details.
|
|
30
|
+
storage: Storage configuration details.
|
|
31
|
+
gpu_memory: GPU memory configuration details.
|
|
32
|
+
ip: IP address assigned to the instance.
|
|
33
|
+
os_volume_id: ID of the operating system volume.
|
|
34
|
+
location: Datacenter location code (default: Locations.FIN_01).
|
|
35
|
+
image: Image ID or type used for the instance.
|
|
36
|
+
startup_script_id: ID of the startup script to run.
|
|
37
|
+
is_spot: Whether the instance is a spot instance.
|
|
38
|
+
contract: Contract type for the instance. (e.g. 'LONG_TERM', 'PAY_AS_YOU_GO', 'SPOT')
|
|
39
|
+
pricing: Pricing model for the instance. (e.g. 'DYNAMIC_PRICE', 'FIXED_PRICE')
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
id: str
|
|
43
|
+
instance_type: str
|
|
44
|
+
price_per_hour: float
|
|
45
|
+
hostname: str
|
|
46
|
+
description: str
|
|
47
|
+
status: str
|
|
48
|
+
created_at: str
|
|
49
|
+
ssh_key_ids: List[str]
|
|
50
|
+
cpu: dict
|
|
51
|
+
gpu: dict
|
|
52
|
+
memory: dict
|
|
53
|
+
storage: dict
|
|
54
|
+
gpu_memory: dict
|
|
55
|
+
# Can be None if instance is still not provisioned
|
|
56
|
+
ip: Optional[str] = None
|
|
57
|
+
# Can be None if instance is still not provisioned
|
|
58
|
+
os_volume_id: Optional[str] = None
|
|
59
|
+
location: str = Locations.FIN_01
|
|
60
|
+
image: Optional[str] = None
|
|
61
|
+
startup_script_id: Optional[str] = None
|
|
62
|
+
is_spot: bool = False
|
|
63
|
+
contract: Optional[Contract] = None
|
|
64
|
+
pricing: Optional[Pricing] = None
|
|
269
65
|
|
|
270
|
-
@property
|
|
271
|
-
def contract(self) -> bool:
|
|
272
|
-
"""Get contract type
|
|
273
66
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"""
|
|
277
|
-
return self._contract
|
|
67
|
+
class InstancesService:
|
|
68
|
+
"""Service for managing cloud instances through the API.
|
|
278
69
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
70
|
+
This service provides methods to create, retrieve, and manage cloud instances
|
|
71
|
+
through the DataCrunch API.
|
|
72
|
+
"""
|
|
282
73
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
"""
|
|
286
|
-
return self._pricing
|
|
287
|
-
|
|
288
|
-
def __str__(self) -> str:
|
|
289
|
-
"""Returns a string of the json representation of the instance
|
|
74
|
+
def __init__(self, http_client) -> None:
|
|
75
|
+
"""Initializes the InstancesService with an HTTP client.
|
|
290
76
|
|
|
291
|
-
:
|
|
292
|
-
|
|
77
|
+
Args:
|
|
78
|
+
http_client: HTTP client for making API requests.
|
|
293
79
|
"""
|
|
294
|
-
return stringify_class_object_properties(self)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
class InstancesService:
|
|
298
|
-
"""A service for interacting with the instances endpoint"""
|
|
299
|
-
|
|
300
|
-
def __init__(self, http_client) -> None:
|
|
301
80
|
self._http_client = http_client
|
|
302
81
|
|
|
303
|
-
def get(self, status: str = None) -> List[Instance]:
|
|
304
|
-
"""
|
|
82
|
+
def get(self, status: Optional[str] = None) -> List[Instance]:
|
|
83
|
+
"""Retrieves all non-deleted instances or instances with specific status.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
status: Optional status filter for instances. If None, returns all
|
|
87
|
+
non-deleted instances.
|
|
305
88
|
|
|
306
|
-
:
|
|
307
|
-
|
|
308
|
-
:return: list of instance details objects
|
|
309
|
-
:rtype: List[Instance]
|
|
89
|
+
Returns:
|
|
90
|
+
List of instance objects matching the criteria.
|
|
310
91
|
"""
|
|
311
92
|
instances_dict = self._http_client.get(
|
|
312
93
|
INSTANCES_ENDPOINT, params={'status': status}).json()
|
|
313
|
-
|
|
314
|
-
id=instance_dict['id'],
|
|
315
|
-
instance_type=instance_dict['instance_type'],
|
|
316
|
-
image=instance_dict['image'],
|
|
317
|
-
price_per_hour=instance_dict['price_per_hour'] if 'price_per_hour' in instance_dict else None,
|
|
318
|
-
location=instance_dict['location'],
|
|
319
|
-
hostname=instance_dict['hostname'],
|
|
320
|
-
description=instance_dict['description'],
|
|
321
|
-
ip=instance_dict['ip'],
|
|
322
|
-
status=instance_dict['status'],
|
|
323
|
-
created_at=instance_dict['created_at'],
|
|
324
|
-
ssh_key_ids=instance_dict['ssh_key_ids'] if 'ssh_key_ids' in instance_dict else [
|
|
325
|
-
],
|
|
326
|
-
startup_script_id=instance_dict['startup_script_id'] if 'startup_script_id' in instance_dict else None,
|
|
327
|
-
cpu=instance_dict['cpu'],
|
|
328
|
-
gpu=instance_dict['gpu'],
|
|
329
|
-
memory=instance_dict['memory'],
|
|
330
|
-
storage=instance_dict['storage'],
|
|
331
|
-
os_volume_id=instance_dict['os_volume_id'] if 'os_volume_id' in instance_dict else None,
|
|
332
|
-
gpu_memory=instance_dict['gpu_memory'] if 'gpu_memory' in instance_dict else None,
|
|
333
|
-
is_spot=instance_dict['is_spot'] if 'is_spot' in instance_dict else False,
|
|
334
|
-
contract=instance_dict['contract'] if 'contract' in instance_dict else False,
|
|
335
|
-
pricing=instance_dict['pricing'] if 'pricing' in instance_dict else False,
|
|
336
|
-
), instances_dict))
|
|
337
|
-
return instances
|
|
94
|
+
return [Instance.from_dict(instance_dict, infer_missing=True) for instance_dict in instances_dict]
|
|
338
95
|
|
|
339
96
|
def get_by_id(self, id: str) -> Instance:
|
|
340
|
-
"""
|
|
97
|
+
"""Retrieves a specific instance by its ID.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
id: Unique identifier of the instance to retrieve.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Instance object with the specified ID.
|
|
341
104
|
|
|
342
|
-
:
|
|
343
|
-
|
|
344
|
-
:return: instance details object
|
|
345
|
-
:rtype: Instance
|
|
105
|
+
Raises:
|
|
106
|
+
HTTPError: If the instance is not found or other API error occurs.
|
|
346
107
|
"""
|
|
347
108
|
instance_dict = self._http_client.get(
|
|
348
109
|
INSTANCES_ENDPOINT + f'/{id}').json()
|
|
349
|
-
|
|
350
|
-
id=instance_dict['id'],
|
|
351
|
-
instance_type=instance_dict['instance_type'],
|
|
352
|
-
image=instance_dict['image'],
|
|
353
|
-
price_per_hour=instance_dict['price_per_hour'] if 'price_per_hour' in instance_dict else None,
|
|
354
|
-
location=instance_dict['location'],
|
|
355
|
-
hostname=instance_dict['hostname'],
|
|
356
|
-
description=instance_dict['description'],
|
|
357
|
-
ip=instance_dict['ip'],
|
|
358
|
-
status=instance_dict['status'],
|
|
359
|
-
created_at=instance_dict['created_at'],
|
|
360
|
-
ssh_key_ids=instance_dict['ssh_key_ids'] if 'ssh_key_ids' in instance_dict else [
|
|
361
|
-
],
|
|
362
|
-
startup_script_id=instance_dict['startup_script_id'] if 'startup_script_id' in instance_dict else None,
|
|
363
|
-
cpu=instance_dict['cpu'],
|
|
364
|
-
gpu=instance_dict['gpu'],
|
|
365
|
-
memory=instance_dict['memory'],
|
|
366
|
-
storage=instance_dict['storage'],
|
|
367
|
-
os_volume_id=instance_dict['os_volume_id'] if 'os_volume_id' in instance_dict else None,
|
|
368
|
-
gpu_memory=instance_dict['gpu_memory'] if 'gpu_memory' in instance_dict else None,
|
|
369
|
-
is_spot=instance_dict['is_spot'] if 'is_spot' in instance_dict else False,
|
|
370
|
-
contract=instance_dict['contract'] if 'contract' in instance_dict else False,
|
|
371
|
-
pricing=instance_dict['pricing'] if 'pricing' in instance_dict else False,
|
|
372
|
-
)
|
|
373
|
-
return instance
|
|
110
|
+
return Instance.from_dict(instance_dict, infer_missing=True)
|
|
374
111
|
|
|
375
112
|
def create(self,
|
|
376
113
|
instance_type: str,
|
|
@@ -379,46 +116,37 @@ class InstancesService:
|
|
|
379
116
|
description: str,
|
|
380
117
|
ssh_key_ids: list = [],
|
|
381
118
|
location: str = Locations.FIN_01,
|
|
382
|
-
startup_script_id: str = None,
|
|
383
|
-
volumes: List[Dict] = None,
|
|
384
|
-
existing_volumes: List[str] = None,
|
|
385
|
-
os_volume: Dict = None,
|
|
119
|
+
startup_script_id: Optional[str] = None,
|
|
120
|
+
volumes: Optional[List[Dict]] = None,
|
|
121
|
+
existing_volumes: Optional[List[str]] = None,
|
|
122
|
+
os_volume: Optional[Dict] = None,
|
|
386
123
|
is_spot: bool = False,
|
|
387
|
-
contract: Contract = None,
|
|
388
|
-
pricing: Pricing = None,
|
|
389
|
-
coupon: str = None) -> Instance:
|
|
390
|
-
"""Creates
|
|
391
|
-
|
|
392
|
-
:
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
:
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
:
|
|
412
|
-
|
|
413
|
-
:type is_spot: bool, optional
|
|
414
|
-
:param pricing: Pricing type
|
|
415
|
-
:type pricing: str, optional
|
|
416
|
-
:param contract: Contract type
|
|
417
|
-
:type contract: str, optional
|
|
418
|
-
:param coupon: coupon code
|
|
419
|
-
:type coupon: str, optional
|
|
420
|
-
:return: the new instance object
|
|
421
|
-
:rtype: id
|
|
124
|
+
contract: Optional[Contract] = None,
|
|
125
|
+
pricing: Optional[Pricing] = None,
|
|
126
|
+
coupon: Optional[str] = None) -> Instance:
|
|
127
|
+
"""Creates and deploys a new cloud instance.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
instance_type: Type of instance to create (e.g., '8V100.48V').
|
|
131
|
+
image: Image type or existing OS volume ID for the instance.
|
|
132
|
+
hostname: Network hostname for the instance.
|
|
133
|
+
description: Human-readable description of the instance.
|
|
134
|
+
ssh_key_ids: List of SSH key IDs to associate with the instance.
|
|
135
|
+
location: Datacenter location code (default: Locations.FIN_01).
|
|
136
|
+
startup_script_id: Optional ID of startup script to run.
|
|
137
|
+
volumes: Optional list of volume configurations to create.
|
|
138
|
+
existing_volumes: Optional list of existing volume IDs to attach.
|
|
139
|
+
os_volume: Optional OS volume configuration details.
|
|
140
|
+
is_spot: Whether to create a spot instance.
|
|
141
|
+
contract: Optional contract type for the instance.
|
|
142
|
+
pricing: Optional pricing model for the instance.
|
|
143
|
+
coupon: Optional coupon code for discounts.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The newly created instance object.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
HTTPError: If instance creation fails or other API error occurs.
|
|
422
150
|
"""
|
|
423
151
|
payload = {
|
|
424
152
|
"instance_type": instance_type,
|
|
@@ -439,18 +167,33 @@ class InstancesService:
|
|
|
439
167
|
if pricing:
|
|
440
168
|
payload['pricing'] = pricing
|
|
441
169
|
id = self._http_client.post(INSTANCES_ENDPOINT, json=payload).text
|
|
442
|
-
|
|
443
|
-
|
|
170
|
+
|
|
171
|
+
# Wait for instance to enter provisioning state with timeout
|
|
172
|
+
MAX_WAIT_TIME = 60 # Maximum wait time in seconds
|
|
173
|
+
POLL_INTERVAL = 0.5 # Time between status checks
|
|
174
|
+
|
|
175
|
+
start_time = time.time()
|
|
176
|
+
while True:
|
|
177
|
+
instance = self.get_by_id(id)
|
|
178
|
+
if instance.status != InstanceStatus.ORDERED:
|
|
179
|
+
return instance
|
|
180
|
+
|
|
181
|
+
if time.time() - start_time > MAX_WAIT_TIME:
|
|
182
|
+
raise TimeoutError(
|
|
183
|
+
f"Instance {id} did not enter provisioning state within {MAX_WAIT_TIME} seconds")
|
|
184
|
+
|
|
185
|
+
time.sleep(POLL_INTERVAL)
|
|
444
186
|
|
|
445
187
|
def action(self, id_list: Union[List[str], str], action: str, volume_ids: Optional[List[str]] = None) -> None:
|
|
446
|
-
"""Performs an action on
|
|
447
|
-
|
|
448
|
-
:
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
:
|
|
188
|
+
"""Performs an action on one or more instances.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
id_list: Single instance ID or list of instance IDs to act upon.
|
|
192
|
+
action: Action to perform on the instances.
|
|
193
|
+
volume_ids: Optional list of volume IDs to delete.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
HTTPError: If the action fails or other API error occurs.
|
|
454
197
|
"""
|
|
455
198
|
if type(id_list) is str:
|
|
456
199
|
id_list = [id_list]
|
|
@@ -464,34 +207,31 @@ class InstancesService:
|
|
|
464
207
|
self._http_client.put(INSTANCES_ENDPOINT, json=payload)
|
|
465
208
|
return
|
|
466
209
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
:
|
|
476
|
-
|
|
477
|
-
:return: True if available to deploy, False otherwise
|
|
478
|
-
:rtype: bool
|
|
210
|
+
def is_available(self, instance_type: str, is_spot: bool = False, location_code: Optional[str] = None) -> bool:
|
|
211
|
+
"""Checks if a specific instance type is available for deployment.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
instance_type: Type of instance to check availability for.
|
|
215
|
+
is_spot: Whether to check spot instance availability.
|
|
216
|
+
location_code: Optional datacenter location code.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
True if the instance type is available, False otherwise.
|
|
479
220
|
"""
|
|
480
221
|
is_spot = str(is_spot).lower()
|
|
481
222
|
query_params = {'isSpot': is_spot, 'location_code': location_code}
|
|
482
223
|
url = f'/instance-availability/{instance_type}'
|
|
483
224
|
return self._http_client.get(url, query_params).json()
|
|
484
225
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
226
|
+
def get_availabilities(self, is_spot: Optional[bool] = None, location_code: Optional[str] = None) -> List[Dict]:
|
|
227
|
+
"""Retrieves a list of available instance types across locations.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
is_spot: Optional flag to filter spot instance availability.
|
|
231
|
+
location_code: Optional datacenter location code to filter by.
|
|
488
232
|
|
|
489
|
-
:
|
|
490
|
-
|
|
491
|
-
:param location_code: datacenter location, defaults to "FIN-01"
|
|
492
|
-
:type location_code: str, optional
|
|
493
|
-
:return: list of available instance types in every location
|
|
494
|
-
:rtype: list
|
|
233
|
+
Returns:
|
|
234
|
+
List of available instance types and their details.
|
|
495
235
|
"""
|
|
496
236
|
is_spot = str(is_spot).lower() if is_spot is not None else None
|
|
497
237
|
query_params = {'isSpot': is_spot, 'locationCode': location_code}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
datacrunch/__init__.py,sha256=OG-5Avmuq3NXyBs_66GMwyzscUi0c-T6vWW5sRIfnZg,51
|
|
2
|
-
datacrunch/__version__.py,sha256=
|
|
3
|
-
datacrunch/constants.py,sha256=
|
|
2
|
+
datacrunch/__version__.py,sha256=o_CvBCGiCiaIWFGapX_FcRb3zCO9gmamKVsYWQWdAmY,19
|
|
3
|
+
datacrunch/constants.py,sha256=i0jCX91H2lKp1Uvk4GDsaTeXk0WmjyeSGpMfPs69BB4,2378
|
|
4
4
|
datacrunch/datacrunch.py,sha256=2IqrTY39sLuwtuQ_QP3jCI1d5AaCwriYgAUEFoZZzPU,3488
|
|
5
5
|
datacrunch/exceptions.py,sha256=uOP_YU2HEUi_mcMxQ9WYrIjqWUuUrwdube-RdL1C4Ps,781
|
|
6
6
|
datacrunch/helpers.py,sha256=Eq5htNxpJUCJG9D6QxbnWwch3ppmi2lfi-rFCGXf3fs,634
|
|
7
7
|
datacrunch/InferenceClient/__init__.py,sha256=oe7RQfoKbKJnIFWOt6TNtdzjXk5Gcl4-smO5DjLE6M0,117
|
|
8
|
-
datacrunch/InferenceClient/inference_client.py,sha256=
|
|
8
|
+
datacrunch/InferenceClient/inference_client.py,sha256=CM-ijRQuvaSf8BSbltkeocaIBvsAoBKT7xxnOT1q-Xs,15555
|
|
9
9
|
datacrunch/authentication/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
datacrunch/authentication/authentication.py,sha256=CThTxA99jseh7TKIdUR1M9RErIJoXvTB8CbF1VGFPCE,3589
|
|
11
11
|
datacrunch/balance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
datacrunch/balance/balance.py,sha256=rkqqXC3MLVxk6ym9Hlp9tsLbLWJculIn8q3BYbsme28,1240
|
|
13
13
|
datacrunch/containers/__init__.py,sha256=T9ROCN-a3rQfboTk3mol4OUhi6FMo5wUqahJZOBg0uw,675
|
|
14
|
-
datacrunch/containers/containers.py,sha256=
|
|
14
|
+
datacrunch/containers/containers.py,sha256=xGDlHi8nAbYZyP039_VbNba_Z0Mk-RI1N3XlHISM3Ao,33167
|
|
15
15
|
datacrunch/http_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
datacrunch/http_client/http_client.py,sha256=tmpVd3p7-NAIaTM4E13inFZWUetdVEFZnRE38p5eVk0,8285
|
|
17
17
|
datacrunch/images/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -19,7 +19,7 @@ datacrunch/images/images.py,sha256=hCAtSzozHcAAJ_UZOvnAbQSEU7BfCuixpIsmcd2RM2k,2
|
|
|
19
19
|
datacrunch/instance_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
datacrunch/instance_types/instance_types.py,sha256=NLkUI6UdfXg-zDkMu9j9RzVISLG8jdABhT_R7XpfBdA,5289
|
|
21
21
|
datacrunch/instances/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
datacrunch/instances/instances.py,sha256=
|
|
22
|
+
datacrunch/instances/instances.py,sha256=GKPpZshn5JBsduR5zyT57fonB0GDdOLkPfPmGqgJQow,9239
|
|
23
23
|
datacrunch/locations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
datacrunch/locations/locations.py,sha256=2f1OF2ObNaqGam_Mm0Btie1GymnAI9UzXulhqSSm7zo,404
|
|
25
25
|
datacrunch/ssh_keys/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -30,7 +30,7 @@ datacrunch/volume_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
|
|
|
30
30
|
datacrunch/volume_types/volume_types.py,sha256=CNJ8kfd_nxmF99x-UAJeku-uN4Gdh-yg15Aa8WGLgWU,1828
|
|
31
31
|
datacrunch/volumes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
32
|
datacrunch/volumes/volumes.py,sha256=aAH4UIVG-7NehjHu-a_4MGSdZ1jmeApV-kKh-X6TB-s,11908
|
|
33
|
-
datacrunch-1.
|
|
33
|
+
datacrunch-1.12.0.dist-info/licenses/LICENSE,sha256=LkdhbR2MArjDfV8M0dySL5mG_kfzxF2ntMgbJvWGyUQ,1069
|
|
34
34
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
35
|
tests/integration_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
36
|
tests/integration_tests/conftest.py,sha256=PWf6K1G3NoddebmDIy_Pk02dHQrEKfrNxpWwqE8Eqrk,546
|
|
@@ -46,7 +46,7 @@ tests/unit_tests/authentication/test_authentication.py,sha256=P84VnD9utk8y3ZPhUf
|
|
|
46
46
|
tests/unit_tests/balance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
47
|
tests/unit_tests/balance/test_balance.py,sha256=Cojbjd7wc9-8eRQb_fR0xLXEX7fGqobdQICH3O7WAx4,651
|
|
48
48
|
tests/unit_tests/containers/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
|
|
49
|
-
tests/unit_tests/containers/test_containers.py,sha256=
|
|
49
|
+
tests/unit_tests/containers/test_containers.py,sha256=lr7Thrpl5hC6iBG91YjBUEpkv6t6CVnXjIzkXmvXg0U,31011
|
|
50
50
|
tests/unit_tests/http_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
51
|
tests/unit_tests/http_client/test_http_client.py,sha256=JfEy7pADx0gS9KNNwVLVeG-bG4DRRXxze4dQkP_WIvw,6776
|
|
52
52
|
tests/unit_tests/images/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -63,7 +63,7 @@ tests/unit_tests/volume_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
|
63
63
|
tests/unit_tests/volume_types/test_volume_types.py,sha256=vGuC3dWjhQLD8bTYgw_we3dZ6vlUKRmKZbb9yCfhe0w,1386
|
|
64
64
|
tests/unit_tests/volumes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
65
|
tests/unit_tests/volumes/test_volumes.py,sha256=p53eSIHddWKL7U9oLLTnxo849LrJSoi6A5lpWF6ydHs,20672
|
|
66
|
-
datacrunch-1.
|
|
67
|
-
datacrunch-1.
|
|
68
|
-
datacrunch-1.
|
|
69
|
-
datacrunch-1.
|
|
66
|
+
datacrunch-1.12.0.dist-info/METADATA,sha256=EoUDXb19WnnqRmbjakFTxgSlC_BAThYyCWLMfHuqz3I,6261
|
|
67
|
+
datacrunch-1.12.0.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
|
68
|
+
datacrunch-1.12.0.dist-info/top_level.txt,sha256=FvH4EZJkbUxNm-aKx0RjmWwnduAMpfRT13Fo123i7yE,17
|
|
69
|
+
datacrunch-1.12.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|