anatools 5.1.28__py3-none-any.whl → 6.0.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.
- anatools/__init__.py +1 -1
- anatools/anaclient/anaclient.py +16 -15
- anatools/anaclient/api/api.py +2 -1
- anatools/anaclient/api/handlers.py +1 -1
- anatools/anaclient/channels.py +50 -25
- anatools/anaclient/datasets.py +33 -6
- anatools/anaclient/helpers.py +11 -10
- anatools/anaclient/services.py +46 -20
- anatools/anaclient/volumes.py +19 -18
- {anatools-5.1.28.data → anatools-6.0.0.data}/scripts/anadeploy +2 -0
- anatools-6.0.0.data/scripts/renderedai +3001 -0
- {anatools-5.1.28.dist-info → anatools-6.0.0.dist-info}/METADATA +1 -1
- {anatools-5.1.28.dist-info → anatools-6.0.0.dist-info}/RECORD +24 -23
- {anatools-5.1.28.dist-info → anatools-6.0.0.dist-info}/WHEEL +1 -1
- {anatools-5.1.28.data → anatools-6.0.0.data}/scripts/ana +0 -0
- {anatools-5.1.28.data → anatools-6.0.0.data}/scripts/anamount +0 -0
- {anatools-5.1.28.data → anatools-6.0.0.data}/scripts/anaprofile +0 -0
- {anatools-5.1.28.data → anatools-6.0.0.data}/scripts/anarules +0 -0
- {anatools-5.1.28.data → anatools-6.0.0.data}/scripts/anaserver +0 -0
- {anatools-5.1.28.data → anatools-6.0.0.data}/scripts/anatransfer +0 -0
- {anatools-5.1.28.data → anatools-6.0.0.data}/scripts/anautils +0 -0
- {anatools-5.1.28.dist-info → anatools-6.0.0.dist-info}/entry_points.txt +0 -0
- {anatools-5.1.28.dist-info → anatools-6.0.0.dist-info}/licenses/LICENSE +0 -0
- {anatools-5.1.28.dist-info → anatools-6.0.0.dist-info}/top_level.txt +0 -0
anatools/__init__.py
CHANGED
anatools/anaclient/anaclient.py
CHANGED
|
@@ -93,7 +93,7 @@ class client:
|
|
|
93
93
|
if self.__bearer_token:
|
|
94
94
|
initial_headers = {'Authorization': f'Bearer {self.__bearer_token}'}
|
|
95
95
|
|
|
96
|
-
self.ana_api = api(self.__url, self.__status_url, initial_headers, self.verbose)
|
|
96
|
+
self.ana_api = api(self.__url, self.__status_url, initial_headers, self.verbose, self.interactive)
|
|
97
97
|
|
|
98
98
|
# initialize client variables
|
|
99
99
|
self.__logout = False
|
|
@@ -188,15 +188,15 @@ class client:
|
|
|
188
188
|
if self.interactive: print_color(f'Failed to login to {self.__environment} with email {self.__email}.', 'ff0000')
|
|
189
189
|
self.user = None
|
|
190
190
|
except requests.exceptions.ConnectionError as e:
|
|
191
|
-
print_color(f'Could not connect to API to login. Try again or contact support@rendered.ai for assistance.', 'ff0000')
|
|
191
|
+
if self.interactive: print_color(f'Could not connect to API to login. Try again or contact support@rendered.ai for assistance.', 'ff0000')
|
|
192
192
|
raise AuthFailedError()
|
|
193
193
|
except requests.exceptions.JSONDecodeError as e:
|
|
194
|
-
print_color(f'Failed to login with email {self.__email} and endpoint {self.__url}. Please confirm this is the correct email and endpoint, contact support@rendered.ai for assistance.', 'ff0000')
|
|
194
|
+
if self.interactive: print_color(f'Failed to login with email {self.__email} and endpoint {self.__url}. Please confirm this is the correct email and endpoint, contact support@rendered.ai for assistance.', 'ff0000')
|
|
195
195
|
raise AuthFailedError()
|
|
196
|
-
self.ana_api = api(self.__url, self.__status_url, {'uid':self.user['uid'], 'Authorization': f'Bearer {self.user["idtoken"]}'}, self.verbose)
|
|
196
|
+
self.ana_api = api(self.__url, self.__status_url, {'uid':self.user['uid'], 'Authorization': f'Bearer {self.user["idtoken"]}'}, self.verbose, self.interactive)
|
|
197
197
|
try:
|
|
198
198
|
response = self.ana_api.getSDKCompatibility()
|
|
199
|
-
if response['version'] != anatools.__version__: print_color(response['message'], 'ff0000')
|
|
199
|
+
if self.interactive and response['version'] != anatools.__version__: print_color(response['message'], 'ff0000')
|
|
200
200
|
except:
|
|
201
201
|
if self.verbose: print_color("Failed to check SDK compatibility.", 'ff0000')
|
|
202
202
|
# ask to create an api key if using email/password
|
|
@@ -301,20 +301,20 @@ class client:
|
|
|
301
301
|
import requests
|
|
302
302
|
|
|
303
303
|
try:
|
|
304
|
-
self.ana_api = api(self.__url, self.__status_url, {'apikey': self.__APIKey}, self.verbose)
|
|
304
|
+
self.ana_api = api(self.__url, self.__status_url, {'apikey': self.__APIKey}, self.verbose, self.interactive)
|
|
305
305
|
apikeydata = self.ana_api.getAPIKeyContext(apiKey=self.__APIKey)
|
|
306
306
|
if not apikeydata:
|
|
307
|
-
print_color("Invalid API Key", 'ff0000')
|
|
307
|
+
if self.interactive: print_color("Invalid API Key", 'ff0000')
|
|
308
308
|
raise AuthFailedError()
|
|
309
309
|
if apikeydata.get('expiresAt'):
|
|
310
310
|
apikey_date = datetime.strptime(apikeydata['expiresAt'], "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
311
311
|
current_date = datetime.now()
|
|
312
312
|
if apikey_date < current_date:
|
|
313
|
-
print_color(f"API Key expired at {apikey_date}", 'ff0000')
|
|
313
|
+
if self.interactive: print_color(f"API Key expired at {apikey_date}", 'ff0000')
|
|
314
314
|
raise AuthFailedError()
|
|
315
315
|
try:
|
|
316
316
|
response = self.ana_api.getSDKCompatibility()
|
|
317
|
-
if response['version'] != anatools.__version__: print_color(response['message'], 'ff0000')
|
|
317
|
+
if self.interactive and response['version'] != anatools.__version__: print_color(response['message'], 'ff0000')
|
|
318
318
|
except:
|
|
319
319
|
if self.verbose: print_color("Failed to check SDK compatibility.", 'ff0000')
|
|
320
320
|
except requests.exceptions.ConnectionError as e:
|
|
@@ -383,15 +383,15 @@ class client:
|
|
|
383
383
|
if self.user:
|
|
384
384
|
if int(time.time()) > int(self.user['expiresAt']):
|
|
385
385
|
self.user = self.ana_api.login(self.__email, self.__password)
|
|
386
|
-
self.ana_api = api(self.__url, self.__status_url, {'uid': self.user['uid'], 'Authorization': f'Bearer {self.user["idtoken"]}'}, self.verbose)
|
|
386
|
+
self.ana_api = api(self.__url, self.__status_url, {'uid': self.user['uid'], 'Authorization': f'Bearer {self.user["idtoken"]}'}, self.verbose, self.interactive)
|
|
387
387
|
try:
|
|
388
388
|
notification = self.ana_api.getSystemNotifications()
|
|
389
389
|
self.__notificationId = notification['notificationId']
|
|
390
390
|
if notification and notification['notificationId'] != self.__notificationId:
|
|
391
391
|
self.__notificationId = notification['notificationId']
|
|
392
|
-
print_color(notification['message'], 'ffff00')
|
|
392
|
+
if self.interactive: print_color(notification['message'], 'ffff00')
|
|
393
393
|
except requests.exceptions.ConnectionError as e:
|
|
394
|
-
print_color(f"Could not get notifications: {e}", 'ffff00')
|
|
394
|
+
if self.interactive: print_color(f"Could not get notifications: {e}", 'ffff00')
|
|
395
395
|
|
|
396
396
|
|
|
397
397
|
def check_logout(self):
|
|
@@ -462,8 +462,9 @@ class client:
|
|
|
462
462
|
f"code_challenge={code_challenge}&"
|
|
463
463
|
f"code_challenge_method=S256")
|
|
464
464
|
|
|
465
|
-
|
|
466
|
-
|
|
465
|
+
if self.interactive:
|
|
466
|
+
print_color("No other authentication methods found, attempting to login via your browser.", 'ffff00')
|
|
467
|
+
print_color(f"If your browser does not open automatically, please open this URL in your browser: \n{auth_url}", "91e600")
|
|
467
468
|
webbrowser.open(auth_url)
|
|
468
469
|
|
|
469
470
|
auth_code = [None]
|
|
@@ -561,7 +562,7 @@ class client:
|
|
|
561
562
|
"""
|
|
562
563
|
from anatools.lib.print import print_color
|
|
563
564
|
services = self.ana_api.getSystemStatus(serviceId)
|
|
564
|
-
if services and display:
|
|
565
|
+
if services and display and self.interactive:
|
|
565
566
|
spacing = max([len(service['serviceName']) for service in services])+4
|
|
566
567
|
print('Service Name'.ljust(spacing, ' ')+'Status')
|
|
567
568
|
for service in services:
|
anatools/anaclient/api/api.py
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
class api:
|
|
4
4
|
|
|
5
|
-
def __init__(self, url, status_url, headers, verbose=False):
|
|
5
|
+
def __init__(self, url, status_url, headers, verbose=False, interactive=True):
|
|
6
6
|
import requests
|
|
7
7
|
self.url = url
|
|
8
8
|
self.status_url = status_url
|
|
9
9
|
self.headers = headers or {}
|
|
10
10
|
self.verbose = verbose
|
|
11
|
+
self.interactive = interactive
|
|
11
12
|
self.types = None
|
|
12
13
|
self.fields = {}
|
|
13
14
|
self.session = requests.Session()
|
|
@@ -21,7 +21,7 @@ def errorhandler(self, response, call):
|
|
|
21
21
|
'token is invalid' in lower_error_message or \
|
|
22
22
|
'token expired' in lower_error_message or \
|
|
23
23
|
'authentication failed' in lower_error_message:
|
|
24
|
-
print_color("Authentication failed. The provided token may be invalid, expired, or you do not have permission for this operation.", 'ff0000')
|
|
24
|
+
if self.interactive: print_color("Authentication failed. The provided token may be invalid, expired, or you do not have permission for this operation.", 'ff0000')
|
|
25
25
|
# Optionally, raise a more specific AuthFailedError if it exists and is appropriate here
|
|
26
26
|
raise Exception(error_message)
|
|
27
27
|
else: raise Exception(f'There was an issue with the {call} API call.')
|
anatools/anaclient/channels.py
CHANGED
|
@@ -182,15 +182,40 @@ def build_channel(self, channelfile, ignore=['data/', 'output/'], verify=False):
|
|
|
182
182
|
disk_usage = shutil.disk_usage('/')
|
|
183
183
|
gb_free = disk_usage.free / (1024**3) # Convert bytes to GB
|
|
184
184
|
if self.verbose == 'debug': print(f"Disk space left: {gb_free:.3f}GB")
|
|
185
|
-
if gb_free < 20: print_color(f'\nWarning: Low disk space detected! Only {gb_free:.1f}GB available.', 'ffff00')
|
|
185
|
+
if gb_free < 20 and self.interactive: print_color(f'\nWarning: Low disk space detected! Only {gb_free:.1f}GB available.', 'ffff00')
|
|
186
186
|
docker_space_cmd = "docker system df --format json"
|
|
187
187
|
result = subprocess.run(docker_space_cmd, shell=True, capture_output=True, text=True)
|
|
188
188
|
if result.returncode == 0:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
# Parse Docker output which may be NDJSON (newline-delimited) or concatenated JSON objects
|
|
190
|
+
# Use JSONDecoder.raw_decode() to handle both formats robustly
|
|
191
|
+
import re
|
|
192
|
+
def parse_docker_json(data):
|
|
193
|
+
results = []
|
|
194
|
+
decoder = json.JSONDecoder()
|
|
195
|
+
idx = 0
|
|
196
|
+
data = data.strip()
|
|
197
|
+
while idx < len(data):
|
|
198
|
+
obj, end_idx = decoder.raw_decode(data, idx)
|
|
199
|
+
results.append(obj)
|
|
200
|
+
idx = end_idx
|
|
201
|
+
# Skip any whitespace between objects
|
|
202
|
+
while idx < len(data) and data[idx].isspace():
|
|
203
|
+
idx += 1
|
|
204
|
+
return results
|
|
205
|
+
docker_stats = parse_docker_json(result.stdout)
|
|
206
|
+
# Parse human-readable size strings (e.g., "85.19GB", "2.087MB", "0B") to GB
|
|
207
|
+
def parse_size_to_gb(size_str):
|
|
208
|
+
if not size_str or size_str == '0B':
|
|
209
|
+
return 0.0
|
|
210
|
+
match = re.match(r'^([\d.]+)([KMGT]?B)$', size_str)
|
|
211
|
+
if not match:
|
|
212
|
+
return 0.0
|
|
213
|
+
value, unit = float(match.group(1)), match.group(2)
|
|
214
|
+
multipliers = {'B': 1/(1024**3), 'KB': 1/(1024**2), 'MB': 1/1024, 'GB': 1, 'TB': 1024}
|
|
215
|
+
return value * multipliers.get(unit, 0)
|
|
216
|
+
docker_gb = sum(parse_size_to_gb(item.get('Size', '0B')) for item in docker_stats)
|
|
192
217
|
if self.verbose == 'debug': print(f"Docker space used: {docker_gb:.3f}GB")
|
|
193
|
-
if docker_gb > 20:
|
|
218
|
+
if docker_gb > 20 and self.interactive:
|
|
194
219
|
print_color(f'\nWarning: Docker is using {docker_gb:.1f}GB of disk space!', 'ffff00')
|
|
195
220
|
print_color('Consider running "docker system prune -a" to free up space.', 'ffff00')
|
|
196
221
|
print_color('This will remove:', 'ffff00')
|
|
@@ -199,11 +224,11 @@ def build_channel(self, channelfile, ignore=['data/', 'output/'], verify=False):
|
|
|
199
224
|
print_color(' - All dangling images', 'ffff00')
|
|
200
225
|
print_color(' - All dangling build cache', 'ffff00')
|
|
201
226
|
except Exception as e:
|
|
202
|
-
print(e)
|
|
227
|
+
if self.verbose == 'debug': print(f"Docker space check failed: {e}")
|
|
203
228
|
pass
|
|
204
229
|
|
|
205
230
|
# build dockerfile
|
|
206
|
-
print('Building Channel Image...', end='', flush=True)
|
|
231
|
+
if self.interactive: print('Building Channel Image...', end='', flush=True)
|
|
207
232
|
if not os.path.isfile(channelfile): raise Exception(f'No channel file {channelfile} found.')
|
|
208
233
|
channeldir, channelfile = os.path.split(channelfile)
|
|
209
234
|
if channeldir == "": channeldir = "./"
|
|
@@ -249,10 +274,10 @@ RUN sudo /home/$USERNAME/miniconda3/envs/anatools/bin/pip install -r /ana/requir
|
|
|
249
274
|
if 'error' in output: print_color(f'{output["error"]}', 'ff0000')
|
|
250
275
|
if 'stream' in output: logfile.write(output['stream'])
|
|
251
276
|
if 'error' in output: logfile.write(output["error"])
|
|
252
|
-
print(f'\rBuilding Channel Image... [{time.time()-start:.3f}s]', end='', flush=True)
|
|
277
|
+
if self.interactive: print(f'\rBuilding Channel Image... [{time.time()-start:.3f}s]', end='', flush=True)
|
|
253
278
|
except StopIteration as e:
|
|
254
279
|
time.sleep(5)
|
|
255
|
-
print(f"\rBuilding Channel Image...done. [{time.time()-start:.3f}s]", flush=True)
|
|
280
|
+
if self.interactive: print(f"\rBuilding Channel Image...done. [{time.time()-start:.3f}s]", flush=True)
|
|
256
281
|
try:
|
|
257
282
|
dockerclient = docker.from_env()
|
|
258
283
|
dockerclient.images.get(channelfile.split('.')[0])
|
|
@@ -267,26 +292,26 @@ RUN sudo /home/$USERNAME/miniconda3/envs/anatools/bin/pip install -r /ana/requir
|
|
|
267
292
|
# if verify, check that image is valid by running anautils command
|
|
268
293
|
if verify:
|
|
269
294
|
try:
|
|
270
|
-
print(f'Verifying Channel Image...', end='', flush=True)
|
|
295
|
+
if self.interactive: print(f'Verifying Channel Image...', end='', flush=True)
|
|
271
296
|
start = time.time()
|
|
272
297
|
|
|
273
298
|
# Run anautils command in the Docker container to verify schema
|
|
274
299
|
container_name = f"verify_{channelfile.split('.')[0]}_{int(time.time())}"
|
|
275
300
|
cmd = [
|
|
276
|
-
"docker", "run", "--name", container_name,
|
|
277
|
-
channelfile.split('.')[0],
|
|
301
|
+
"docker", "run", "--name", container_name,
|
|
302
|
+
channelfile.split('.')[0],
|
|
278
303
|
"anautils", "--mode=schema", "--output=/tmp"
|
|
279
304
|
]
|
|
280
|
-
|
|
305
|
+
|
|
281
306
|
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
282
307
|
subprocess.run(["docker", "rm", container_name], capture_output=True)
|
|
283
308
|
if result.returncode != 0:
|
|
284
|
-
print_color(f"\n\n{result.stderr}\n", "ff0000")
|
|
309
|
+
if self.interactive: print_color(f"\n\n{result.stderr}\n", "ff0000")
|
|
285
310
|
raise Exception(f'Error encountered while verifying Docker image with the anautils command.')
|
|
286
|
-
|
|
287
|
-
print(f"\rVerifying Channel Image...done. [{time.time()-start:.3f}s]")
|
|
311
|
+
|
|
312
|
+
if self.interactive: print(f"\rVerifying Channel Image...done. [{time.time()-start:.3f}s]")
|
|
288
313
|
status = True
|
|
289
|
-
|
|
314
|
+
|
|
290
315
|
except Exception as e:
|
|
291
316
|
raise Exception(f'Error encountered while verifying Docker image. Please check that you can generate the schema using the anautils command.')
|
|
292
317
|
|
|
@@ -339,7 +364,7 @@ def deploy_channel(self, channelId=None, channelfile=None, image=None):
|
|
|
339
364
|
except: raise Exception('Error connecting to Docker.')
|
|
340
365
|
|
|
341
366
|
# get repository info
|
|
342
|
-
print(f"Pushing Channel Image...", end='', flush=True)
|
|
367
|
+
if self.interactive: print(f"Pushing Channel Image...", end='', flush=True)
|
|
343
368
|
dockerinfo = self.ana_api.deployChannel(channelId, image)
|
|
344
369
|
deploymentId = dockerinfo['deploymentId']
|
|
345
370
|
reponame = dockerinfo['ecrEndpoint']
|
|
@@ -363,11 +388,11 @@ def deploy_channel(self, channelId=None, channelfile=None, image=None):
|
|
|
363
388
|
progressDetail = line['progressDetail']
|
|
364
389
|
if progressDetail['total'] >= largest:
|
|
365
390
|
largest = progressDetail['total']
|
|
366
|
-
print(f"\rPushing Channel Image... [{time.time()-start:.3f}s, {min(100,round((progressDetail['current']/progressDetail['total']) * 100))}%]", end='', flush=True)
|
|
391
|
+
if self.interactive: print(f"\rPushing Channel Image... [{time.time()-start:.3f}s, {min(100,round((progressDetail['current']/progressDetail['total']) * 100))}%]", end='', flush=True)
|
|
367
392
|
logfile.write(str(line) + '\n')
|
|
368
393
|
logfile.close()
|
|
369
394
|
if self.verbose != 'debug': os.remove(os.path.join(channeldir, 'dockerpush.log'))
|
|
370
|
-
print(f"\rPushing Channel Image...done. [{time.time()-start:.3f}s] ", flush=True)
|
|
395
|
+
if self.interactive: print(f"\rPushing Channel Image...done. [{time.time()-start:.3f}s] ", flush=True)
|
|
371
396
|
|
|
372
397
|
# cleanup docker and update channels
|
|
373
398
|
dockerclient.images.remove(reponame)
|
|
@@ -396,13 +421,13 @@ def get_deployment_status(self, deploymentId, stream=False):
|
|
|
396
421
|
if deploymentId is None: raise Exception('DeploymentId must be specified.')
|
|
397
422
|
if stream:
|
|
398
423
|
data = self.ana_api.getChannelDeployment(deploymentId=deploymentId)
|
|
399
|
-
print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
424
|
+
if self.interactive: print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
400
425
|
while (data['status']['state'] not in ['Channel Deployment Complete','Channel Deployment Failed']):
|
|
401
426
|
time.sleep(10)
|
|
402
|
-
print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
427
|
+
if self.interactive: print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
403
428
|
if self.check_logout(): return
|
|
404
429
|
data = self.ana_api.getChannelDeployment(deploymentId=deploymentId)
|
|
405
|
-
print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", flush=True)
|
|
430
|
+
if self.interactive: print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", flush=True)
|
|
406
431
|
return data
|
|
407
432
|
else: return self.ana_api.getChannelDeployment(deploymentId=deploymentId)
|
|
408
433
|
|
|
@@ -457,8 +482,8 @@ def upload_channel_documentation(self, channelId, mdfile):
|
|
|
457
482
|
"X-Amz-Signature": fileinfo['fields']['signature'],
|
|
458
483
|
}
|
|
459
484
|
response = requests.post(fileinfo['url'], data=data, files=files)
|
|
460
|
-
if response.status_code != 204:
|
|
461
|
-
print(response.status_code)
|
|
485
|
+
if response.status_code != 204:
|
|
486
|
+
if self.verbose: print(response.status_code)
|
|
462
487
|
raise Exception('Failed to upload channel documentation file.')
|
|
463
488
|
return True
|
|
464
489
|
|
anatools/anaclient/datasets.py
CHANGED
|
@@ -170,28 +170,55 @@ def delete_dataset(self, datasetId, workspaceId=None):
|
|
|
170
170
|
|
|
171
171
|
def download_dataset(self, datasetId, workspaceId=None, localDir=None):
|
|
172
172
|
"""Download a dataset.
|
|
173
|
-
|
|
173
|
+
|
|
174
174
|
Parameters
|
|
175
175
|
----------
|
|
176
176
|
datasetId : str
|
|
177
177
|
Dataset ID of dataset to download.
|
|
178
178
|
workspaceId : str
|
|
179
|
-
Workspace ID that the dataset is in. If none is provided, the default workspace will get used.
|
|
179
|
+
Workspace ID that the dataset is in. If none is provided, the default workspace will get used.
|
|
180
180
|
localDir : str
|
|
181
181
|
Path for where to download the dataset. If none is provided, current working directory will be used.
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
Returns
|
|
184
184
|
-------
|
|
185
185
|
str
|
|
186
186
|
Returns the path the dataset was downloaded to.
|
|
187
|
+
|
|
188
|
+
Raises
|
|
189
|
+
------
|
|
190
|
+
ValueError
|
|
191
|
+
If datasetId is not provided.
|
|
192
|
+
Exception
|
|
193
|
+
If the dataset cannot be downloaded (e.g., still running, failed, or not found).
|
|
187
194
|
"""
|
|
188
195
|
from anatools.lib.download import download_file
|
|
189
196
|
|
|
190
197
|
self.check_logout()
|
|
191
198
|
if datasetId is None: raise ValueError("The datasetId parameter is required!")
|
|
192
|
-
if workspaceId is None: workspaceId = self.workspace
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
if workspaceId is None: workspaceId = self.workspace
|
|
200
|
+
|
|
201
|
+
# Get dataset info to provide better error messages
|
|
202
|
+
datasets = self.ana_api.getDatasets(workspaceId=workspaceId, datasetId=datasetId)
|
|
203
|
+
if not datasets:
|
|
204
|
+
raise Exception(f"Dataset with ID '{datasetId}' not found in workspace.")
|
|
205
|
+
|
|
206
|
+
dataset = datasets[0]
|
|
207
|
+
dataset_name = dataset.get('name', datasetId)
|
|
208
|
+
dataset_status = dataset.get('status', 'unknown')
|
|
209
|
+
|
|
210
|
+
url = self.ana_api.downloadDataset(workspaceId=workspaceId, datasetId=datasetId)
|
|
211
|
+
|
|
212
|
+
if url is None:
|
|
213
|
+
# Provide a helpful error message based on the dataset status
|
|
214
|
+
if dataset_status.lower() in ['running', 'pending', 'queued']:
|
|
215
|
+
raise Exception(f"Dataset '{dataset_name}' is still {dataset_status}. Please wait for the dataset to complete before downloading.")
|
|
216
|
+
elif dataset_status.lower() in ['failed', 'error', 'cancelled']:
|
|
217
|
+
raise Exception(f"Dataset '{dataset_name}' has status '{dataset_status}' and cannot be downloaded.")
|
|
218
|
+
else:
|
|
219
|
+
raise Exception(f"Dataset '{dataset_name}' is not available for download (status: {dataset_status}). The dataset may still be processing or may have failed.")
|
|
220
|
+
|
|
221
|
+
fname = dataset_name + '.zip'
|
|
195
222
|
return download_file(url=url, fname=fname, localDir=localDir)
|
|
196
223
|
|
|
197
224
|
|
anatools/anaclient/helpers.py
CHANGED
|
@@ -16,10 +16,10 @@ def generate_etag(file_path, chunk_size=128 * hashlib.md5().block_size):
|
|
|
16
16
|
raise IOError(f"Error reading file {file_path}: {e}")
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def upload_part(i, file_path, url, part_size, total_parts, progress_tracker, prefix_message, max_retries=3):
|
|
19
|
+
def upload_part(i, file_path, url, part_size, total_parts, progress_tracker, prefix_message, max_retries=3, interactive=True):
|
|
20
20
|
"""Uploads a single part, creating a new session for each request, updating progress with retry and delay."""
|
|
21
21
|
percent_complete = (progress_tracker["completed_parts"] / total_parts) * 100
|
|
22
|
-
print(f"\x1b[1K\r{prefix_message}: {percent_complete:.2f}% complete", end="", flush=True)
|
|
22
|
+
if interactive: print(f"\x1b[1K\r{prefix_message}: {percent_complete:.2f}% complete", end="", flush=True)
|
|
23
23
|
retries = 0
|
|
24
24
|
|
|
25
25
|
while retries <= max_retries:
|
|
@@ -36,21 +36,22 @@ def upload_part(i, file_path, url, part_size, total_parts, progress_tracker, pre
|
|
|
36
36
|
e_tag = response.headers["ETag"]
|
|
37
37
|
progress_tracker["completed_parts"] += 1
|
|
38
38
|
percent_complete = (progress_tracker["completed_parts"] / total_parts) * 100
|
|
39
|
-
print(f"\x1b[1K\r{prefix_message}: {percent_complete:.2f}% complete", end="", flush=True)
|
|
39
|
+
if interactive: print(f"\x1b[1K\r{prefix_message}: {percent_complete:.2f}% complete", end="", flush=True)
|
|
40
40
|
return {"partNumber": i + 1, "eTag": e_tag}
|
|
41
41
|
|
|
42
42
|
except requests.RequestException as e:
|
|
43
43
|
if retries == max_retries:
|
|
44
44
|
raise Exception(f"Failed to upload part {i+1} after {retries} attempts. Error: {e}")
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
if interactive:
|
|
47
|
+
print(f"\n\033[91m{prefix_message}: Attempt {retries + 1} failed with error", flush=True)
|
|
48
|
+
print(f"{e}. ", flush=True)
|
|
49
|
+
print("Retrying in 30 seconds...\033[0m", flush=True)
|
|
49
50
|
time.sleep(30) # Wait 30 seconds before retrying
|
|
50
51
|
retries += 1
|
|
51
52
|
|
|
52
53
|
|
|
53
|
-
def multipart_upload_file(file_path, part_size, urls, prefix_message):
|
|
54
|
+
def multipart_upload_file(file_path, part_size, urls, prefix_message, interactive=True):
|
|
54
55
|
if len(urls) > 0:
|
|
55
56
|
parts = []
|
|
56
57
|
total_parts = len(urls)
|
|
@@ -61,14 +62,14 @@ def multipart_upload_file(file_path, part_size, urls, prefix_message):
|
|
|
61
62
|
for i, url in enumerate(urls):
|
|
62
63
|
futures.append(
|
|
63
64
|
executor.submit(
|
|
64
|
-
upload_part, i, file_path, url, part_size, total_parts, progress_tracker, prefix_message
|
|
65
|
+
upload_part, i, file_path, url, part_size, total_parts, progress_tracker, prefix_message, interactive=interactive
|
|
65
66
|
)
|
|
66
67
|
)
|
|
67
68
|
|
|
68
69
|
for future in as_completed(futures):
|
|
69
70
|
parts.append(future.result())
|
|
70
71
|
|
|
71
|
-
print(f"\x1b[1K\r{prefix_message}: upload complete.", end="", flush=True)
|
|
72
|
+
if interactive: print(f"\x1b[1K\r{prefix_message}: upload complete.", end="", flush=True)
|
|
72
73
|
return parts
|
|
73
74
|
else:
|
|
74
75
|
with open(file_path, "rb") as filebytes:
|
|
@@ -77,5 +78,5 @@ def multipart_upload_file(file_path, part_size, urls, prefix_message):
|
|
|
77
78
|
response = requests.put(url, files=files)
|
|
78
79
|
if response.status_code != 204:
|
|
79
80
|
raise Exception(f"Failed to upload, with status code: {response.status_code}")
|
|
80
|
-
print(f"\x1b[1K\r{prefix_message}: upload complete.", end="", flush=True)
|
|
81
|
+
if interactive: print(f"\x1b[1K\r{prefix_message}: upload complete.", end="", flush=True)
|
|
81
82
|
return []
|
anatools/anaclient/services.py
CHANGED
|
@@ -174,15 +174,40 @@ def build_service(self, servicefile):
|
|
|
174
174
|
disk_usage = shutil.disk_usage('/')
|
|
175
175
|
gb_free = disk_usage.free / (1024**3) # Convert bytes to GB
|
|
176
176
|
if self.verbose == 'debug': print(f"Disk space left: {gb_free:.3f}GB")
|
|
177
|
-
if gb_free < 20: print_color(f'\nWarning: Low disk space detected! Only {gb_free:.1f}GB available.', 'ffff00')
|
|
177
|
+
if gb_free < 20 and self.interactive: print_color(f'\nWarning: Low disk space detected! Only {gb_free:.1f}GB available.', 'ffff00')
|
|
178
178
|
docker_space_cmd = "docker system df --format json"
|
|
179
179
|
result = subprocess.run(docker_space_cmd, shell=True, capture_output=True, text=True)
|
|
180
180
|
if result.returncode == 0:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
# Parse Docker output which may be NDJSON (newline-delimited) or concatenated JSON objects
|
|
182
|
+
# Use JSONDecoder.raw_decode() to handle both formats robustly
|
|
183
|
+
import re
|
|
184
|
+
def parse_docker_json(data):
|
|
185
|
+
results = []
|
|
186
|
+
decoder = json.JSONDecoder()
|
|
187
|
+
idx = 0
|
|
188
|
+
data = data.strip()
|
|
189
|
+
while idx < len(data):
|
|
190
|
+
obj, end_idx = decoder.raw_decode(data, idx)
|
|
191
|
+
results.append(obj)
|
|
192
|
+
idx = end_idx
|
|
193
|
+
# Skip any whitespace between objects
|
|
194
|
+
while idx < len(data) and data[idx].isspace():
|
|
195
|
+
idx += 1
|
|
196
|
+
return results
|
|
197
|
+
docker_stats = parse_docker_json(result.stdout)
|
|
198
|
+
# Parse human-readable size strings (e.g., "85.19GB", "2.087MB", "0B") to GB
|
|
199
|
+
def parse_size_to_gb(size_str):
|
|
200
|
+
if not size_str or size_str == '0B':
|
|
201
|
+
return 0.0
|
|
202
|
+
match = re.match(r'^([\d.]+)([KMGT]?B)$', size_str)
|
|
203
|
+
if not match:
|
|
204
|
+
return 0.0
|
|
205
|
+
value, unit = float(match.group(1)), match.group(2)
|
|
206
|
+
multipliers = {'B': 1/(1024**3), 'KB': 1/(1024**2), 'MB': 1/1024, 'GB': 1, 'TB': 1024}
|
|
207
|
+
return value * multipliers.get(unit, 0)
|
|
208
|
+
docker_gb = sum(parse_size_to_gb(item.get('Size', '0B')) for item in docker_stats)
|
|
184
209
|
if self.verbose == 'debug': print(f"Docker space used: {docker_gb:.3f}GB")
|
|
185
|
-
if docker_gb > 20:
|
|
210
|
+
if docker_gb > 20 and self.interactive:
|
|
186
211
|
print_color(f'\nWarning: Docker is using {docker_gb:.1f}GB of disk space!', 'ffff00')
|
|
187
212
|
print_color('Consider running "docker system prune -a" to free up space.', 'ffff00')
|
|
188
213
|
print_color('This will remove:', 'ffff00')
|
|
@@ -191,10 +216,11 @@ def build_service(self, servicefile):
|
|
|
191
216
|
print_color(' - All dangling images', 'ffff00')
|
|
192
217
|
print_color(' - All dangling build cache', 'ffff00')
|
|
193
218
|
except Exception as e:
|
|
219
|
+
if self.verbose == 'debug': print(f"Docker space check failed: {e}")
|
|
194
220
|
pass
|
|
195
221
|
|
|
196
222
|
# check for service Dockerfile
|
|
197
|
-
print('Building Service Image...', end='', flush=True)
|
|
223
|
+
if self.interactive: print('Building Service Image...', end='', flush=True)
|
|
198
224
|
if not os.path.isfile(servicefile): raise Exception(f'No service file {servicefile} found.')
|
|
199
225
|
servicedir, servicefile = os.path.split(servicefile)
|
|
200
226
|
imgtime = int(time.time())
|
|
@@ -207,12 +233,12 @@ def build_service(self, servicefile):
|
|
|
207
233
|
try:
|
|
208
234
|
start = time.time()
|
|
209
235
|
streamer = dockerclient.build(
|
|
210
|
-
path=servicedir,
|
|
211
|
-
dockerfile=os.path.join(servicedir, '.devcontainer/Dockerfile'),
|
|
212
|
-
tag=image,
|
|
213
|
-
rm=True,
|
|
214
|
-
decode=True,
|
|
215
|
-
platform='linux/amd64',
|
|
236
|
+
path=servicedir,
|
|
237
|
+
dockerfile=os.path.join(servicedir, '.devcontainer/Dockerfile'),
|
|
238
|
+
tag=image,
|
|
239
|
+
rm=True,
|
|
240
|
+
decode=True,
|
|
241
|
+
platform='linux/amd64',
|
|
216
242
|
nocache=False,
|
|
217
243
|
pull=False)
|
|
218
244
|
logfilepath = os.path.join(tempfile.gettempdir(), 'dockerbuild.log')
|
|
@@ -225,7 +251,7 @@ def build_service(self, servicefile):
|
|
|
225
251
|
if 'error' in output: print_color(f'{output["error"]}', 'ff0000')
|
|
226
252
|
if 'stream' in output: logfile.write(output['stream'])
|
|
227
253
|
if 'error' in output: logfile.write(output["error"])
|
|
228
|
-
print(f'\rBuilding Service Image... [{time.time()-start:.3f}s]', end='', flush=True)
|
|
254
|
+
if self.interactive: print(f'\rBuilding Service Image... [{time.time()-start:.3f}s]', end='', flush=True)
|
|
229
255
|
except StopIteration:
|
|
230
256
|
time.sleep(1)
|
|
231
257
|
try:
|
|
@@ -238,7 +264,7 @@ def build_service(self, servicefile):
|
|
|
238
264
|
logfile.close()
|
|
239
265
|
raise Exception(f'Error encountered while building Docker image. Please check logfile {logfilepath}.')
|
|
240
266
|
if self.verbose != 'debug' and os.path.exists(logfilepath): os.remove(logfilepath)
|
|
241
|
-
print(f"\rBuilding Service Image...done. [{time.time()-start:.3f}s]", flush=True)
|
|
267
|
+
if self.interactive: print(f"\rBuilding Service Image...done. [{time.time()-start:.3f}s]", flush=True)
|
|
242
268
|
return image
|
|
243
269
|
|
|
244
270
|
|
|
@@ -296,13 +322,13 @@ def deploy_service(self, serviceId=None, servicefile=None):
|
|
|
296
322
|
progressDetail = line['progressDetail']
|
|
297
323
|
if progressDetail['total'] >= largest:
|
|
298
324
|
largest = progressDetail['total']
|
|
299
|
-
print(f"\rPushing Service Image... [{time.time()-start:.3f}s, {min(100,round((progressDetail['current']/progressDetail['total']) * 100))}%]", end='', flush=True)
|
|
300
|
-
if 'error' in line:
|
|
325
|
+
if self.interactive: print(f"\rPushing Service Image... [{time.time()-start:.3f}s, {min(100,round((progressDetail['current']/progressDetail['total']) * 100))}%]", end='', flush=True)
|
|
326
|
+
if 'error' in line:
|
|
301
327
|
if 'HTTP 403' in line['error']: raise Exception('You do not have permission to push to this repository.')
|
|
302
328
|
else: raise Exception(line['error'])
|
|
303
329
|
logfile.close()
|
|
304
330
|
if self.verbose != 'debug' and os.path.exists(logfilepath): os.remove(logfilepath)
|
|
305
|
-
print(f"\rPushing Service Image...done. [{time.time()-start:.3f}s] ", flush=True)
|
|
331
|
+
if self.interactive: print(f"\rPushing Service Image...done. [{time.time()-start:.3f}s] ", flush=True)
|
|
306
332
|
|
|
307
333
|
# cleanup docker and update services
|
|
308
334
|
dockerclient.images.remove(reponame)
|
|
@@ -331,13 +357,13 @@ def get_service_deployment(self, deploymentId, stream=False):
|
|
|
331
357
|
if deploymentId is None: raise Exception('DeploymentId must be specified.')
|
|
332
358
|
if stream:
|
|
333
359
|
data = self.ana_api.getServiceDeployment(deploymentId=deploymentId)
|
|
334
|
-
print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
360
|
+
if self.interactive: print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
335
361
|
while (data['status']['state'] not in ['Service Deployment Complete','Service Deployment Failed']):
|
|
336
362
|
time.sleep(10)
|
|
337
|
-
print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
363
|
+
if self.interactive: print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
338
364
|
if self.check_logout(): return
|
|
339
365
|
data = self.ana_api.getServiceDeployment(deploymentId=deploymentId)
|
|
340
|
-
print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", flush=True)
|
|
366
|
+
if self.interactive: print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", flush=True)
|
|
341
367
|
return data
|
|
342
368
|
else: return self.ana_api.getServiceDeployment(deploymentId=deploymentId)
|
|
343
369
|
|
anatools/anaclient/volumes.py
CHANGED
|
@@ -274,8 +274,8 @@ def download_volume_data(self, volumeId, files=[], localDir=None, recursive=True
|
|
|
274
274
|
if self.interactive:
|
|
275
275
|
print(f"\x1b[1K\rdownload: {response[index]['key']} to {filename}", flush=True)
|
|
276
276
|
except:
|
|
277
|
-
traceback.print_exc()
|
|
278
|
-
print(f"\x1b[1K\rdownload: failed to download {response[index]['key']}", flush=True)
|
|
277
|
+
if self.verbose: traceback.print_exc()
|
|
278
|
+
if self.interactive: print(f"\x1b[1K\rdownload: failed to download {response[index]['key']}", flush=True)
|
|
279
279
|
return
|
|
280
280
|
|
|
281
281
|
|
|
@@ -330,7 +330,8 @@ def upload_volume_data(self, volumeId, files=None, localDir=None, destinationDir
|
|
|
330
330
|
if sync == True:
|
|
331
331
|
file_hash = generate_etag(filepath)
|
|
332
332
|
source_hashes.append(file + file_hash)
|
|
333
|
-
else:
|
|
333
|
+
else:
|
|
334
|
+
if self.interactive: print(f"Could not find {filepath}.")
|
|
334
335
|
else:
|
|
335
336
|
for root, dirs, files in os.walk(localDir):
|
|
336
337
|
for file in files:
|
|
@@ -371,37 +372,37 @@ def upload_volume_data(self, volumeId, files=None, localDir=None, destinationDir
|
|
|
371
372
|
delete_files.append(destination_file)
|
|
372
373
|
|
|
373
374
|
if (len(delete_files)):
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
375
|
+
if self.interactive:
|
|
376
|
+
print(f"The following files will be deleted:", end='\n', flush=True)
|
|
377
|
+
for file in delete_files:
|
|
378
|
+
print(f" {file}", end='\n', flush=True)
|
|
379
|
+
answer = input("Delete these files [Y/n]: ")
|
|
380
|
+
if answer.lower() == "y":
|
|
381
|
+
self.refresh_token()
|
|
382
|
+
self.ana_api.deleteVolumeData(volumeId=volumeId, keys=delete_files)
|
|
381
383
|
|
|
382
384
|
for index, file in enumerate(source_files):
|
|
383
385
|
destination_key = (destinationDir or '') + file
|
|
384
|
-
print(f"\x1b[1K\rUploading {file} to the volume [{index+1} / {len(source_files)}]", end='\n' if self.verbose else '', flush=True)
|
|
386
|
+
if self.interactive: print(f"\x1b[1K\rUploading {file} to the volume [{index+1} / {len(source_files)}]", end='\n' if self.verbose else '', flush=True)
|
|
385
387
|
if (sync == True and (source_hashes[index] in destination_hashes)):
|
|
386
|
-
print(f"\x1b[1K\rsync: {file}'s hash exists", end='\n' if self.verbose else '', flush=True)
|
|
388
|
+
if self.interactive: print(f"\x1b[1K\rsync: {file}'s hash exists", end='\n' if self.verbose else '', flush=True)
|
|
387
389
|
elif sync == False or (source_hashes[index] not in destination_hashes):
|
|
388
390
|
try:
|
|
389
391
|
self.refresh_token()
|
|
390
392
|
filepath = os.path.join(localDir, file)
|
|
391
393
|
filesize = os.path.getsize(filepath)
|
|
392
394
|
fileinfo = self.ana_api.uploadVolumeData(volumeId=volumeId, key=destination_key, size=filesize)
|
|
393
|
-
|
|
394
|
-
parts = multipart_upload_file(filepath, int(fileinfo["partSize"]), fileinfo["urls"], f"Uploading {file} to the volume [{index+1} / {len(source_files)}]")
|
|
395
|
+
parts = multipart_upload_file(filepath, int(fileinfo["partSize"]), fileinfo["urls"], f"Uploading {file} to the volume [{index+1} / {len(source_files)}]", interactive=self.interactive)
|
|
395
396
|
self.refresh_token()
|
|
396
397
|
finalize_success = self.ana_api.uploadVolumeDataFinalizer(uploadId=fileinfo['uploadId'], key=fileinfo['key'], parts=parts)
|
|
397
398
|
if not finalize_success:
|
|
398
399
|
faileduploads.append(file)
|
|
399
400
|
except:
|
|
400
|
-
traceback.print_exc()
|
|
401
|
+
if self.verbose: traceback.print_exc()
|
|
401
402
|
faileduploads.append(file)
|
|
402
|
-
print(f"\x1b[1K\rupload: {file} failed", end='\n' if self.verbose else '', flush=True)
|
|
403
|
-
print("\x1b[1K\rUploading files completed.", flush=True)
|
|
404
|
-
if len(faileduploads): print('The following files failed to upload:', faileduploads, flush=True)
|
|
403
|
+
if self.interactive: print(f"\x1b[1K\rupload: {file} failed", end='\n' if self.verbose else '', flush=True)
|
|
404
|
+
if self.interactive: print("\x1b[1K\rUploading files completed.", flush=True)
|
|
405
|
+
if len(faileduploads) and self.interactive: print('The following files failed to upload:', faileduploads, flush=True)
|
|
405
406
|
return
|
|
406
407
|
|
|
407
408
|
|