fusion-platform-python-sdk 2.2.3__py3-none-any.whl → 2.3.8__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.
- fusion_platform/__init__.py +6 -3
- fusion_platform/base.py +2 -3
- fusion_platform/command.py +969 -296
- fusion_platform/common/raise_thread.py +3 -2
- fusion_platform/models/data_file.py +35 -3
- fusion_platform/models/model.py +13 -2
- fusion_platform/models/process.py +12 -7
- fusion_platform/models/process_service_execution.md +1 -0
- fusion_platform/models/process_service_execution.py +1 -0
- fusion_platform/translations.py +181 -39
- {fusion_platform_python_sdk-2.2.3.dist-info → fusion_platform_python_sdk-2.3.8.dist-info}/METADATA +2 -1
- {fusion_platform_python_sdk-2.2.3.dist-info → fusion_platform_python_sdk-2.3.8.dist-info}/RECORD +16 -16
- {fusion_platform_python_sdk-2.2.3.dist-info → fusion_platform_python_sdk-2.3.8.dist-info}/WHEEL +1 -1
- {fusion_platform_python_sdk-2.2.3.dist-info → fusion_platform_python_sdk-2.3.8.dist-info}/entry_points.txt +0 -0
- {fusion_platform_python_sdk-2.2.3.dist-info → fusion_platform_python_sdk-2.3.8.dist-info}/licenses/LICENSE.txt +0 -0
- {fusion_platform_python_sdk-2.2.3.dist-info → fusion_platform_python_sdk-2.3.8.dist-info}/top_level.txt +0 -0
fusion_platform/command.py
CHANGED
|
@@ -12,24 +12,25 @@ from collections import defaultdict
|
|
|
12
12
|
from copy import deepcopy
|
|
13
13
|
import csv
|
|
14
14
|
from datetime import datetime, timedelta, timezone
|
|
15
|
+
from dateutil.relativedelta import relativedelta
|
|
15
16
|
from decimal import Decimal
|
|
16
17
|
from functools import partial
|
|
18
|
+
import i18n
|
|
17
19
|
import json
|
|
20
|
+
import keyring
|
|
18
21
|
import logging
|
|
22
|
+
import math
|
|
19
23
|
import os
|
|
20
|
-
import re
|
|
21
|
-
import shutil
|
|
22
|
-
import tempfile
|
|
23
|
-
from time import sleep
|
|
24
|
-
import uuid
|
|
25
|
-
|
|
26
|
-
from dateutil.relativedelta import relativedelta
|
|
27
|
-
import i18n
|
|
28
24
|
from pathos.multiprocessing import ThreadPool as Pool
|
|
29
25
|
from prompt_toolkit import print_formatted_text as print, prompt
|
|
30
26
|
from prompt_toolkit.completion import WordCompleter
|
|
31
27
|
from prompt_toolkit.validation import Validator
|
|
28
|
+
import re
|
|
29
|
+
import shutil
|
|
30
|
+
import tempfile
|
|
32
31
|
from tqdm import tqdm
|
|
32
|
+
from time import sleep
|
|
33
|
+
import uuid
|
|
33
34
|
import yaml
|
|
34
35
|
|
|
35
36
|
# Make sure localisation is set up before any SDK-specific imports.
|
|
@@ -39,8 +40,9 @@ Localise.setup()
|
|
|
39
40
|
|
|
40
41
|
import fusion_platform
|
|
41
42
|
from fusion_platform.base import Base
|
|
42
|
-
from fusion_platform.common.utilities import json_default, string_blank
|
|
43
|
+
from fusion_platform.common.utilities import dict_nested_get, json_default, string_blank
|
|
43
44
|
from fusion_platform.models.data import Data
|
|
45
|
+
from fusion_platform.models.data_file import DataFile
|
|
44
46
|
from fusion_platform.models.model import Model
|
|
45
47
|
from fusion_platform.session import RequestError
|
|
46
48
|
|
|
@@ -84,10 +86,38 @@ class Command(Base):
|
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
_KEYRING_DEPLOYMENT_FORMAT = f"{fusion_platform.FUSION_PLATFORM}.{{deployment}}"
|
|
90
|
+
|
|
87
91
|
# Useful download constants.
|
|
88
92
|
_DOWNLOAD_INPUT = 'input'
|
|
89
93
|
_DOWNLOAD_OUTPUT = 'output'
|
|
90
94
|
_DOWNLOAD_STORAGE = 'storage'
|
|
95
|
+
_DOWNLOAD_STATISTICS = 'statistics'
|
|
96
|
+
_DOWNLOAD_UPLOAD = 'upload'
|
|
97
|
+
|
|
98
|
+
# Process execution exit types.
|
|
99
|
+
_EXIT_SUCCESS = 'success'
|
|
100
|
+
_EXIT_FATAL_ERROR = 'fatal error'
|
|
101
|
+
_EXIT_FATAL_WARNING = 'fatal warning'
|
|
102
|
+
_EXIT_NON_FATAL_WARNING = 'non-fatal warning'
|
|
103
|
+
|
|
104
|
+
# File types to extensions.
|
|
105
|
+
_FILE_TYPES_TO_EXTENSIONS = {
|
|
106
|
+
fusion_platform.FILE_TYPE_GEOTIFF: ['.tif', '.tiff'],
|
|
107
|
+
fusion_platform.FILE_TYPE_JPEG2000: ['.jp2'],
|
|
108
|
+
fusion_platform.FILE_TYPE_DEM: ['.dem'],
|
|
109
|
+
fusion_platform.FILE_TYPE_GEOJSON: ['.json', '.geojson'],
|
|
110
|
+
fusion_platform.FILE_TYPE_KML: ['.kml'],
|
|
111
|
+
fusion_platform.FILE_TYPE_KMZ: ['.kmz'],
|
|
112
|
+
fusion_platform.FILE_TYPE_CSV: ['.csv'],
|
|
113
|
+
fusion_platform.FILE_TYPE_ESRI_SHAPEFILE: ['.zip'],
|
|
114
|
+
fusion_platform.FILE_TYPE_JPEG: ['.jpeg', '.jpg'],
|
|
115
|
+
fusion_platform.FILE_TYPE_PNG: ['.png'],
|
|
116
|
+
fusion_platform.FILE_TYPE_PDF: ['.pdf'],
|
|
117
|
+
fusion_platform.FILE_TYPE_GZIP: ['.gz'],
|
|
118
|
+
fusion_platform.FILE_TYPE_ZIP: ['.zip']
|
|
119
|
+
# Not "Other" as it has no associated extensions.
|
|
120
|
+
}
|
|
91
121
|
|
|
92
122
|
# Metrics used during download.
|
|
93
123
|
_METRICS_FILE = 'metrics.csv'
|
|
@@ -130,8 +160,17 @@ class Command(Base):
|
|
|
130
160
|
|
|
131
161
|
# Progress bar constants.
|
|
132
162
|
__PROGRESS_BAR_FORMAT = '{l_bar}{bar:50}{r_bar}'
|
|
163
|
+
__PROGRESS_BAR_FORMAT_PROCESS = '{{percentage:3.0f}}%|{{bar:50}}| {current:>3}/{total:>3} (S {successes:>3}|W {warnings:>3}|E {errors:>3}|X {stopped:>3}), {process_status:>7}: {description}'
|
|
133
164
|
__PROGRESS_BAR_COLOUR_ACTIVE = '#3379B6'
|
|
134
165
|
__PROGRESS_BAR_COLOUR_COMPLETE = '#5BB85C'
|
|
166
|
+
__PROGRESS_BAR_COLOUR_WARNING = '#F09E22'
|
|
167
|
+
__PROGRESS_BAR_COLOUR_ERROR = '#D13B32'
|
|
168
|
+
|
|
169
|
+
# Error messages.
|
|
170
|
+
__DATA_ITEM_STILL_IN_USE = 'still in use'
|
|
171
|
+
|
|
172
|
+
# File constants.
|
|
173
|
+
__FILE_SIZE_UNIT_LIST = list(zip(['B ', 'KB', 'MB', 'GB', 'TB', 'PB'], [0, 0, 1, 2, 2, 2]))
|
|
135
174
|
|
|
136
175
|
def __init__(self):
|
|
137
176
|
"""
|
|
@@ -152,113 +191,29 @@ class Command(Base):
|
|
|
152
191
|
# Set the default print level.
|
|
153
192
|
self.__print_level = logging.INFO
|
|
154
193
|
|
|
155
|
-
def
|
|
194
|
+
def __bytes_to_human(self, size):
|
|
156
195
|
"""
|
|
157
|
-
|
|
196
|
+
Converts a file size into a human-readable string. Based on https://gist.github.com/ElectroDrome/365347f9d71154a517d71b3655a768f7.
|
|
158
197
|
|
|
159
198
|
Args:
|
|
160
|
-
|
|
161
|
-
process_name: The name of the process.
|
|
162
|
-
options: The list of options.
|
|
163
|
-
dispatchers: The list of dispatchers.
|
|
164
|
-
service: The base service to create the process from.
|
|
165
|
-
data_items: The list of data items to be used as inputs.
|
|
199
|
+
size: The size in bytes.
|
|
166
200
|
|
|
167
201
|
Returns:
|
|
168
|
-
The
|
|
202
|
+
The human-readable version of the size.
|
|
169
203
|
"""
|
|
204
|
+
human = '0 B '
|
|
170
205
|
|
|
171
|
-
|
|
172
|
-
|
|
206
|
+
if size == 1:
|
|
207
|
+
human = '1 B '
|
|
173
208
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
209
|
+
elif size > 1:
|
|
210
|
+
exponent = min(int(math.log(size, 1024)), len(Command.__FILE_SIZE_UNIT_LIST) - 1)
|
|
211
|
+
quotient = float(size) / (1024 ** exponent)
|
|
212
|
+
unit, number_of_decimal_places = Command.__FILE_SIZE_UNIT_LIST[exponent]
|
|
213
|
+
format_string = '{:.%sf} {}' % number_of_decimal_places
|
|
214
|
+
human = format_string.format(quotient, unit)
|
|
177
215
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
for key, value in options.items():
|
|
181
|
-
if key in Command._MODEL_FIELDS:
|
|
182
|
-
kwargs[key] = value
|
|
183
|
-
else:
|
|
184
|
-
process.update(option_name=key, value=value, coerce_value=True)
|
|
185
|
-
|
|
186
|
-
if len(kwargs) > 0:
|
|
187
|
-
process.update(**kwargs)
|
|
188
|
-
|
|
189
|
-
# Add in optional dispatchers.
|
|
190
|
-
dispatchers_added = 0
|
|
191
|
-
|
|
192
|
-
for i, dispatcher in enumerate(process.available_dispatchers):
|
|
193
|
-
finished = False
|
|
194
|
-
|
|
195
|
-
while not finished:
|
|
196
|
-
# Either find the dispatchers from what has been loaded from the YAML file or prompt the user.
|
|
197
|
-
add = False
|
|
198
|
-
dispatch_intermediate = False
|
|
199
|
-
dispatcher_options = {}
|
|
200
|
-
|
|
201
|
-
if dispatchers is not None:
|
|
202
|
-
# See if any of the remaining dispatchers match this one. Once used, the corresponding dispatcher is marked as used.
|
|
203
|
-
filtered_dispatchers = [item for item in dispatchers if
|
|
204
|
-
(item.get(Model._FIELD_NAME) == dispatcher.name) and (not item.get(Model._FIELD_USED, False))]
|
|
205
|
-
|
|
206
|
-
if len(filtered_dispatchers) > 0:
|
|
207
|
-
selected_dispatcher = filtered_dispatchers[0]
|
|
208
|
-
selected_dispatcher[Model._FIELD_USED] = True
|
|
209
|
-
add = True
|
|
210
|
-
dispatch_intermediate = selected_dispatcher.get(Model._FIELD_DISPATCH_INTERMEDIATE) if selected_dispatcher.get(
|
|
211
|
-
Model._FIELD_DISPATCH_INTERMEDIATE) is not None else False
|
|
212
|
-
dispatcher_options = selected_dispatcher.get(Model._FIELD_OPTIONS) if selected_dispatcher.get(Model._FIELD_OPTIONS) is not None else {}
|
|
213
|
-
|
|
214
|
-
else:
|
|
215
|
-
response = self.__get_input(i18n.t('command.add_dispatcher', dispatcher=dispatcher.name),
|
|
216
|
-
constrained_values=[i18n.t('command.prompt_no'), i18n.t('command.prompt_yes')])
|
|
217
|
-
|
|
218
|
-
if (response is not None) and response.lower().startswith(i18n.t('command.prompt_yes').lower()[0]):
|
|
219
|
-
add = True
|
|
220
|
-
|
|
221
|
-
if hasattr(dispatcher, Model._FIELD_DISPATCH_INTERMEDIATE):
|
|
222
|
-
response = self.__get_input(i18n.t('command.dispatch_intermediate'),
|
|
223
|
-
constrained_values=[i18n.t('command.prompt_no'), i18n.t('command.prompt_yes')])
|
|
224
|
-
|
|
225
|
-
if (response is not None) and response.lower().startswith(i18n.t('command.prompt_yes').lower()[0]):
|
|
226
|
-
dispatch_intermediate = True
|
|
227
|
-
|
|
228
|
-
for option in dispatcher.options:
|
|
229
|
-
finished_options = False
|
|
230
|
-
option_value = None
|
|
231
|
-
|
|
232
|
-
while not finished_options:
|
|
233
|
-
constrained_values = option.constrained_values if option.data_type == fusion_platform.DATA_TYPE_CONSTRAINED else None
|
|
234
|
-
|
|
235
|
-
prompt_constrained_values = i18n.t('models.process.option.representation_constrained_values',
|
|
236
|
-
constrained_values=constrained_values) if constrained_values is not None else ''
|
|
237
|
-
prompt_required = i18n.t('models.process.option.representation_required') if option.required else ''
|
|
238
|
-
prompt = i18n.t('command.prompt_option', title=option.title, name=option.name, data_type=option.data_type,
|
|
239
|
-
constrained_values=prompt_constrained_values, required=prompt_required)
|
|
240
|
-
|
|
241
|
-
option_value = self.__get_input(prompt, default=option.value, constrained_values=constrained_values)
|
|
242
|
-
finished_options = (not option.required) or (
|
|
243
|
-
option.required and (option_value is not None) and (not string_blank(str(option_value))))
|
|
244
|
-
|
|
245
|
-
if option_value is not None:
|
|
246
|
-
dispatcher_options[option.name] = option_value
|
|
247
|
-
|
|
248
|
-
if add:
|
|
249
|
-
process.add_dispatcher(number=(i + 1))
|
|
250
|
-
dispatchers_added += 1
|
|
251
|
-
|
|
252
|
-
for option_name, option_value in dispatcher_options.items():
|
|
253
|
-
process.update(dispatcher_number=dispatchers_added, option_name=option_name, value=option_value, coerce_value=True,
|
|
254
|
-
dispatch_intermediate=dispatch_intermediate)
|
|
255
|
-
else:
|
|
256
|
-
finished = True
|
|
257
|
-
|
|
258
|
-
# Create the process, which will validate its options and inputs.
|
|
259
|
-
process.create()
|
|
260
|
-
|
|
261
|
-
return process
|
|
216
|
+
return human
|
|
262
217
|
|
|
263
218
|
def __data_name_to_file_name(self, data_name):
|
|
264
219
|
"""
|
|
@@ -287,10 +242,10 @@ class Command(Base):
|
|
|
287
242
|
"""
|
|
288
243
|
self.__print(logging.INFO, i18n.t('command.define_process', process=process_name, inputs=download_inputs, storage=download_storage))
|
|
289
244
|
|
|
290
|
-
# We are defining an existing process.
|
|
245
|
+
# We are defining an existing process. This will raise an exception if the process is not found.
|
|
291
246
|
process, _ = self.get_process_or_execution(organisation, process_name)
|
|
292
247
|
|
|
293
|
-
# Get the underlying service name.
|
|
248
|
+
# Get the underlying service, if available. We only optionally use this for the name.
|
|
294
249
|
service, _ = organisation.find_services(ssd_id=process.ssd_id)
|
|
295
250
|
|
|
296
251
|
# Get the options and dispatchers, optionally add in whether intermediate services are being dispatched.
|
|
@@ -410,6 +365,109 @@ class Command(Base):
|
|
|
410
365
|
|
|
411
366
|
return process_definition
|
|
412
367
|
|
|
368
|
+
def __delete_with_confirmation(self, item, force, prompt_message, delete_message):
|
|
369
|
+
"""
|
|
370
|
+
Deletes the item with an optional confirmation prompt. The item is deleted if force is True or the user responds with Yes to the prompt.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
item: The model item to delete. This must have a #delete method.
|
|
374
|
+
force: Force deletion without prompting for confirmation?
|
|
375
|
+
prompt_message: The message to be displayed with the prompt.
|
|
376
|
+
delete_message: The message to be displayed with the deletion.
|
|
377
|
+
"""
|
|
378
|
+
delete = force
|
|
379
|
+
|
|
380
|
+
if not delete:
|
|
381
|
+
confirm = self.__get_input(prompt_message, constrained_values=[i18n.t('command.prompt_no'), i18n.t('command.prompt_yes')])
|
|
382
|
+
|
|
383
|
+
if (confirm is not None) and confirm.lower().startswith(i18n.t('command.prompt_yes').lower()[0]):
|
|
384
|
+
delete = True
|
|
385
|
+
|
|
386
|
+
if delete:
|
|
387
|
+
self.__print(logging.INFO, delete_message)
|
|
388
|
+
item.delete()
|
|
389
|
+
|
|
390
|
+
def delete_process(self, organisation, process_name, force):
|
|
391
|
+
"""
|
|
392
|
+
Deletes all processes matching the process name or id. An execution id may also be provided in which case just the execution is deleted.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
organisation: The logged in organisation.
|
|
396
|
+
process_name: The name of the process to delete.
|
|
397
|
+
force: Force deletion without prompting for confirmation?
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
True if a process or execution was deleted.
|
|
401
|
+
"""
|
|
402
|
+
attempted_delete = True
|
|
403
|
+
|
|
404
|
+
# Find the process or execution. This will raise an exception if the process is not found, which we catch.
|
|
405
|
+
process = None
|
|
406
|
+
execution = None
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
process, execution = self.get_process_or_execution(organisation, process_name)
|
|
410
|
+
except CommandError:
|
|
411
|
+
attempted_delete = False
|
|
412
|
+
|
|
413
|
+
# Delete the process or execution. If an execution id has been provided, we will have both an execution and process, hence the order below is important to
|
|
414
|
+
# make sure we do not delete the process as well if just the execution is to be deleted.
|
|
415
|
+
if (process is not None) and (execution is not None):
|
|
416
|
+
self.__delete_with_confirmation(
|
|
417
|
+
execution,
|
|
418
|
+
force,
|
|
419
|
+
i18n.t('command.delete_execution_confirmation', execution=process_name, process=process.name),
|
|
420
|
+
i18n.t('command.delete_execution', execution=process_name, process=process.name)
|
|
421
|
+
)
|
|
422
|
+
elif process is not None:
|
|
423
|
+
self.__delete_with_confirmation(
|
|
424
|
+
process,
|
|
425
|
+
force,
|
|
426
|
+
i18n.t('command.delete_process_confirmation', process=process.name),
|
|
427
|
+
i18n.t('command.delete_process', process=process.name)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
return attempted_delete
|
|
431
|
+
|
|
432
|
+
def delete_upload(self, organisation, upload_name, force):
|
|
433
|
+
"""
|
|
434
|
+
Deletes all uploads matching the upload name or id.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
organisation: The logged in organisation.
|
|
438
|
+
upload_name: The name of the upload to delete.
|
|
439
|
+
force: Force deletion without prompting for confirmation?
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
True if an upload was deleted.
|
|
443
|
+
"""
|
|
444
|
+
attempted_delete = True
|
|
445
|
+
|
|
446
|
+
# Find the data item. This will raise an exception if the data item is not found, which we catch.
|
|
447
|
+
data_item = None
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
data_item = self.get_data(organisation, upload_name)
|
|
451
|
+
except CommandError:
|
|
452
|
+
attempted_delete = False
|
|
453
|
+
|
|
454
|
+
# Delete the data item. We catch any in use error to display a more friendly message.
|
|
455
|
+
if data_item is not None:
|
|
456
|
+
try:
|
|
457
|
+
self.__delete_with_confirmation(
|
|
458
|
+
data_item,
|
|
459
|
+
force,
|
|
460
|
+
i18n.t('command.delete_upload_confirmation', upload=data_item.name),
|
|
461
|
+
i18n.t('command.delete_upload', upload=data_item.name)
|
|
462
|
+
)
|
|
463
|
+
except RequestError as e:
|
|
464
|
+
if Command.__DATA_ITEM_STILL_IN_USE in str(e):
|
|
465
|
+
pass # A more friendly error will have already been displayed.
|
|
466
|
+
else:
|
|
467
|
+
raise
|
|
468
|
+
|
|
469
|
+
return attempted_delete
|
|
470
|
+
|
|
413
471
|
def display_service(self, organisation, service_name):
|
|
414
472
|
"""
|
|
415
473
|
Displays the documentation for the service. If the service does not exist, it will be ignored.
|
|
@@ -480,8 +538,14 @@ class Command(Base):
|
|
|
480
538
|
self.__print(logging.INFO, i18n.t('command.log_bookend'))
|
|
481
539
|
|
|
482
540
|
else:
|
|
541
|
+
has_executions = False
|
|
542
|
+
|
|
483
543
|
for execution in process.executions:
|
|
484
544
|
self.__print_execution(execution)
|
|
545
|
+
has_executions = True
|
|
546
|
+
|
|
547
|
+
if not has_executions:
|
|
548
|
+
self.__print(logging.INFO, i18n.t('command.log_no_executions'))
|
|
485
549
|
|
|
486
550
|
self.__print(logging.INFO, i18n.t('command.log_bookend'))
|
|
487
551
|
|
|
@@ -490,8 +554,8 @@ class Command(Base):
|
|
|
490
554
|
|
|
491
555
|
return found
|
|
492
556
|
|
|
493
|
-
def __download_execution(self, process, download_inputs, download_outputs, download_storage, download_intermediate, download_components,
|
|
494
|
-
execution):
|
|
557
|
+
def __download_execution(self, process, download_inputs, download_outputs, download_storage, download_statistics, download_intermediate, download_components,
|
|
558
|
+
output_dir, wait, execution):
|
|
495
559
|
"""
|
|
496
560
|
Downloads an execution.
|
|
497
561
|
|
|
@@ -500,6 +564,7 @@ class Command(Base):
|
|
|
500
564
|
download_inputs: Should inputs be downloaded?
|
|
501
565
|
download_outputs: Should outputs be downloaded?
|
|
502
566
|
download_storage: Should storage be downloaded?
|
|
567
|
+
download_statistics: Should statistics be downloaded?
|
|
503
568
|
download_intermediate: Should intermediate components be downloaded?
|
|
504
569
|
download_components: The names of the components that should be downloaded? If None or [], then all components are downloaded.
|
|
505
570
|
output_dir: The output directory.
|
|
@@ -510,8 +575,6 @@ class Command(Base):
|
|
|
510
575
|
The list of file tuples to be downloaded (file model, download path), and the metrics.
|
|
511
576
|
"""
|
|
512
577
|
download_components = [] if download_components is None else download_components
|
|
513
|
-
metrics = []
|
|
514
|
-
downloads = []
|
|
515
578
|
|
|
516
579
|
# Optionally wait for the execution to complete. We explicitly wait in this method to keep the SDK free.
|
|
517
580
|
complete = False
|
|
@@ -543,12 +606,15 @@ class Command(Base):
|
|
|
543
606
|
components = [
|
|
544
607
|
component
|
|
545
608
|
for component in execution.components
|
|
546
|
-
if ((not component
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
)
|
|
609
|
+
if ((not hasattr(component, Model._FIELD_INTERMEDIATE)) or (
|
|
610
|
+
(hasattr(component, Model._FIELD_INTERMEDIATE) and ((not component.intermediate) or (component.intermediate and download_intermediate))))) and (
|
|
611
|
+
(len(download_components) <= 0) or ((len(download_components) >= 1) and (component.name in download_components)))
|
|
550
612
|
]
|
|
551
613
|
|
|
614
|
+
# Prepare the downloads and metrics for each component.
|
|
615
|
+
metrics = []
|
|
616
|
+
downloads = []
|
|
617
|
+
|
|
552
618
|
for j, component in enumerate(components):
|
|
553
619
|
# Download the component.
|
|
554
620
|
component_name = '_'.join([item for item in re.sub(r'[\W]', '_', component.name).split('_') if len(item) > 0])
|
|
@@ -585,8 +651,7 @@ class Command(Base):
|
|
|
585
651
|
]:
|
|
586
652
|
metric[key] = sum([metric[key] for metric in component.metrics if metric.get(key) is not None])
|
|
587
653
|
|
|
588
|
-
# Find each input, output and
|
|
589
|
-
# for every file, so wait for all items to be analysed.
|
|
654
|
+
# Find each input, output, storage and statistics to download.
|
|
590
655
|
data_items = []
|
|
591
656
|
|
|
592
657
|
if download_inputs:
|
|
@@ -604,44 +669,17 @@ class Command(Base):
|
|
|
604
669
|
if storage_data_item is not None:
|
|
605
670
|
data_items += [(1, Command._DOWNLOAD_STORAGE, storage_data_item)]
|
|
606
671
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
for k, data_type, data_item in data_items:
|
|
611
|
-
# Download the data item once it is complete.
|
|
612
|
-
data_item.check_analysis_complete(wait=True)
|
|
613
|
-
|
|
614
|
-
name = re.sub('_+', '_', f"{data_type.capitalize()}_{str(k)}_{self.__data_name_to_file_name(data_item.name)}").rstrip('_')
|
|
615
|
-
download_dir = os.path.join(execution_dir, component_dir, name)
|
|
616
|
-
os.makedirs(download_dir, exist_ok=True)
|
|
617
|
-
|
|
618
|
-
# We also create the corresponding STAC definitions.
|
|
619
|
-
stac_definitions = []
|
|
620
|
-
|
|
621
|
-
for file in data_item.files:
|
|
622
|
-
path = os.path.join(download_dir, file.file_name)
|
|
623
|
-
|
|
624
|
-
# Download each file for the data item. We don't do anything with "storage".
|
|
625
|
-
if data_type == Command._DOWNLOAD_INPUT:
|
|
626
|
-
input_sizes.append(file.size)
|
|
627
|
-
elif data_type == Command._DOWNLOAD_OUTPUT:
|
|
628
|
-
output_sizes.append(file.size)
|
|
629
|
-
|
|
630
|
-
downloads.append((file, path, file.size))
|
|
631
|
-
stac_item, stac_item_file = file.get_stac_item()
|
|
672
|
+
if download_statistics:
|
|
673
|
+
statistics_data_item = self.__get_statistics_data_item(component)
|
|
632
674
|
|
|
633
|
-
|
|
634
|
-
|
|
675
|
+
if statistics_data_item is not None:
|
|
676
|
+
data_items += [(1, Command._DOWNLOAD_STATISTICS, statistics_data_item)]
|
|
635
677
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
# Write the STAC definitions.
|
|
642
|
-
for stac_definition, stac_file_name in stac_definitions:
|
|
643
|
-
with open(os.path.join(download_dir, stac_file_name), 'w') as stac_file:
|
|
644
|
-
stac_file.write(json.dumps(stac_definition, default=json_default))
|
|
678
|
+
# Prepare each download data item by finding the constituent files, creating the download directories and calculating the input and output sizes.
|
|
679
|
+
download_base_dir = os.path.join(execution_dir, component_dir)
|
|
680
|
+
input_sizes, output_sizes, component_downloads = self.__prepare_downloads(data_items, download_base_dir, owner=component.name,
|
|
681
|
+
created_at=component.created_at, detail=component_options)
|
|
682
|
+
downloads.extend(component_downloads)
|
|
645
683
|
|
|
646
684
|
metric[Command._METRIC_S3_INPUT_SIZE] = sum(input_sizes) if len(input_sizes) > 0 else ''
|
|
647
685
|
metric[Command._METRIC_S3_OUTPUT_SIZE] = sum(output_sizes) if len(output_sizes) > 0 else ''
|
|
@@ -717,8 +755,8 @@ class Command(Base):
|
|
|
717
755
|
progress_bar.colour = Command.__PROGRESS_BAR_COLOUR_COMPLETE
|
|
718
756
|
progress_bar.close()
|
|
719
757
|
|
|
720
|
-
def download_process_execution(self, organisation, process_name, download_inputs, download_outputs, download_storage,
|
|
721
|
-
download_components, save_metrics, stac_only, wait_for_execution_to_complete):
|
|
758
|
+
def download_process_execution(self, organisation, process_name, download_inputs, download_outputs, download_storage, download_statistics,
|
|
759
|
+
download_intermediate, download_components, save_metrics, stac_only, wait_for_execution_to_complete):
|
|
722
760
|
"""
|
|
723
761
|
Downloads the inputs and/or outputs from a process or execution(s).
|
|
724
762
|
|
|
@@ -728,6 +766,7 @@ class Command(Base):
|
|
|
728
766
|
download_inputs: Should inputs be downloaded?
|
|
729
767
|
download_outputs: Should outputs be downloaded?
|
|
730
768
|
download_storage: Should storage be downloaded?
|
|
769
|
+
download_statistics: Should statistics be downloaded?
|
|
731
770
|
download_intermediate: Should intermediate components be downloaded?
|
|
732
771
|
download_components: The names of the components that should be downloaded? If None or [], then all components are downloaded.
|
|
733
772
|
save_metrics: Save process metrics to file?
|
|
@@ -739,18 +778,19 @@ class Command(Base):
|
|
|
739
778
|
"""
|
|
740
779
|
|
|
741
780
|
# If we are downloading nothing, at least download the outputs.
|
|
742
|
-
if (not download_inputs) and (not download_outputs) and (not download_storage):
|
|
781
|
+
if (not download_inputs) and (not download_outputs) and (not download_storage) and (not download_statistics):
|
|
743
782
|
download_outputs = True
|
|
744
783
|
|
|
745
|
-
self.__print(logging.INFO,
|
|
746
|
-
|
|
747
|
-
|
|
784
|
+
self.__print(logging.INFO,
|
|
785
|
+
i18n.t('command.download_process_execution', process=process_name, inputs=download_inputs, outputs=download_outputs, storage=download_storage,
|
|
786
|
+
statistics=download_statistics, intermediate=download_intermediate, stac_only=stac_only, metrics=save_metrics,
|
|
787
|
+
components=download_components))
|
|
748
788
|
|
|
749
789
|
# We are downloading from an existing process.
|
|
750
790
|
process, execution = self.get_process_or_execution(organisation, process_name)
|
|
751
791
|
|
|
752
792
|
# Get the output directory and remove it if it exists.
|
|
753
|
-
output_dir = self.
|
|
793
|
+
output_dir = self.__name_to_file_name(process.name)
|
|
754
794
|
shutil.rmtree(output_dir, ignore_errors=True)
|
|
755
795
|
|
|
756
796
|
if execution is None:
|
|
@@ -785,8 +825,8 @@ class Command(Base):
|
|
|
785
825
|
|
|
786
826
|
try:
|
|
787
827
|
# Now download each execution in batches.
|
|
788
|
-
partial_download_execution = partial(self.__download_execution, process, download_inputs, download_outputs, download_storage,
|
|
789
|
-
download_components, output_dir, wait_for_execution_to_complete)
|
|
828
|
+
partial_download_execution = partial(self.__download_execution, process, download_inputs, download_outputs, download_storage, download_statistics,
|
|
829
|
+
download_intermediate, download_components, output_dir, wait_for_execution_to_complete)
|
|
790
830
|
results = []
|
|
791
831
|
|
|
792
832
|
# We use a thread pool as it preserves the marshmallow configuration and log level. Using a ProcessPool resulted in fields being labelled as missing
|
|
@@ -811,7 +851,7 @@ class Command(Base):
|
|
|
811
851
|
downloads.extend(execution_downloads)
|
|
812
852
|
metrics.extend(execution_metrics)
|
|
813
853
|
|
|
814
|
-
# Download the files unless we only want the stac information
|
|
854
|
+
# Download the files unless we only want the stac information.
|
|
815
855
|
if not stac_only:
|
|
816
856
|
self.__download_files(downloads)
|
|
817
857
|
|
|
@@ -824,6 +864,30 @@ class Command(Base):
|
|
|
824
864
|
|
|
825
865
|
return process
|
|
826
866
|
|
|
867
|
+
def download_upload(self, organisation, upload_name):
|
|
868
|
+
"""
|
|
869
|
+
Downloads the upload with the specified name.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
organisation: The logged in organisation.
|
|
873
|
+
upload_name: The name of the upload to download.
|
|
874
|
+
"""
|
|
875
|
+
|
|
876
|
+
# We are downloading from an existing upload.
|
|
877
|
+
data_item = self.get_data(organisation, upload_name)
|
|
878
|
+
|
|
879
|
+
# Get the output directory and remove it if it exists.
|
|
880
|
+
output_dir = self.__name_to_file_name(data_item.name)
|
|
881
|
+
shutil.rmtree(output_dir, ignore_errors=True)
|
|
882
|
+
|
|
883
|
+
self.__print(logging.INFO, i18n.t('command.download_upload', upload=upload_name, output=output_dir))
|
|
884
|
+
|
|
885
|
+
# Prepare each download data item by finding the constituent files.
|
|
886
|
+
_, _, downloads = self.__prepare_downloads([(1, Command._DOWNLOAD_UPLOAD, data_item)], output_dir, owner=None, created_at=data_item.created_at, detail=None)
|
|
887
|
+
|
|
888
|
+
# Download the files.
|
|
889
|
+
self.__download_files(downloads)
|
|
890
|
+
|
|
827
891
|
def extract_start_parameters(self, service_or_yaml, input_list, option_list):
|
|
828
892
|
"""
|
|
829
893
|
Extracts the start parameters from either the command line arguments or the supplied YAML file.
|
|
@@ -836,7 +900,6 @@ class Command(Base):
|
|
|
836
900
|
Returns:
|
|
837
901
|
The service name, process name, input files, storage_files, options and dispatchers.
|
|
838
902
|
"""
|
|
839
|
-
|
|
840
903
|
yaml_file = None
|
|
841
904
|
service_name = None
|
|
842
905
|
process_name = None
|
|
@@ -916,6 +979,33 @@ class Command(Base):
|
|
|
916
979
|
|
|
917
980
|
return formatted
|
|
918
981
|
|
|
982
|
+
def get_data(self, organisation, data_name):
|
|
983
|
+
"""
|
|
984
|
+
Attempts to find a data item using the given data name or id.
|
|
985
|
+
|
|
986
|
+
Args:
|
|
987
|
+
organisation: The logged in organisation.
|
|
988
|
+
data_name: The name of the data item. This can also be the id.
|
|
989
|
+
|
|
990
|
+
Returns:
|
|
991
|
+
The found data item.
|
|
992
|
+
|
|
993
|
+
Raises:
|
|
994
|
+
CommandError: if a data item could not be found.
|
|
995
|
+
"""
|
|
996
|
+
|
|
997
|
+
# Find the data item using its case-sensitive name (starts with).
|
|
998
|
+
data_item, _ = organisation.find_data(name=data_name)
|
|
999
|
+
|
|
1000
|
+
# If the data item cannot be found, try to find it by its id.
|
|
1001
|
+
if data_item is None:
|
|
1002
|
+
data_item = next((item for item in organisation.data if (item.name == data_name) or (str(item.id) == data_name)), None)
|
|
1003
|
+
|
|
1004
|
+
if data_item is None:
|
|
1005
|
+
raise CommandError(i18n.t('command.no_such_data_item', data=data_name))
|
|
1006
|
+
|
|
1007
|
+
return data_item
|
|
1008
|
+
|
|
919
1009
|
def __get_input(self, input_prompt, is_password=False, default=None, constrained_values=None):
|
|
920
1010
|
"""
|
|
921
1011
|
Gets an input from the user.
|
|
@@ -1022,9 +1112,32 @@ class Command(Base):
|
|
|
1022
1112
|
|
|
1023
1113
|
return service
|
|
1024
1114
|
|
|
1115
|
+
def __get_statistics_data_item(self, process_service_execution):
|
|
1116
|
+
"""
|
|
1117
|
+
Gets the statistics data item for a process service execution.
|
|
1118
|
+
|
|
1119
|
+
Args:
|
|
1120
|
+
process_service_execution: The process service execution for which the statistics item is to be obtained.
|
|
1121
|
+
|
|
1122
|
+
Returns:
|
|
1123
|
+
The statistics data item if found. Otherwise, None.
|
|
1124
|
+
"""
|
|
1125
|
+
statistics_data_item = None
|
|
1126
|
+
statistics_id = process_service_execution.statistics_id if hasattr(process_service_execution, Model._FIELD_STATISTICS_ID) and (
|
|
1127
|
+
process_service_execution.statistics_id is not None) else None
|
|
1128
|
+
|
|
1129
|
+
if statistics_id is not None:
|
|
1130
|
+
try:
|
|
1131
|
+
statistics_data_item = Data._model_from_api_id(process_service_execution._session, organisation_id=process_service_execution.organisation_id,
|
|
1132
|
+
data_id=statistics_id)
|
|
1133
|
+
except:
|
|
1134
|
+
pass # There is no statistics item.
|
|
1135
|
+
|
|
1136
|
+
return statistics_data_item
|
|
1137
|
+
|
|
1025
1138
|
def __get_storage_data_item(self, process, group_index, group_count, ssd_id, chain_index):
|
|
1026
1139
|
"""
|
|
1027
|
-
Gets the storage
|
|
1140
|
+
Gets the storage data item for a process' group and chain index.
|
|
1028
1141
|
|
|
1029
1142
|
Args:
|
|
1030
1143
|
process: The process for which the storage item is to be obtained.
|
|
@@ -1078,6 +1191,8 @@ class Command(Base):
|
|
|
1078
1191
|
Args:
|
|
1079
1192
|
organisation: The logged in organisation.
|
|
1080
1193
|
"""
|
|
1194
|
+
count = 0
|
|
1195
|
+
|
|
1081
1196
|
for process in organisation.processes:
|
|
1082
1197
|
executions = [execution for execution in process.executions]
|
|
1083
1198
|
|
|
@@ -1094,16 +1209,104 @@ class Command(Base):
|
|
|
1094
1209
|
progress = sum([execution.progress for execution in executions]) // len(executions) if len(executions) > 0 else 0
|
|
1095
1210
|
group_progress_summary = ''
|
|
1096
1211
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1212
|
+
if len(groups) > 1:
|
|
1213
|
+
for i, group in enumerate(groups.values()):
|
|
1214
|
+
group_progress = sum([execution.progress for execution in group]) // len(group)
|
|
1215
|
+
group_progress_summary += i18n.t('command.log_process_group', group_number=(i + 1), progress=group_progress)
|
|
1216
|
+
|
|
1217
|
+
# Show the process information using a progress bar.
|
|
1218
|
+
process_status = i18n.t(f"command.log_process_status.{process.process_status}")
|
|
1219
|
+
warnings = sum([execution.exit_type in [Command._EXIT_NON_FATAL_WARNING, Command._EXIT_FATAL_WARNING] for execution in executions if
|
|
1220
|
+
hasattr(execution, Model._FIELD_EXIT_TYPE)])
|
|
1221
|
+
errors = sum([execution.exit_type == Command._EXIT_FATAL_ERROR for execution in executions if hasattr(execution, Model._FIELD_EXIT_TYPE)])
|
|
1222
|
+
successes = sum([execution.success for execution in executions]) - warnings # Warnings also count as successes, but we want to report a valid total.
|
|
1223
|
+
stopped = sum([execution.stopped for execution in executions])
|
|
1224
|
+
current = sum([1 if execution.progress >= 100.0 else 0 for execution in executions])
|
|
1225
|
+
total = len(executions) if len(executions) > 0 else 0
|
|
1226
|
+
|
|
1227
|
+
if progress < 100.0:
|
|
1228
|
+
colour = Command.__PROGRESS_BAR_COLOUR_ACTIVE
|
|
1229
|
+
elif errors > 0:
|
|
1230
|
+
colour = Command.__PROGRESS_BAR_COLOUR_ERROR
|
|
1231
|
+
elif warnings > 0:
|
|
1232
|
+
colour = Command.__PROGRESS_BAR_COLOUR_WARNING
|
|
1233
|
+
elif successes >= total:
|
|
1234
|
+
colour = Command.__PROGRESS_BAR_COLOUR_COMPLETE
|
|
1235
|
+
else:
|
|
1236
|
+
colour = Command.__PROGRESS_BAR_COLOUR_ERROR
|
|
1237
|
+
|
|
1238
|
+
description = i18n.t('command.log_process_summary', process=process.name, group_progress=group_progress_summary)
|
|
1239
|
+
bar_format = Command.__PROGRESS_BAR_FORMAT_PROCESS.format(current=current, total=total, process_status=process_status, successes=successes,
|
|
1240
|
+
warnings=warnings, errors=errors, stopped=stopped, description=description)
|
|
1241
|
+
progress_bar = tqdm(total=100.0, bar_format=bar_format, colour=colour)
|
|
1242
|
+
progress_bar.update(progress)
|
|
1243
|
+
progress_bar.close()
|
|
1244
|
+
|
|
1245
|
+
count += 1
|
|
1246
|
+
|
|
1247
|
+
if count <= 0:
|
|
1248
|
+
self.__print(logging.INFO, i18n.t('command.log_no_processes'))
|
|
1249
|
+
|
|
1250
|
+
def list_services(self, organisation):
|
|
1251
|
+
"""
|
|
1252
|
+
Lists all available services.
|
|
1253
|
+
|
|
1254
|
+
Args:
|
|
1255
|
+
organisation: The logged in organisation.
|
|
1256
|
+
"""
|
|
1257
|
+
count = 0
|
|
1258
|
+
|
|
1259
|
+
# Get a unique list of the latest services and sort them by name.
|
|
1260
|
+
services = sorted(organisation.services, key=lambda service: service.name)
|
|
1261
|
+
|
|
1262
|
+
for service in services:
|
|
1263
|
+
self.__print(logging.INFO, i18n.t('command.log_service_detail', name=service.name, summary=service.documentation_summary))
|
|
1264
|
+
count += 1
|
|
1100
1265
|
|
|
1101
|
-
|
|
1102
|
-
self.__print(logging.INFO,
|
|
1103
|
-
i18n.t('command.log_process_summary', process=process.name, executions=len(executions), progress=progress, groups=len(groups),
|
|
1104
|
-
group_progress=group_progress_summary))
|
|
1266
|
+
if count <= 0:
|
|
1267
|
+
self.__print(logging.INFO, i18n.t('command.log_no_services'))
|
|
1105
1268
|
|
|
1106
|
-
def
|
|
1269
|
+
def list_uploads(self, organisation):
|
|
1270
|
+
"""
|
|
1271
|
+
Lists all uploads.
|
|
1272
|
+
|
|
1273
|
+
Args:
|
|
1274
|
+
organisation: The logged in organisation.
|
|
1275
|
+
"""
|
|
1276
|
+
count = 0
|
|
1277
|
+
|
|
1278
|
+
# Get the width of all upload names.
|
|
1279
|
+
uploads = [data for data in organisation.data]
|
|
1280
|
+
maximum_name_width = max([len(data.name) for data in uploads]) if len(uploads) > 0 else 0
|
|
1281
|
+
blank_name = ' ' * maximum_name_width
|
|
1282
|
+
|
|
1283
|
+
for data in uploads:
|
|
1284
|
+
name = data.name.ljust(maximum_name_width)
|
|
1285
|
+
|
|
1286
|
+
for i, file in enumerate(data.files):
|
|
1287
|
+
file_name = file.file_name if hasattr(file, Model._FIELD_FILE_NAME) and (file.file_name is not None) else ''
|
|
1288
|
+
crs = file.crs if hasattr(file, Model._FIELD_CRS) and (file.crs is not None) else ''
|
|
1289
|
+
source = file.source if hasattr(file, Model._FIELD_SOURCE) and (file.source is not None) else None
|
|
1290
|
+
size = self.__bytes_to_human(file.size) if hasattr(file, Model._FIELD_SIZE) and (file.size is not None) else ''
|
|
1291
|
+
|
|
1292
|
+
file_type = f" {file.file_type.ljust(14)}"
|
|
1293
|
+
file_name = f" {file_name.ljust(20)}"
|
|
1294
|
+
crs = f" {crs.ljust(10)}"
|
|
1295
|
+
source = ' ' if source is not None else ' >'
|
|
1296
|
+
size = f" {size.rjust(10)}"
|
|
1297
|
+
|
|
1298
|
+
self.__print(
|
|
1299
|
+
logging.INFO,
|
|
1300
|
+
i18n.t('command.log_data_file', name=name if i == 0 else blank_name, file_type=file_type, file_name=file_name, source=source, crs=crs,
|
|
1301
|
+
size=size)
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
count += 1
|
|
1305
|
+
|
|
1306
|
+
if count <= 0:
|
|
1307
|
+
self.__print(logging.INFO, i18n.t('command.log_no_uploads'))
|
|
1308
|
+
|
|
1309
|
+
def login(self, deployment, email, organisation_name, no_save_password):
|
|
1107
1310
|
"""
|
|
1108
1311
|
Logs into the desired Fusion Platform(r) deployment and allows the user to select an organisation.
|
|
1109
1312
|
|
|
@@ -1111,6 +1314,7 @@ class Command(Base):
|
|
|
1111
1314
|
deployment: The deployment to use.
|
|
1112
1315
|
email: The optional email address used to login. If not provided, the email will be prompted for.
|
|
1113
1316
|
organisation_name: The optional organisation name. If not provided, the organisation will be prompted for.
|
|
1317
|
+
no_save_password: Attempt to save the password using keyring? If this is True, no password will be saved and any existing password will be deleted.
|
|
1114
1318
|
|
|
1115
1319
|
Returns:
|
|
1116
1320
|
The selected organisation and its corresponding stage name and region.
|
|
@@ -1118,8 +1322,29 @@ class Command(Base):
|
|
|
1118
1322
|
|
|
1119
1323
|
# Which API deployment?
|
|
1120
1324
|
deployment = deployment.lower()
|
|
1121
|
-
stage = Command.DEPLOYMENTS[deployment
|
|
1122
|
-
region = Command.DEPLOYMENTS[deployment
|
|
1325
|
+
stage = dict_nested_get(Command.DEPLOYMENTS, [deployment, Command._DEPLOYMENT_KEY_STAGE])
|
|
1326
|
+
region = dict_nested_get(Command.DEPLOYMENTS, [deployment, Command._DEPLOYMENT_KEY_REGION])
|
|
1327
|
+
|
|
1328
|
+
if (stage is None) or (region is None):
|
|
1329
|
+
raise CommandError(i18n.t('command.no_such_deployment', deployment=deployment))
|
|
1330
|
+
|
|
1331
|
+
# If we are not saving passwords, then delete any password associated with the email address.
|
|
1332
|
+
if no_save_password:
|
|
1333
|
+
try:
|
|
1334
|
+
keyring.delete_password(Command._KEYRING_DEPLOYMENT_FORMAT.format(deployment=deployment), email)
|
|
1335
|
+
except:
|
|
1336
|
+
# A Keyring suggests there is no suitable backend. Carry on regardless.
|
|
1337
|
+
pass
|
|
1338
|
+
|
|
1339
|
+
# Attempt to get the password using keyring if we have an email address. This is linked to the deployment.
|
|
1340
|
+
password = None
|
|
1341
|
+
|
|
1342
|
+
if email is not None:
|
|
1343
|
+
try:
|
|
1344
|
+
password = keyring.get_password(Command._KEYRING_DEPLOYMENT_FORMAT.format(deployment=deployment), email)
|
|
1345
|
+
except:
|
|
1346
|
+
# A Keyring suggests there is no suitable backend. Carry on regardless.
|
|
1347
|
+
pass
|
|
1123
1348
|
|
|
1124
1349
|
# Login with email address and password. We retry the email and password up to three times.
|
|
1125
1350
|
count = 0
|
|
@@ -1128,18 +1353,31 @@ class Command(Base):
|
|
|
1128
1353
|
if email is None:
|
|
1129
1354
|
email = self.__get_input(i18n.t('command.email'), default=email)
|
|
1130
1355
|
|
|
1131
|
-
password
|
|
1356
|
+
if password is None:
|
|
1357
|
+
password = self.__get_input(i18n.t('command.password'), is_password=True)
|
|
1132
1358
|
|
|
1133
1359
|
try:
|
|
1134
|
-
user = fusion_platform.login(email=email, password=password,
|
|
1360
|
+
user = fusion_platform.login(email=email, password=password,
|
|
1361
|
+
api_url=dict_nested_get(Command.DEPLOYMENTS, [deployment, Command._DEPLOYMENT_KEY_URL]))
|
|
1135
1362
|
break
|
|
1136
1363
|
except RequestError as e:
|
|
1137
1364
|
count += 1
|
|
1138
1365
|
|
|
1366
|
+
# Clear the password, which cannot have been correct for the email address.
|
|
1367
|
+
password = None
|
|
1368
|
+
|
|
1139
1369
|
# Cope with finger trouble.
|
|
1140
1370
|
if (i18n.t('command.invalid_login_response') not in str(e)) or (count >= 3):
|
|
1141
1371
|
raise
|
|
1142
1372
|
|
|
1373
|
+
# We have successfully logged in. If allowed, save the email address and password for the deployment.
|
|
1374
|
+
try:
|
|
1375
|
+
if not no_save_password:
|
|
1376
|
+
keyring.set_password(Command._KEYRING_DEPLOYMENT_FORMAT.format(deployment=deployment), email, password)
|
|
1377
|
+
except:
|
|
1378
|
+
# A Keyring suggests there is no suitable backend. Carry on regardless.
|
|
1379
|
+
pass
|
|
1380
|
+
|
|
1143
1381
|
# Get the list of organisation names the user belongs to if the organisation is not provided.
|
|
1144
1382
|
if organisation_name is None:
|
|
1145
1383
|
organisation_names = [(item.name, item.updated_at) for item in user.organisations]
|
|
@@ -1176,19 +1414,94 @@ class Command(Base):
|
|
|
1176
1414
|
|
|
1177
1415
|
try:
|
|
1178
1416
|
# Login to the correct deployment and select the organisation.
|
|
1179
|
-
organisation, _, _ = self.login(arguments.deployment, arguments.email, arguments.organisation)
|
|
1417
|
+
organisation, _, _ = self.login(arguments.deployment, arguments.email, arguments.organisation, arguments.no_save_password)
|
|
1180
1418
|
|
|
1181
|
-
# Perform the
|
|
1182
|
-
if arguments.command == i18n.t('command.
|
|
1183
|
-
#
|
|
1184
|
-
|
|
1419
|
+
# Perform the define, display, download, list, start or template.
|
|
1420
|
+
if arguments.command == i18n.t('command.define.command'):
|
|
1421
|
+
# Define one or more processes as YAML files.
|
|
1422
|
+
for process_name in arguments.process:
|
|
1423
|
+
# Get the process definition, optionally downloading the inputs and storage to the current directory as well.
|
|
1424
|
+
process_definition = self.define_process(organisation, process_name, download_inputs=arguments.inputs, download_storage=arguments.storage)
|
|
1425
|
+
|
|
1426
|
+
# Write the YAML to file. This will overwrite the existing file.
|
|
1427
|
+
file_name = f"{self.__name_to_file_name(process_name)}{Command._YAML_EXTENSION}"
|
|
1428
|
+
|
|
1429
|
+
with open(file_name, 'w') as file:
|
|
1430
|
+
yaml.dump(process_definition, file, sort_keys=False, width=float('inf'))
|
|
1431
|
+
|
|
1432
|
+
elif arguments.command == i18n.t('command.delete.command'):
|
|
1433
|
+
if arguments.process_or_upload == i18n.t('command.delete.process.command'):
|
|
1434
|
+
# Delete a process with the specified name or ids, or execution with ids.
|
|
1435
|
+
for process in arguments.process:
|
|
1436
|
+
if not self.delete_process(organisation, process, arguments.force):
|
|
1437
|
+
self.__print(logging.WARNING, i18n.t('command.no_such_process', process=process))
|
|
1438
|
+
|
|
1439
|
+
elif arguments.process_or_upload == i18n.t('command.delete.upload.command'):
|
|
1440
|
+
# Delete an upload with the specified name or ids.
|
|
1441
|
+
for upload in arguments.upload:
|
|
1442
|
+
if not self.delete_upload(organisation, upload, arguments.force):
|
|
1443
|
+
self.__print(logging.WARNING, i18n.t('command.no_such_upload', upload=upload))
|
|
1444
|
+
|
|
1445
|
+
else:
|
|
1446
|
+
# Unknown command.
|
|
1447
|
+
raise CommandError(i18n.t('command.unknown_command', command=arguments.process_or_upload))
|
|
1185
1448
|
|
|
1186
1449
|
elif arguments.command == i18n.t('command.display.command'):
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
if not self.
|
|
1191
|
-
self.__print(logging.WARNING, i18n.t('command.
|
|
1450
|
+
if arguments.process_or_service == i18n.t('command.display.process.command'):
|
|
1451
|
+
# Display a process with the specified name or ids.
|
|
1452
|
+
for process in arguments.process:
|
|
1453
|
+
if not self.display_process(organisation, process):
|
|
1454
|
+
self.__print(logging.WARNING, i18n.t('command.no_such_process', process=process))
|
|
1455
|
+
|
|
1456
|
+
elif arguments.process_or_service == i18n.t('command.display.service.command'):
|
|
1457
|
+
# Display a service with the specified name or ids.
|
|
1458
|
+
for service in arguments.service:
|
|
1459
|
+
if not self.display_service(organisation, service):
|
|
1460
|
+
self.__print(logging.WARNING, i18n.t('command.no_such_service', service=service))
|
|
1461
|
+
|
|
1462
|
+
else:
|
|
1463
|
+
# Unknown command.
|
|
1464
|
+
raise CommandError(i18n.t('command.unknown_command', command=arguments.process_or_service))
|
|
1465
|
+
|
|
1466
|
+
elif arguments.command == i18n.t('command.download.command'):
|
|
1467
|
+
if arguments.process_or_upload == i18n.t('command.download.process.command'):
|
|
1468
|
+
# Download one or more processes.
|
|
1469
|
+
for process_name in arguments.process:
|
|
1470
|
+
# Download the process.
|
|
1471
|
+
process = self.download_process_execution(organisation, process_name, arguments.inputs, arguments.outputs, arguments.storage,
|
|
1472
|
+
arguments.statistics, arguments.intermediate, arguments.component, arguments.metrics,
|
|
1473
|
+
arguments.stac,
|
|
1474
|
+
not arguments.no_wait_for_completion)
|
|
1475
|
+
|
|
1476
|
+
# And optionally remove the process and its inputs.
|
|
1477
|
+
if arguments.remove:
|
|
1478
|
+
self.remove_process_and_inputs(organisation, process)
|
|
1479
|
+
|
|
1480
|
+
elif arguments.process_or_upload == i18n.t('command.download.upload.command'):
|
|
1481
|
+
# Download one or more uploads.
|
|
1482
|
+
for upload_name in arguments.upload:
|
|
1483
|
+
self.download_upload(organisation, upload_name)
|
|
1484
|
+
|
|
1485
|
+
else:
|
|
1486
|
+
# Unknown command.
|
|
1487
|
+
raise CommandError(i18n.t('command.unknown_command', command=arguments.process_or_upload))
|
|
1488
|
+
|
|
1489
|
+
elif arguments.command == i18n.t('command.list.command'):
|
|
1490
|
+
if arguments.process_service_or_upload == i18n.t('command.list.process.command'):
|
|
1491
|
+
# List all processes.
|
|
1492
|
+
self.list_processes(organisation)
|
|
1493
|
+
|
|
1494
|
+
elif arguments.process_service_or_upload == i18n.t('command.list.service.command'):
|
|
1495
|
+
# List all services.
|
|
1496
|
+
self.list_services(organisation)
|
|
1497
|
+
|
|
1498
|
+
elif arguments.process_service_or_upload == i18n.t('command.list.upload.command'):
|
|
1499
|
+
# List all uploads.
|
|
1500
|
+
self.list_uploads(organisation)
|
|
1501
|
+
|
|
1502
|
+
else:
|
|
1503
|
+
# Unknown command.
|
|
1504
|
+
raise CommandError(i18n.t('command.unknown_command', command=arguments.process_service_or_upload))
|
|
1192
1505
|
|
|
1193
1506
|
elif arguments.command == i18n.t('command.start.command'):
|
|
1194
1507
|
# Start one or more processes.
|
|
@@ -1207,36 +1520,28 @@ class Command(Base):
|
|
|
1207
1520
|
# Also perform the download, if required.
|
|
1208
1521
|
if arguments.download:
|
|
1209
1522
|
process = self.download_process_execution(organisation, process_name, arguments.inputs, arguments.outputs, arguments.storage,
|
|
1210
|
-
arguments.
|
|
1211
|
-
not arguments.no_wait_for_completion)
|
|
1523
|
+
arguments.statistics, arguments.intermediate, arguments.component, arguments.metrics,
|
|
1524
|
+
arguments.stac, not arguments.no_wait_for_completion)
|
|
1212
1525
|
|
|
1213
1526
|
# And optionally remove the process and its inputs.
|
|
1214
1527
|
if arguments.remove:
|
|
1215
1528
|
self.remove_process_and_inputs(organisation, process)
|
|
1216
1529
|
|
|
1217
|
-
elif arguments.command == i18n.t('command.
|
|
1218
|
-
#
|
|
1219
|
-
for
|
|
1220
|
-
# Get the
|
|
1221
|
-
|
|
1530
|
+
elif arguments.command == i18n.t('command.template.command'):
|
|
1531
|
+
# Template one or more services.
|
|
1532
|
+
for service_name in arguments.service:
|
|
1533
|
+
# Get the service definition.
|
|
1534
|
+
service_definition = self.template_service(organisation, service_name)
|
|
1222
1535
|
|
|
1223
1536
|
# Write the YAML to file. This will overwrite the existing file.
|
|
1224
|
-
file_name = f"{self.
|
|
1537
|
+
file_name = f"{self.__name_to_file_name(service_name)}{Command._YAML_EXTENSION}"
|
|
1225
1538
|
|
|
1226
1539
|
with open(file_name, 'w') as file:
|
|
1227
|
-
yaml.dump(
|
|
1228
|
-
|
|
1229
|
-
elif arguments.command == i18n.t('command.download.command'):
|
|
1230
|
-
# Download one or more processes.
|
|
1231
|
-
for process_name in arguments.process:
|
|
1232
|
-
# Download the process.
|
|
1233
|
-
process = self.download_process_execution(organisation, process_name, arguments.inputs, arguments.outputs, arguments.storage,
|
|
1234
|
-
arguments.intermediate, arguments.component, arguments.metrics, arguments.stac,
|
|
1235
|
-
not arguments.no_wait_for_completion)
|
|
1540
|
+
yaml.dump(service_definition, file, sort_keys=False, width=float('inf'))
|
|
1236
1541
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1542
|
+
elif arguments.command == i18n.t('command.upload.command'):
|
|
1543
|
+
# Upload files to an upload data item.
|
|
1544
|
+
self.upload(organisation, arguments.upload_name, arguments.file_type, arguments.file)
|
|
1240
1545
|
|
|
1241
1546
|
else:
|
|
1242
1547
|
# Unknown command.
|
|
@@ -1249,13 +1554,24 @@ class Command(Base):
|
|
|
1249
1554
|
self.__print(logging.ERROR, str(e))
|
|
1250
1555
|
exit(1) # We want to exit with an error status.
|
|
1251
1556
|
|
|
1557
|
+
def __name_to_file_name(self, process_name):
|
|
1558
|
+
"""
|
|
1559
|
+
Converts a process name into a sane file name (without extension).
|
|
1560
|
+
|
|
1561
|
+
Args:
|
|
1562
|
+
process_name: The process name to convert.
|
|
1563
|
+
|
|
1564
|
+
Returns:
|
|
1565
|
+
The corresponding file name.
|
|
1566
|
+
"""
|
|
1567
|
+
return re.sub(r"[^A-Za-z0-9._\-]", '_', str(process_name)) if process_name is not None else None
|
|
1568
|
+
|
|
1252
1569
|
def __parse_arguments(self):
|
|
1253
1570
|
"""
|
|
1254
1571
|
Parses the command line arguments.
|
|
1255
1572
|
|
|
1256
1573
|
:return: The parsed arguments.
|
|
1257
1574
|
"""
|
|
1258
|
-
|
|
1259
1575
|
parser = argparse.ArgumentParser(
|
|
1260
1576
|
formatter_class=RawTextHelpFormatter,
|
|
1261
1577
|
prog=i18n.t('command.program'),
|
|
@@ -1264,28 +1580,132 @@ class Command(Base):
|
|
|
1264
1580
|
)
|
|
1265
1581
|
|
|
1266
1582
|
# Create the subparsers.
|
|
1267
|
-
subparsers = parser.add_subparsers(
|
|
1583
|
+
subparsers = parser.add_subparsers(description=i18n.t('command.subparser'), dest='command')
|
|
1268
1584
|
parser.add_argument(i18n.t('command.version_short'), i18n.t('command.version_long'), help=i18n.t('command.version_help'), action='version',
|
|
1269
1585
|
version=i18n.t('command.version_content', version=fusion_platform.__version__, version_date=fusion_platform.__version_date__))
|
|
1270
1586
|
|
|
1271
1587
|
subparsers.required = True
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1588
|
+
parser_define = subparsers.add_parser(i18n.t('command.define.command'), help=i18n.t('command.define.help'),
|
|
1589
|
+
description=i18n.t('command.define.description'))
|
|
1590
|
+
parser_delete = subparsers.add_parser(i18n.t('command.delete.command'), help=i18n.t('command.delete.help'),
|
|
1591
|
+
description=i18n.t('command.delete.description'))
|
|
1592
|
+
parser_display = subparsers.add_parser(i18n.t('command.display.command'), help=i18n.t('command.display.help'),
|
|
1593
|
+
description=i18n.t('command.display.description'))
|
|
1594
|
+
parser_download = subparsers.add_parser(i18n.t('command.download.command'), help=i18n.t('command.download.help'),
|
|
1595
|
+
description=i18n.t('command.download.description'))
|
|
1596
|
+
parser_list = subparsers.add_parser(i18n.t('command.list.command'), help=i18n.t('command.list.help'), description=i18n.t('command.list.description'))
|
|
1597
|
+
parser_start = subparsers.add_parser(i18n.t('command.start.command'), help=i18n.t('command.start.help'), description=i18n.t('command.start.description'))
|
|
1598
|
+
parser_template = subparsers.add_parser(i18n.t('command.template.command'), help=i18n.t('command.template.help'),
|
|
1599
|
+
description=i18n.t('command.template.description'))
|
|
1600
|
+
parser_upload = subparsers.add_parser(i18n.t('command.upload.command'), help=i18n.t('command.upload.help'),
|
|
1601
|
+
description=i18n.t('command.upload.description'))
|
|
1602
|
+
|
|
1603
|
+
# Create the subparses for delete.
|
|
1604
|
+
delete_subparsers = parser_delete.add_subparsers(description=i18n.t('command.delete.subparser'), dest='process_or_upload')
|
|
1605
|
+
delete_subparsers.required = True
|
|
1606
|
+
|
|
1607
|
+
parser_delete_process = delete_subparsers.add_parser(i18n.t('command.delete.process.command'), help=i18n.t('command.delete.process.help'),
|
|
1608
|
+
description=i18n.t('command.delete.process.description'))
|
|
1609
|
+
parser_delete_upload = delete_subparsers.add_parser(i18n.t('command.delete.upload.command'), help=i18n.t('command.delete.upload.help'),
|
|
1610
|
+
description=i18n.t('command.delete.upload.description'))
|
|
1611
|
+
|
|
1612
|
+
# Create the subparses for display.
|
|
1613
|
+
display_subparsers = parser_display.add_subparsers(description=i18n.t('command.display.subparser'), dest='process_or_service')
|
|
1614
|
+
display_subparsers.required = True
|
|
1615
|
+
|
|
1616
|
+
parser_display_process = display_subparsers.add_parser(i18n.t('command.display.process.command'), help=i18n.t('command.display.process.help'),
|
|
1617
|
+
description=i18n.t('command.display.process.description'))
|
|
1618
|
+
parser_display_service = display_subparsers.add_parser(i18n.t('command.display.service.command'), help=i18n.t('command.display.service.help'),
|
|
1619
|
+
description=i18n.t('command.display.service.description'))
|
|
1620
|
+
|
|
1621
|
+
# Create the subparses for download.
|
|
1622
|
+
download_subparsers = parser_download.add_subparsers(description=i18n.t('command.download.subparser'), dest='process_or_upload')
|
|
1623
|
+
download_subparsers.required = True
|
|
1624
|
+
|
|
1625
|
+
parser_download_process = download_subparsers.add_parser(i18n.t('command.download.process.command'), help=i18n.t('command.download.process.help'),
|
|
1626
|
+
description=i18n.t('command.download.process.description'))
|
|
1627
|
+
parser_download_upload = download_subparsers.add_parser(i18n.t('command.download.upload.command'), help=i18n.t('command.download.upload.help'),
|
|
1628
|
+
description=i18n.t('command.download.upload.description'))
|
|
1629
|
+
|
|
1630
|
+
# Create the subparses for list.
|
|
1631
|
+
list_subparsers = parser_list.add_subparsers(description=i18n.t('command.list.subparser'), dest='process_service_or_upload')
|
|
1632
|
+
list_subparsers.required = True
|
|
1633
|
+
|
|
1634
|
+
parser_list_process = list_subparsers.add_parser(i18n.t('command.list.process.command'), help=i18n.t('command.list.process.help'),
|
|
1635
|
+
description=i18n.t('command.list.process.description'))
|
|
1636
|
+
parser_list_service = list_subparsers.add_parser(i18n.t('command.list.service.command'), help=i18n.t('command.list.service.help'),
|
|
1637
|
+
description=i18n.t('command.list.service.description'))
|
|
1638
|
+
parser_list_upload = list_subparsers.add_parser(i18n.t('command.list.upload.command'), help=i18n.t('command.list.upload.help'),
|
|
1639
|
+
description=i18n.t('command.list.upload.description'))
|
|
1277
1640
|
|
|
1278
1641
|
# Add in the common arguments.
|
|
1279
|
-
for subparser in [
|
|
1642
|
+
for subparser in [
|
|
1643
|
+
parser_define,
|
|
1644
|
+
parser_delete_process,
|
|
1645
|
+
parser_delete_upload,
|
|
1646
|
+
parser_display_process,
|
|
1647
|
+
parser_display_service,
|
|
1648
|
+
parser_download_process,
|
|
1649
|
+
parser_download_upload,
|
|
1650
|
+
parser_list_process,
|
|
1651
|
+
parser_list_service,
|
|
1652
|
+
parser_list_upload,
|
|
1653
|
+
parser_start,
|
|
1654
|
+
parser_template,
|
|
1655
|
+
parser_upload
|
|
1656
|
+
]:
|
|
1280
1657
|
subparser.add_argument(i18n.t('command.deployment_short'), i18n.t('command.deployment_long'), help=i18n.t('command.deployment_help'),
|
|
1281
1658
|
default=Command._DEPLOYMENT_PRODUCTION_IRL)
|
|
1282
1659
|
subparser.add_argument(i18n.t('command.email_short'), i18n.t('command.email_long'), help=i18n.t('command.email_help'))
|
|
1283
1660
|
subparser.add_argument(i18n.t('command.organisation_short'), i18n.t('command.organisation_long'), help=i18n.t('command.organisation_help'))
|
|
1661
|
+
subparser.add_argument(i18n.t('command.no_save_password_short'), i18n.t('command.no_save_password_long'), help=i18n.t('command.no_save_password_help'),
|
|
1662
|
+
default=False, action='store_true')
|
|
1284
1663
|
subparser.add_argument(i18n.t('command.debug_short'), i18n.t('command.debug_long'), help=i18n.t('command.debug_help'), default=False,
|
|
1285
|
-
action=
|
|
1664
|
+
action='store_true')
|
|
1665
|
+
|
|
1666
|
+
# Define arguments.
|
|
1667
|
+
parser_define.add_argument(i18n.t('command.define.process_long'), help=i18n.t('command.define.process_help'), nargs='+')
|
|
1668
|
+
parser_define.add_argument(i18n.t('command.start.inputs_short'), i18n.t('command.start.inputs_long'), help=i18n.t('command.start.inputs_help'),
|
|
1669
|
+
default=False, action='store_true')
|
|
1670
|
+
parser_define.add_argument(i18n.t('command.start.storage_short'), i18n.t('command.start.storage_long'), help=i18n.t('command.start.storage_help'),
|
|
1671
|
+
default=False, action='store_true')
|
|
1672
|
+
|
|
1673
|
+
# Delete arguments.
|
|
1674
|
+
parser_delete_upload.add_argument(i18n.t('command.delete.upload_long'), help=i18n.t('command.delete.upload_help'), nargs='+')
|
|
1675
|
+
parser_delete_upload.add_argument(i18n.t('command.delete.force_short'), i18n.t('command.delete.force_long'), help=i18n.t('command.delete.force_help'),
|
|
1676
|
+
default=False, action='store_true')
|
|
1677
|
+
parser_delete_process.add_argument(i18n.t('command.delete.process_long'), help=i18n.t('command.delete.process_help'), nargs='+')
|
|
1678
|
+
parser_delete_process.add_argument(i18n.t('command.delete.force_short'), i18n.t('command.delete.force_long'), help=i18n.t('command.delete.force_help'),
|
|
1679
|
+
default=False, action='store_true')
|
|
1286
1680
|
|
|
1287
1681
|
# Display arguments.
|
|
1288
|
-
|
|
1682
|
+
parser_display_process.add_argument(i18n.t('command.display.process_long'), help=i18n.t('command.display.process_help'), nargs='+')
|
|
1683
|
+
parser_display_service.add_argument(i18n.t('command.display.service_long'), help=i18n.t('command.display.service_help'), nargs='+')
|
|
1684
|
+
|
|
1685
|
+
# Download arguments.
|
|
1686
|
+
parser_download_process.add_argument(i18n.t('command.download.process_long'), help=i18n.t('command.download.process_help'), nargs='+')
|
|
1687
|
+
parser_download_process.add_argument(i18n.t('command.start.inputs_short'), i18n.t('command.start.inputs_long'), help=i18n.t('command.start.inputs_help'),
|
|
1688
|
+
default=False, action='store_true')
|
|
1689
|
+
parser_download_process.add_argument(i18n.t('command.start.outputs_short'), i18n.t('command.start.outputs_long'), help=i18n.t('command.start.outputs_help'),
|
|
1690
|
+
default=False, action='store_true')
|
|
1691
|
+
parser_download_process.add_argument(i18n.t('command.start.storage_short'), i18n.t('command.start.storage_long'), help=i18n.t('command.start.storage_help'),
|
|
1692
|
+
default=False, action='store_true')
|
|
1693
|
+
parser_download_process.add_argument(i18n.t('command.start.statistics_short'), i18n.t('command.start.statistics_long'),
|
|
1694
|
+
help=i18n.t('command.start.statistics_help'), default=False, action='store_true')
|
|
1695
|
+
parser_download_process.add_argument(i18n.t('command.start.remove_short'), i18n.t('command.start.remove_long'), help=i18n.t('command.start.remove_help'),
|
|
1696
|
+
default=False, action='store_true')
|
|
1697
|
+
parser_download_process.add_argument(i18n.t('command.start.intermediate_short'), i18n.t('command.start.intermediate_long'),
|
|
1698
|
+
help=i18n.t('command.start.intermediate_help'), default=False, action='store_true')
|
|
1699
|
+
parser_download_process.add_argument(i18n.t('command.start.metrics_short'), i18n.t('command.start.metrics_long'), help=i18n.t('command.start.metrics_help'),
|
|
1700
|
+
default=False, action='store_true')
|
|
1701
|
+
parser_download_process.add_argument(i18n.t('command.start.component_short'), i18n.t('command.start.component_long'),
|
|
1702
|
+
help=i18n.t('command.start.component_help'), nargs='*')
|
|
1703
|
+
parser_download_process.add_argument(i18n.t('command.start.no_wait_for_completion_short'), i18n.t('command.start.no_wait_for_completion_long'),
|
|
1704
|
+
help=i18n.t('command.start.no_wait_for_completion_help'), default=False, action='store_true')
|
|
1705
|
+
parser_download_process.add_argument(i18n.t('command.start.stac_only_short'), i18n.t('command.start.stac_only_long'),
|
|
1706
|
+
help=i18n.t('command.start.stac_only_help'),
|
|
1707
|
+
default=False, action='store_true')
|
|
1708
|
+
parser_download_upload.add_argument(i18n.t('command.download.upload_long'), help=i18n.t('command.download.upload_help'), nargs='+')
|
|
1289
1709
|
|
|
1290
1710
|
# Start arguments.
|
|
1291
1711
|
parser_start.add_argument(i18n.t('command.start.definition_long'), help=i18n.t('command.start.definition_help'), nargs='+')
|
|
@@ -1294,58 +1714,126 @@ class Command(Base):
|
|
|
1294
1714
|
parser_start.add_argument(i18n.t('command.start.options_short'), i18n.t('command.start.options_long'), help=i18n.t('command.start.options_help'),
|
|
1295
1715
|
nargs='*')
|
|
1296
1716
|
parser_start.add_argument(i18n.t('command.start.wait_for_start_short'), i18n.t('command.start.wait_for_start_long'),
|
|
1297
|
-
help=i18n.t('command.start.wait_for_start_help'), default=False, action=
|
|
1717
|
+
help=i18n.t('command.start.wait_for_start_help'), default=False, action='store_true')
|
|
1298
1718
|
parser_start.add_argument(i18n.t('command.start.download_short'), i18n.t('command.start.download_long'), help=i18n.t('command.start.download_help'),
|
|
1299
|
-
default=False, action=
|
|
1719
|
+
default=False, action='store_true')
|
|
1300
1720
|
parser_start.add_argument(i18n.t('command.start.remove_short'), i18n.t('command.start.remove_long'), help=i18n.t('command.start.remove_help'),
|
|
1301
|
-
default=False, action=
|
|
1721
|
+
default=False, action='store_true')
|
|
1302
1722
|
parser_start.add_argument(i18n.t('command.start.inputs_short'), i18n.t('command.start.inputs_long'), help=i18n.t('command.start.inputs_help'),
|
|
1303
|
-
default=False, action=
|
|
1723
|
+
default=False, action='store_true')
|
|
1304
1724
|
parser_start.add_argument(i18n.t('command.start.outputs_short'), i18n.t('command.start.outputs_long'), help=i18n.t('command.start.outputs_help'),
|
|
1305
|
-
default=False, action=
|
|
1725
|
+
default=False, action='store_true')
|
|
1306
1726
|
parser_start.add_argument(i18n.t('command.start.storage_short'), i18n.t('command.start.storage_long'), help=i18n.t('command.start.storage_help'),
|
|
1307
|
-
default=False, action=
|
|
1727
|
+
default=False, action='store_true')
|
|
1728
|
+
parser_start.add_argument(i18n.t('command.start.statistics_short'), i18n.t('command.start.statistics_long'), help=i18n.t('command.start.statistics_help'),
|
|
1729
|
+
default=False, action='store_true')
|
|
1308
1730
|
parser_start.add_argument(i18n.t('command.start.intermediate_short'), i18n.t('command.start.intermediate_long'),
|
|
1309
|
-
help=i18n.t('command.start.intermediate_help'), default=False, action=
|
|
1731
|
+
help=i18n.t('command.start.intermediate_help'), default=False, action='store_true')
|
|
1310
1732
|
parser_start.add_argument(i18n.t('command.start.metrics_short'), i18n.t('command.start.metrics_long'), help=i18n.t('command.start.metrics_help'),
|
|
1311
|
-
default=False, action=
|
|
1733
|
+
default=False, action='store_true')
|
|
1312
1734
|
parser_start.add_argument(i18n.t('command.start.component_short'), i18n.t('command.start.component_long'), help=i18n.t('command.start.component_help'),
|
|
1313
1735
|
nargs='*')
|
|
1314
1736
|
parser_start.add_argument(i18n.t('command.start.no_wait_for_completion_short'), i18n.t('command.start.no_wait_for_completion_long'),
|
|
1315
|
-
help=i18n.t('command.start.no_wait_for_completion_help'), default=False, action=
|
|
1737
|
+
help=i18n.t('command.start.no_wait_for_completion_help'), default=False, action='store_true')
|
|
1316
1738
|
parser_start.add_argument(i18n.t('command.start.stac_only_short'), i18n.t('command.start.stac_only_long'), help=i18n.t('command.start.stac_only_help'),
|
|
1317
|
-
default=False, action=
|
|
1739
|
+
default=False, action='store_true')
|
|
1318
1740
|
|
|
1319
|
-
#
|
|
1320
|
-
|
|
1321
|
-
parser_define.add_argument(i18n.t('command.start.inputs_short'), i18n.t('command.start.inputs_long'), help=i18n.t('command.start.inputs_help'),
|
|
1322
|
-
default=False, action="store_true")
|
|
1323
|
-
parser_define.add_argument(i18n.t('command.start.storage_short'), i18n.t('command.start.storage_long'), help=i18n.t('command.start.storage_help'),
|
|
1324
|
-
default=False, action="store_true")
|
|
1741
|
+
# Template arguments.
|
|
1742
|
+
parser_template.add_argument(i18n.t('command.template.service_long'), help=i18n.t('command.template.service_help'), nargs='+')
|
|
1325
1743
|
|
|
1326
|
-
#
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
parser_download.add_argument(i18n.t('command.start.outputs_short'), i18n.t('command.start.outputs_long'), help=i18n.t('command.start.outputs_help'),
|
|
1331
|
-
default=False, action="store_true")
|
|
1332
|
-
parser_download.add_argument(i18n.t('command.start.storage_short'), i18n.t('command.start.storage_long'), help=i18n.t('command.start.storage_help'),
|
|
1333
|
-
default=False, action="store_true")
|
|
1334
|
-
parser_download.add_argument(i18n.t('command.start.remove_short'), i18n.t('command.start.remove_long'), help=i18n.t('command.start.remove_help'),
|
|
1335
|
-
default=False, action="store_true")
|
|
1336
|
-
parser_download.add_argument(i18n.t('command.start.intermediate_short'), i18n.t('command.start.intermediate_long'),
|
|
1337
|
-
help=i18n.t('command.start.intermediate_help'), default=False, action="store_true")
|
|
1338
|
-
parser_download.add_argument(i18n.t('command.start.metrics_short'), i18n.t('command.start.metrics_long'), help=i18n.t('command.start.metrics_help'),
|
|
1339
|
-
default=False, action="store_true")
|
|
1340
|
-
parser_download.add_argument(i18n.t('command.start.component_short'), i18n.t('command.start.component_long'),
|
|
1341
|
-
help=i18n.t('command.start.component_help'), nargs='*')
|
|
1342
|
-
parser_download.add_argument(i18n.t('command.start.no_wait_for_completion_short'), i18n.t('command.start.no_wait_for_completion_long'),
|
|
1343
|
-
help=i18n.t('command.start.no_wait_for_completion_help'), default=False, action="store_true")
|
|
1344
|
-
parser_download.add_argument(i18n.t('command.start.stac_only_short'), i18n.t('command.start.stac_only_long'), help=i18n.t('command.start.stac_only_help'),
|
|
1345
|
-
default=False, action="store_true")
|
|
1744
|
+
# Upload arguments.
|
|
1745
|
+
parser_upload.add_argument(i18n.t('command.upload.upload_name_long'), help=i18n.t('command.upload.upload_name_help'))
|
|
1746
|
+
parser_upload.add_argument(i18n.t('command.upload.file_type_long'), help=i18n.t('command.upload.file_type_help'))
|
|
1747
|
+
parser_upload.add_argument(i18n.t('command.upload.file_long'), help=i18n.t('command.upload.file_help'), nargs='+')
|
|
1346
1748
|
|
|
1347
1749
|
return parser.parse_args()
|
|
1348
1750
|
|
|
1751
|
+
def __prepare_downloads(self, data_items, download_base_dir, owner=None, created_at=None, detail=None):
|
|
1752
|
+
"""
|
|
1753
|
+
Prepare each download data item by finding the constituent files, creating the download directories and calculating the input and output sizes. A STAC
|
|
1754
|
+
collection is also written using the overarching owner, created at and detail for each of the data items.
|
|
1755
|
+
|
|
1756
|
+
Data items are allocated an item number to prefix the download directory with for inputs and outputs. They also have a data type which dictates what
|
|
1757
|
+
download directory is used and whether to add the item to the input or output size lists, or create STAC item. Data types can be: _DOWNLOAD_INPUT,
|
|
1758
|
+
_DOWNLOAD_OUTPUT, _DOWNLOAD_STORAGE, _DOWNLOAD_STATISTICS or _DOWNLOAD_UPLOAD.
|
|
1759
|
+
|
|
1760
|
+
Args:
|
|
1761
|
+
data_items: A list of tuples containing the item number, data type and data model.
|
|
1762
|
+
download_base_dir: The base directory into which the downloads will be placed in a subdirectory based upon the data item name.
|
|
1763
|
+
owner: The optional owner of the STAC collection. Default None.
|
|
1764
|
+
created_at: Optionally when the STAC collection was created. Default None.
|
|
1765
|
+
detail: The optional STAC collection detail as a dictionary. Default None.
|
|
1766
|
+
|
|
1767
|
+
Returns:
|
|
1768
|
+
The list of input sizes, list of output sizes and list of downloads to be performed.
|
|
1769
|
+
"""
|
|
1770
|
+
|
|
1771
|
+
# While traversing each, save the input and output size. The size might not be immediately available for every file, so wait for all items to be analysed.
|
|
1772
|
+
input_sizes = []
|
|
1773
|
+
output_sizes = []
|
|
1774
|
+
downloads = []
|
|
1775
|
+
|
|
1776
|
+
for i, data_type, data_item in data_items:
|
|
1777
|
+
# Download the data item once it is complete.
|
|
1778
|
+
data_item.check_analysis_complete(wait=True)
|
|
1779
|
+
|
|
1780
|
+
if data_type in [Command._DOWNLOAD_INPUT, Command._DOWNLOAD_OUTPUT]:
|
|
1781
|
+
name = re.sub('_+', '_', f"{data_type.capitalize()}_{str(i)}_{self.__data_name_to_file_name(data_item.name)}").rstrip('_')
|
|
1782
|
+
else:
|
|
1783
|
+
name = '' # We do not need subdirectories for these data types.
|
|
1784
|
+
|
|
1785
|
+
download_dir = os.path.join(download_base_dir, name)
|
|
1786
|
+
os.makedirs(download_dir, exist_ok=True)
|
|
1787
|
+
|
|
1788
|
+
# Get a list of the unique file names. This will be used in determining whether we need to make the file name unique.
|
|
1789
|
+
files = [file for file in data_item.files] # So that we do not request multiple times.
|
|
1790
|
+
file_names = [file.file_name for file in files]
|
|
1791
|
+
file_names_count = {file_name: file_names.count(file_name) for file_name in file_names}
|
|
1792
|
+
|
|
1793
|
+
# Queue the files for download. We also create the corresponding STAC definitions.
|
|
1794
|
+
stac_definitions = []
|
|
1795
|
+
used_file_names = set()
|
|
1796
|
+
|
|
1797
|
+
for file in files:
|
|
1798
|
+
# We are downloading all the data item's files to a single directory, so we need to make sure the file names are unique.
|
|
1799
|
+
i = 1
|
|
1800
|
+
file_name = f"{i}-{file.file_name}" if file_names_count.get(file.file_name, 1) > 1 else file.file_name
|
|
1801
|
+
|
|
1802
|
+
while file_name in used_file_names:
|
|
1803
|
+
i += 1
|
|
1804
|
+
file_name = f"{i}-{file.file_name}"
|
|
1805
|
+
|
|
1806
|
+
used_file_names.add(file_name)
|
|
1807
|
+
path = os.path.join(download_dir, file_name)
|
|
1808
|
+
|
|
1809
|
+
# Add the download for each file for the data item.
|
|
1810
|
+
if data_type == Command._DOWNLOAD_INPUT:
|
|
1811
|
+
input_sizes.append(file.size)
|
|
1812
|
+
elif data_type == Command._DOWNLOAD_OUTPUT:
|
|
1813
|
+
output_sizes.append(file.size)
|
|
1814
|
+
|
|
1815
|
+
downloads.append((file, path, file.size))
|
|
1816
|
+
|
|
1817
|
+
# Now get the corresponding STAC item. Note that we adjust the STAC item file name to match the unique file name generated above. This is achieved
|
|
1818
|
+
# by replacing the STAC item file name.
|
|
1819
|
+
stac_item_file = DataFile._STAC_ITEM_FILE_NAME_FORMAT.format(file_name=os.path.basename(file_name))
|
|
1820
|
+
stac_item, _ = file.get_stac_item(file_name=file_name, item_file_name=stac_item_file)
|
|
1821
|
+
|
|
1822
|
+
if (data_type in [Command._DOWNLOAD_INPUT, Command._DOWNLOAD_OUTPUT, Command._DOWNLOAD_UPLOAD]) and (stac_item is not None) and (
|
|
1823
|
+
stac_item_file is not None):
|
|
1824
|
+
stac_definitions.append((stac_item, stac_item_file))
|
|
1825
|
+
|
|
1826
|
+
if len(stac_definitions) > 0:
|
|
1827
|
+
stac_collection = data_item.get_stac_collection(stac_definitions, owner=owner, created_at=created_at, detail=detail)
|
|
1828
|
+
stac_definitions.append(stac_collection)
|
|
1829
|
+
|
|
1830
|
+
# Write the STAC definitions.
|
|
1831
|
+
for stac_definition, stac_file_name in stac_definitions:
|
|
1832
|
+
with open(os.path.join(download_dir, stac_file_name), 'w') as stac_file:
|
|
1833
|
+
stac_file.write(json.dumps(stac_definition, default=json_default))
|
|
1834
|
+
|
|
1835
|
+
return input_sizes, output_sizes, downloads
|
|
1836
|
+
|
|
1349
1837
|
def __print(self, level, message, format=True, **kwargs):
|
|
1350
1838
|
"""
|
|
1351
1839
|
Prints a message to the console. This works much the same was a log message.
|
|
@@ -1502,17 +1990,103 @@ class Command(Base):
|
|
|
1502
1990
|
|
|
1503
1991
|
self.__print(logging.INFO, i18n.t('command.log_bookend'))
|
|
1504
1992
|
|
|
1505
|
-
def
|
|
1993
|
+
def __provision_process(self, process, inputs, options, dispatchers):
|
|
1506
1994
|
"""
|
|
1507
|
-
|
|
1995
|
+
Provisions a template process based upon the inputs, options and dispatchers. The supplied process template is updated and persisted.
|
|
1508
1996
|
|
|
1509
1997
|
Args:
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
The
|
|
1998
|
+
process: The template process.
|
|
1999
|
+
inputs: The list of data items to be used as inputs.
|
|
2000
|
+
options: The list of options.
|
|
2001
|
+
dispatchers: The list of dispatchers.
|
|
1514
2002
|
"""
|
|
1515
|
-
|
|
2003
|
+
|
|
2004
|
+
# Configure the process to use the inputs and set its options. Note that we filter out model level fields and set those as keyword arguments.
|
|
2005
|
+
for i, input in enumerate(inputs):
|
|
2006
|
+
process.update(input_number=i + 1, data=input)
|
|
2007
|
+
|
|
2008
|
+
kwargs = {}
|
|
2009
|
+
|
|
2010
|
+
for key, value in options.items():
|
|
2011
|
+
if key in Command._MODEL_FIELDS:
|
|
2012
|
+
kwargs[key] = value
|
|
2013
|
+
else:
|
|
2014
|
+
process.update(option_name=key, value=value, coerce_value=True)
|
|
2015
|
+
|
|
2016
|
+
if len(kwargs) > 0:
|
|
2017
|
+
process.update(**kwargs)
|
|
2018
|
+
|
|
2019
|
+
# Add in optional dispatchers.
|
|
2020
|
+
dispatchers_added = 0
|
|
2021
|
+
|
|
2022
|
+
for i, dispatcher in enumerate(process.available_dispatchers):
|
|
2023
|
+
finished = False
|
|
2024
|
+
|
|
2025
|
+
while not finished:
|
|
2026
|
+
# Either find the dispatchers from what has been loaded from the YAML file or prompt the user.
|
|
2027
|
+
add = False
|
|
2028
|
+
dispatch_intermediate = False
|
|
2029
|
+
dispatcher_options = {}
|
|
2030
|
+
|
|
2031
|
+
if dispatchers is not None:
|
|
2032
|
+
# See if any of the remaining dispatchers match this one. Once used, the corresponding dispatcher is marked as used.
|
|
2033
|
+
filtered_dispatchers = [item for item in dispatchers if
|
|
2034
|
+
(item.get(Model._FIELD_NAME) == dispatcher.name) and (not item.get(Model._FIELD_USED, False))]
|
|
2035
|
+
|
|
2036
|
+
if len(filtered_dispatchers) > 0:
|
|
2037
|
+
selected_dispatcher = filtered_dispatchers[0]
|
|
2038
|
+
selected_dispatcher[Model._FIELD_USED] = True
|
|
2039
|
+
add = True
|
|
2040
|
+
dispatch_intermediate = selected_dispatcher.get(Model._FIELD_DISPATCH_INTERMEDIATE) if selected_dispatcher.get(
|
|
2041
|
+
Model._FIELD_DISPATCH_INTERMEDIATE) is not None else False
|
|
2042
|
+
dispatcher_options = selected_dispatcher.get(Model._FIELD_OPTIONS) if selected_dispatcher.get(Model._FIELD_OPTIONS) is not None else {}
|
|
2043
|
+
|
|
2044
|
+
else:
|
|
2045
|
+
response = self.__get_input(i18n.t('command.add_dispatcher', dispatcher=dispatcher.name),
|
|
2046
|
+
constrained_values=[i18n.t('command.prompt_no'), i18n.t('command.prompt_yes')])
|
|
2047
|
+
|
|
2048
|
+
if (response is not None) and response.lower().startswith(i18n.t('command.prompt_yes').lower()[0]):
|
|
2049
|
+
add = True
|
|
2050
|
+
|
|
2051
|
+
if hasattr(dispatcher, Model._FIELD_DISPATCH_INTERMEDIATE):
|
|
2052
|
+
response = self.__get_input(i18n.t('command.dispatch_intermediate'),
|
|
2053
|
+
constrained_values=[i18n.t('command.prompt_no'), i18n.t('command.prompt_yes')])
|
|
2054
|
+
|
|
2055
|
+
if (response is not None) and response.lower().startswith(i18n.t('command.prompt_yes').lower()[0]):
|
|
2056
|
+
dispatch_intermediate = True
|
|
2057
|
+
|
|
2058
|
+
for option in dispatcher.options:
|
|
2059
|
+
finished_options = False
|
|
2060
|
+
option_value = None
|
|
2061
|
+
|
|
2062
|
+
while not finished_options:
|
|
2063
|
+
constrained_values = option.constrained_values if option.data_type == fusion_platform.DATA_TYPE_CONSTRAINED else None
|
|
2064
|
+
|
|
2065
|
+
prompt_constrained_values = i18n.t('models.process.option.representation_constrained_values',
|
|
2066
|
+
constrained_values=constrained_values) if constrained_values is not None else ''
|
|
2067
|
+
prompt_required = i18n.t('models.process.option.representation_required') if option.required else ''
|
|
2068
|
+
prompt = i18n.t('command.prompt_option', title=option.title, name=option.name, data_type=option.data_type,
|
|
2069
|
+
constrained_values=prompt_constrained_values, required=prompt_required)
|
|
2070
|
+
|
|
2071
|
+
option_value = self.__get_input(prompt, default=option.value, constrained_values=constrained_values)
|
|
2072
|
+
finished_options = (not option.required) or (
|
|
2073
|
+
option.required and (option_value is not None) and (not string_blank(str(option_value))))
|
|
2074
|
+
|
|
2075
|
+
if option_value is not None:
|
|
2076
|
+
dispatcher_options[option.name] = option_value
|
|
2077
|
+
|
|
2078
|
+
if add:
|
|
2079
|
+
process.add_dispatcher(number=(i + 1))
|
|
2080
|
+
dispatchers_added += 1
|
|
2081
|
+
|
|
2082
|
+
for option_name, option_value in dispatcher_options.items():
|
|
2083
|
+
process.update(dispatcher_number=dispatchers_added, option_name=option_name, value=option_value, coerce_value=True,
|
|
2084
|
+
dispatch_intermediate=dispatch_intermediate)
|
|
2085
|
+
else:
|
|
2086
|
+
finished = True
|
|
2087
|
+
|
|
2088
|
+
# Create the process, which will validate its options and inputs.
|
|
2089
|
+
process.create()
|
|
1516
2090
|
|
|
1517
2091
|
def remove_process_and_inputs(self, organisation, process):
|
|
1518
2092
|
"""
|
|
@@ -1633,18 +2207,22 @@ class Command(Base):
|
|
|
1633
2207
|
"""
|
|
1634
2208
|
self.__print(logging.INFO, i18n.t('command.start_process', process=process_name, service=service_name))
|
|
1635
2209
|
|
|
1636
|
-
# Upload the inputs.
|
|
1637
|
-
if (input_files is not None) and (len(input_files) > 0):
|
|
1638
|
-
data_items = self.__upload_or_find_inputs(organisation, process_name, input_files)
|
|
1639
|
-
else:
|
|
1640
|
-
data_items = []
|
|
1641
|
-
|
|
1642
2210
|
# Find the service.
|
|
1643
2211
|
service = self.get_service(organisation, service_name)
|
|
1644
2212
|
self.__print(logging.DEBUG, i18n.t('command.using_service', service=service.name))
|
|
1645
2213
|
|
|
1646
|
-
# Create
|
|
1647
|
-
process =
|
|
2214
|
+
# Create a template process from the service.
|
|
2215
|
+
process = organisation.new_process(name=process_name, service=service)
|
|
2216
|
+
|
|
2217
|
+
# Upload or find the inputs. We upload using the known file types.
|
|
2218
|
+
if (input_files is not None) and (len(input_files) > 0):
|
|
2219
|
+
file_types = [input.file_type for input in process.inputs]
|
|
2220
|
+
data_items = self.__upload_or_find_inputs(organisation, process_name, input_files, file_types)
|
|
2221
|
+
else:
|
|
2222
|
+
data_items = []
|
|
2223
|
+
|
|
2224
|
+
# Provision the process with its options, inputs and dispatchers.
|
|
2225
|
+
self.__provision_process(process, data_items, options, dispatchers)
|
|
1648
2226
|
|
|
1649
2227
|
# Upload the optional storage data items now that the process has been created because we need the process id.
|
|
1650
2228
|
if (storage_files is not None) and (len(storage_files) > 0):
|
|
@@ -1661,7 +2239,111 @@ class Command(Base):
|
|
|
1661
2239
|
|
|
1662
2240
|
return process
|
|
1663
2241
|
|
|
1664
|
-
def
|
|
2242
|
+
def template_service(self, organisation, service_name):
|
|
2243
|
+
"""
|
|
2244
|
+
Obtains the service definition for a service.
|
|
2245
|
+
|
|
2246
|
+
Args:
|
|
2247
|
+
organisation: The logged in organisation.
|
|
2248
|
+
service_name: The name of the service to be defined. This can also be the id or SSD id.
|
|
2249
|
+
|
|
2250
|
+
Returns:
|
|
2251
|
+
The service definition.
|
|
2252
|
+
"""
|
|
2253
|
+
self.__print(logging.INFO, i18n.t('command.template_service', service=service_name))
|
|
2254
|
+
|
|
2255
|
+
# We are defining a service. This will raise an exception if the service is not found.
|
|
2256
|
+
service = self.get_service(organisation, service_name)
|
|
2257
|
+
|
|
2258
|
+
# Create a template process from the service so that we can obtain the options, inputs and dispatchers.
|
|
2259
|
+
process_name = f"{service.name} {datetime.now(timezone.utc).isoformat()}"
|
|
2260
|
+
process = organisation.new_process(name=process_name, service=service)
|
|
2261
|
+
|
|
2262
|
+
# Get the options and add in any model-level attributes that are required. Any missing attributes are set to None.
|
|
2263
|
+
options = {option.name: self.__format_value(option.value, data_type=option.data_type) for option in process.options}
|
|
2264
|
+
|
|
2265
|
+
for key in Command._MODEL_FIELDS:
|
|
2266
|
+
options[key] = self.__format_value(getattr(process, key)) if hasattr(process, key) else self.__format_value(None)
|
|
2267
|
+
|
|
2268
|
+
# Add in the inputs.
|
|
2269
|
+
inputs = []
|
|
2270
|
+
|
|
2271
|
+
for i, input in enumerate(process.inputs):
|
|
2272
|
+
title = input.title if hasattr(input, Model._FIELD_TITLE) and (input.title is not None) else str(i + 1)
|
|
2273
|
+
file_type = input.file_type if hasattr(input, Model._FIELD_FILE_TYPE) and (input.file_type is not None) else fusion_platform.FILE_TYPE_OTHER
|
|
2274
|
+
extension = Command._FILE_TYPES_TO_EXTENSIONS.get(file_type)
|
|
2275
|
+
extension = extension[0] if (extension is not None) and len(extension) > 0 else None
|
|
2276
|
+
file = f"{self.__data_name_to_file_name(title.lower())}{extension if extension is not None else ''}"
|
|
2277
|
+
inputs.append({Model._FIELD_NAME: title, Model._FIELD_FILE: file})
|
|
2278
|
+
|
|
2279
|
+
# Add in all available dispatchers and their options.
|
|
2280
|
+
dispatchers = []
|
|
2281
|
+
|
|
2282
|
+
for dispatcher in process.available_dispatchers:
|
|
2283
|
+
found_dispatcher = {Model._FIELD_NAME: dispatcher.name}
|
|
2284
|
+
found_dispatcher[Model._FIELD_OPTIONS] = {option.name: self.__format_value(option.value, data_type=option.data_type) for option in dispatcher.options}
|
|
2285
|
+
dispatchers.append(found_dispatcher)
|
|
2286
|
+
|
|
2287
|
+
# Build the definition.
|
|
2288
|
+
service_definition = {
|
|
2289
|
+
Command._YAML_SERVICE_NAME: service.name,
|
|
2290
|
+
Command._YAML_PROCESS_NAME: process_name,
|
|
2291
|
+
Command._YAML_INPUTS: inputs,
|
|
2292
|
+
Command._YAML_OPTIONS: options,
|
|
2293
|
+
Command._YAML_DISPATCHERS: dispatchers
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
return service_definition
|
|
2297
|
+
|
|
2298
|
+
def upload(self, organisation, upload_name, file_type, files):
|
|
2299
|
+
"""
|
|
2300
|
+
Creates a new upload item with the specified file type and uploads the files to it.
|
|
2301
|
+
|
|
2302
|
+
Args:
|
|
2303
|
+
organisation: The logged in organisation.
|
|
2304
|
+
upload_name: The name of the upload to create.
|
|
2305
|
+
file_type: The file type to be associated with the files.
|
|
2306
|
+
files: The files to be uploaded.
|
|
2307
|
+
"""
|
|
2308
|
+
|
|
2309
|
+
# Make sure the file type is valid.
|
|
2310
|
+
validated_file_types = [
|
|
2311
|
+
item for item in list(Command._FILE_TYPES_TO_EXTENSIONS.keys()) + [fusion_platform.FILE_TYPE_OTHER] if file_type.lower() == item.lower()
|
|
2312
|
+
]
|
|
2313
|
+
|
|
2314
|
+
if len(validated_file_types) <= 0:
|
|
2315
|
+
raise CommandError(i18n.t('command.unknown_file_type', file_type=file_type))
|
|
2316
|
+
|
|
2317
|
+
validated_file_type = validated_file_types[0]
|
|
2318
|
+
|
|
2319
|
+
# Check that all the files exist and that they have a valid extension.
|
|
2320
|
+
total_size = 0
|
|
2321
|
+
|
|
2322
|
+
for file in files:
|
|
2323
|
+
if not os.path.exists(file):
|
|
2324
|
+
raise CommandError(i18n.t('command.missing_file', file=file))
|
|
2325
|
+
else:
|
|
2326
|
+
total_size += os.path.getsize(file)
|
|
2327
|
+
|
|
2328
|
+
# Check the extension for anything but the "Other" file type.
|
|
2329
|
+
if validated_file_type != fusion_platform.FILE_TYPE_OTHER:
|
|
2330
|
+
extension = os.path.splitext(file.lower())[1]
|
|
2331
|
+
|
|
2332
|
+
if extension not in Command._FILE_TYPES_TO_EXTENSIONS[validated_file_type]:
|
|
2333
|
+
raise CommandError(i18n.t('command.invalid_file_extension', file=file, extension=extension, file_type=validated_file_type))
|
|
2334
|
+
|
|
2335
|
+
# Make sure we have files to upload.
|
|
2336
|
+
if len(files) <= 0:
|
|
2337
|
+
raise CommandError(i18n.t('command.no_upload_files'))
|
|
2338
|
+
|
|
2339
|
+
# Create the required item. The upload has a progress bar.
|
|
2340
|
+
self.__print(logging.INFO, i18n.t('command.upload_files', upload_name=upload_name, file_type=validated_file_type, number_of_files=len(files)))
|
|
2341
|
+
created = organisation.create_data(name=upload_name, file_type=validated_file_type, files=files, wait=False)
|
|
2342
|
+
|
|
2343
|
+
# Wait for the analysis of any created items to complete.
|
|
2344
|
+
self.__wait_for_uploads([(created, False, 0, total_size)])
|
|
2345
|
+
|
|
2346
|
+
def __upload_or_find_inputs(self, organisation, process_name, input_files, file_types):
|
|
1665
2347
|
"""
|
|
1666
2348
|
Uploads or finds the inputs ready to start a process.
|
|
1667
2349
|
|
|
@@ -1669,10 +2351,13 @@ class Command(Base):
|
|
|
1669
2351
|
organisation: The logged in organisation.
|
|
1670
2352
|
process_name: The name of the process.
|
|
1671
2353
|
input_files: The list of input files to be uploaded or pre-existing input names to be found.
|
|
2354
|
+
file_type: The list of file types to be associated with the inputs.
|
|
1672
2355
|
|
|
1673
2356
|
Returns:
|
|
1674
2357
|
The found data items in the same order as the input files.
|
|
1675
2358
|
"""
|
|
2359
|
+
file_types = [] if file_types is None else file_types
|
|
2360
|
+
file_types = file_types + ([None] * (len(input_files) - len(file_types)))
|
|
1676
2361
|
data_items = [None] * len(input_files)
|
|
1677
2362
|
find_items = []
|
|
1678
2363
|
create_items = []
|
|
@@ -1698,29 +2383,16 @@ class Command(Base):
|
|
|
1698
2383
|
extension = None
|
|
1699
2384
|
|
|
1700
2385
|
if extension is not None:
|
|
1701
|
-
# Create a data item for each input.
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
file_type = fusion_platform.FILE_TYPE_KML
|
|
1712
|
-
elif extension == '.kmz':
|
|
1713
|
-
file_type = fusion_platform.FILE_TYPE_KMZ
|
|
1714
|
-
elif extension == '.csv':
|
|
1715
|
-
file_type = fusion_platform.FILE_TYPE_CSV
|
|
1716
|
-
elif extension == '.zip':
|
|
1717
|
-
file_type = fusion_platform.FILE_TYPE_ESRI_SHAPEFILE
|
|
1718
|
-
elif (extension == '.jpeg') or (extension == '.jpg'):
|
|
1719
|
-
file_type = fusion_platform.FILE_TYPE_JPEG
|
|
1720
|
-
elif extension == '.png':
|
|
1721
|
-
file_type = fusion_platform.FILE_TYPE_PNG
|
|
1722
|
-
else:
|
|
1723
|
-
file_type = fusion_platform.FILE_TYPE_OTHER
|
|
2386
|
+
# Create a data item for each input. We try to use the supplied file types, but if they are not available, we guess the file type from the extension.
|
|
2387
|
+
file_type = file_types[i]
|
|
2388
|
+
|
|
2389
|
+
if file_type is None:
|
|
2390
|
+
for key, value in Command._FILE_TYPES_TO_EXTENSIONS.items():
|
|
2391
|
+
if extension in value:
|
|
2392
|
+
file_type = key
|
|
2393
|
+
break
|
|
2394
|
+
|
|
2395
|
+
file_type = fusion_platform.FILE_TYPE_OTHER if file_type is None else file_type
|
|
1724
2396
|
|
|
1725
2397
|
input_name = f"{process_name} {i + 1}" if input_name is None else input_name
|
|
1726
2398
|
create_items.append((i, input_name, file_type, filename))
|
|
@@ -1773,7 +2445,7 @@ class Command(Base):
|
|
|
1773
2445
|
|
|
1774
2446
|
Args:
|
|
1775
2447
|
organisation: The logged in organisation.
|
|
1776
|
-
storage_files: The
|
|
2448
|
+
storage_files: The dictionary of storage files to be uploaded.
|
|
1777
2449
|
process: The newly created process.
|
|
1778
2450
|
"""
|
|
1779
2451
|
self.__print(logging.INFO, i18n.t('command.upload_storage', files=len(storage_files)))
|
|
@@ -1801,7 +2473,8 @@ class Command(Base):
|
|
|
1801
2473
|
created = organisation.create_data(name=storage_name, file_type=fusion_platform.FILE_TYPE_GZIP, files=[upload_file], wait=False, id=storage_id)
|
|
1802
2474
|
created_items.append((created, False, 0, os.path.getsize(upload_file)))
|
|
1803
2475
|
|
|
1804
|
-
os.
|
|
2476
|
+
if os.path.exists(upload_file):
|
|
2477
|
+
os.remove(upload_file)
|
|
1805
2478
|
|
|
1806
2479
|
# Wait for the analysis of any created items to complete.
|
|
1807
2480
|
self.__wait_for_uploads(created_items)
|