fusion-platform-python-sdk 1.15.12__tar.gz → 1.16.6__tar.gz
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_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/LICENSE.txt +1 -1
- {fusion_platform_python_sdk-1.15.12/fusion_platform_python_sdk.egg-info → fusion_platform_python_sdk-1.16.6}/PKG-INFO +11 -2
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/__init__.py +2 -2
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/command.py +296 -158
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/fusion_platform_sdk.pdf +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/data.py +36 -1
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/data_file.md +2 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/data_file.py +9 -131
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/model.py +2 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/process_execution.py +7 -2
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/session.py +35 -1
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/translations.py +6 -2
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6/fusion_platform_python_sdk.egg-info}/PKG-INFO +11 -2
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/setup.py +1 -1
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/tests/test_session.py +6 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/MANIFEST.in +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/README.md +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/__main__.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/base.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/common/__init__.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/common/raise_thread.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/common/utilities.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/detailed_example.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/documentation.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/glasgow.geojson +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/lake_district.geojson +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/localisations.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/localise.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/__init__.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/credit.md +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/credit.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/data.md +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/fields.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/organisation.md +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/organisation.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/process.md +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/process.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/process_execution.md +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/process_service_execution.md +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/process_service_execution.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/process_service_execution_log.md +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/process_service_execution_log.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/service.md +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/service.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/user.md +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/models/user.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/quick_example.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform_python_sdk.egg-info/SOURCES.txt +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform_python_sdk.egg-info/dependency_links.txt +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform_python_sdk.egg-info/entry_points.txt +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform_python_sdk.egg-info/requires.txt +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform_python_sdk.egg-info/top_level.txt +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/setup.cfg +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/tests/test_base.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/tests/test_command.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/tests/test_init.py +0 -0
- {fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/tests/test_localise.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2025 Digital Content Analysis Technology Ltd
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: fusion-platform-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.6
|
|
4
4
|
Summary: Python SDK used to interact with the Fusion Platform(r)
|
|
5
5
|
Home-page: https://github.com/d-cat-support/fusion-platform-python-sdk
|
|
6
6
|
Author: Digital Content Analysis Technology Ltd
|
|
@@ -24,6 +24,15 @@ Requires-Dist: python-i18n[YAML]
|
|
|
24
24
|
Requires-Dist: requests
|
|
25
25
|
Requires-Dist: tenacity
|
|
26
26
|
Requires-Dist: tqdm
|
|
27
|
+
Dynamic: author
|
|
28
|
+
Dynamic: author-email
|
|
29
|
+
Dynamic: classifier
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: license
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: summary
|
|
27
36
|
|
|
28
37
|
# Fusion Platform<sup>®</sup> Python SDK
|
|
29
38
|
|
{fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/__init__.py
RENAMED
|
@@ -11,8 +11,8 @@ API, and this is defined within the corresponding OpenAPI 3.0 specification, whi
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
# Do not modify the following two lines as they are maintained by the version.sh script.
|
|
14
|
-
__version__ = '1.
|
|
15
|
-
__version_date__ = '
|
|
14
|
+
__version__ = '1.16.6'
|
|
15
|
+
__version_date__ = '2025-02-11T12:17:15Z'
|
|
16
16
|
|
|
17
17
|
# Exclude certain sub-modules from documentation.
|
|
18
18
|
# @formatter:off
|
{fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/command.py
RENAMED
|
@@ -126,6 +126,11 @@ class Command(Base):
|
|
|
126
126
|
_YAML_OPTIONS = 'options'
|
|
127
127
|
_YAML_DISPATCHERS = 'dispatchers'
|
|
128
128
|
|
|
129
|
+
# Progress bar constants.
|
|
130
|
+
__PROGRESS_BAR_FORMAT = '{l_bar}{bar:50}{r_bar}'
|
|
131
|
+
__PROGRESS_BAR_COLOUR_ACTIVE = '#3379B6'
|
|
132
|
+
__PROGRESS_BAR_COLOUR_COMPLETE = '#5BB85C'
|
|
133
|
+
|
|
129
134
|
def __init__(self):
|
|
130
135
|
"""
|
|
131
136
|
Initialises the object.
|
|
@@ -271,26 +276,33 @@ class Command(Base):
|
|
|
271
276
|
extracted_inputs = [(k + 1, input) for k, input in enumerate(process.inputs)]
|
|
272
277
|
|
|
273
278
|
self.__print(logging.INFO, i18n.t('command.define_inputs', files=len(extracted_inputs)))
|
|
274
|
-
progress_bar = tqdm(total=len(extracted_inputs),
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
# Get the data item for this input.
|
|
278
|
-
data, _ = organisation.find_data(id=input.id)
|
|
279
|
-
data.check_analysis_complete(wait=True)
|
|
280
|
-
|
|
281
|
-
# Get the list of files which we filter to only use those files that are not sources. This will include any files which do not have an alternative.
|
|
282
|
-
data_files = [data_file for data_file in data.files if data_file.file_type.lower().strip() == input.file_type.lower().strip()]
|
|
283
|
-
file_ids_to_ignore = [str(data_file.source) for data_file in data_files if hasattr(data_file, 'source') and (data_file.source is not None)]
|
|
284
|
-
data_files = [data_file for data_file in data_files if str(data_file.file_id) not in file_ids_to_ignore]
|
|
279
|
+
progress_bar = tqdm(total=len(extracted_inputs), bar_format=Command.__PROGRESS_BAR_FORMAT, colour=Command.__PROGRESS_BAR_COLOUR_ACTIVE,
|
|
280
|
+
unit='input') # Unit matches keyword argument.
|
|
281
|
+
progress_bar.set_postfix(input=1)
|
|
285
282
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
283
|
+
try:
|
|
284
|
+
for k, input in extracted_inputs:
|
|
285
|
+
# Get the data item for this input.
|
|
286
|
+
data, _ = organisation.find_data(id=input.id)
|
|
287
|
+
data.check_analysis_complete(wait=True)
|
|
288
|
+
|
|
289
|
+
# Get the list of files which we filter to only use those files that are not sources. This will include any files which do not have an alternative.
|
|
290
|
+
data_files = [data_file for data_file in data.files if data_file.file_type.lower().strip() == input.file_type.lower().strip()]
|
|
291
|
+
file_ids_to_ignore = [str(data_file.source) for data_file in data_files if hasattr(data_file, 'source') and (data_file.source is not None)]
|
|
292
|
+
data_files = [data_file for data_file in data_files if str(data_file.file_id) not in file_ids_to_ignore]
|
|
293
|
+
|
|
294
|
+
# Build the list of downloads. Here we take just the first file found on the assumption that there should be only one.
|
|
295
|
+
if len(data_files) > 0:
|
|
296
|
+
downloads.append((data_files[0], data_files[0].file_name, data_files[0].size))
|
|
297
|
+
inputs.append({Model._FIELD_NAME: data.name, Model._FIELD_FILE: data_files[0].file_name})
|
|
298
|
+
|
|
299
|
+
progress_bar.set_postfix(input=k)
|
|
300
|
+
progress_bar.update(1)
|
|
292
301
|
|
|
293
|
-
|
|
302
|
+
finally:
|
|
303
|
+
progress_bar.set_postfix()
|
|
304
|
+
progress_bar.colour = Command.__PROGRESS_BAR_COLOUR_COMPLETE
|
|
305
|
+
progress_bar.close()
|
|
294
306
|
|
|
295
307
|
else:
|
|
296
308
|
# Just get the names of the inputs.
|
|
@@ -306,29 +318,36 @@ class Command(Base):
|
|
|
306
318
|
non_aggregator_chains = process.chains if hasattr(process, Model._FIELD_CHAINS) else []
|
|
307
319
|
|
|
308
320
|
self.__print(logging.INFO, i18n.t('command.define_storage', slices=non_aggregator_count))
|
|
309
|
-
progress_bar = tqdm(total=non_aggregator_count,
|
|
321
|
+
progress_bar = tqdm(total=non_aggregator_count, bar_format=Command.__PROGRESS_BAR_FORMAT, colour=Command.__PROGRESS_BAR_COLOUR_ACTIVE,
|
|
322
|
+
unit='slice') # Unit matches keyword argument.
|
|
323
|
+
progress_bar.set_postfix(slice=1)
|
|
310
324
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
325
|
+
try:
|
|
326
|
+
for group_index in range(1, non_aggregator_count + 1):
|
|
327
|
+
ssd_ids = [(i, non_aggregator_chains[i].get(Model._FIELD_SSD_ID)) for i in range(len(non_aggregator_chains))]
|
|
328
|
+
chain_digits = len(str(len(ssd_ids)))
|
|
314
329
|
|
|
315
|
-
|
|
316
|
-
|
|
330
|
+
for chain_index, ssd_id in ssd_ids:
|
|
331
|
+
storage_data_item = self.__get_storage_data_item(process, group_index, non_aggregator_count, ssd_id, chain_index)
|
|
317
332
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
333
|
+
if storage_data_item is not None:
|
|
334
|
+
# Add the storage file to the downloads. We assume there are zero or one file for the storage data item.
|
|
335
|
+
storage_data_item.check_analysis_complete(wait=True)
|
|
336
|
+
storage_data_file = next(storage_data_item.files, None)
|
|
322
337
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
338
|
+
if storage_data_file is not None:
|
|
339
|
+
path = f"{str(group_index).zfill(non_aggregator_digits)}_{str(chain_index).zfill(chain_digits)}_{storage_data_file.file_name.split('_')[-1]}"
|
|
340
|
+
key = f"{group_index}_{non_aggregator_count}_{ssd_id}_{chain_index}"
|
|
341
|
+
downloads.append((storage_data_file, path, storage_data_file.size))
|
|
342
|
+
storage[key] = path
|
|
328
343
|
|
|
329
|
-
|
|
344
|
+
progress_bar.set_postfix(slice=group_index)
|
|
345
|
+
progress_bar.update(1)
|
|
330
346
|
|
|
331
|
-
|
|
347
|
+
finally:
|
|
348
|
+
progress_bar.set_postfix()
|
|
349
|
+
progress_bar.colour = Command.__PROGRESS_BAR_COLOUR_COMPLETE
|
|
350
|
+
progress_bar.close()
|
|
332
351
|
|
|
333
352
|
# Download the files.
|
|
334
353
|
self.__download_files(downloads)
|
|
@@ -539,11 +558,16 @@ class Command(Base):
|
|
|
539
558
|
elif data_type == Command._DOWNLOAD_OUTPUT:
|
|
540
559
|
output_sizes.append(file.size)
|
|
541
560
|
|
|
542
|
-
downloads.append((file, path))
|
|
543
|
-
|
|
561
|
+
downloads.append((file, path, file.size))
|
|
562
|
+
stac_item, stac_item_file = file.get_stac_item()
|
|
544
563
|
|
|
545
|
-
|
|
546
|
-
|
|
564
|
+
if (stac_item is not None) and (stac_item_file is not None):
|
|
565
|
+
stac_definitions.append((stac_item, stac_item_file))
|
|
566
|
+
|
|
567
|
+
if len(stac_definitions) > 0:
|
|
568
|
+
stac_collection = data_item.get_stac_collection(stac_definitions, owner=component.name, created_at=component.created_at,
|
|
569
|
+
detail=component_options)
|
|
570
|
+
stac_definitions.append(stac_collection)
|
|
547
571
|
|
|
548
572
|
# Write the STAC definitions.
|
|
549
573
|
for stac_definition, stac_file_name in stac_definitions:
|
|
@@ -561,29 +585,68 @@ class Command(Base):
|
|
|
561
585
|
Downloads the files from the downloads list.
|
|
562
586
|
|
|
563
587
|
Args:
|
|
564
|
-
downloads: A list of tuples with the file model
|
|
588
|
+
downloads: A list of tuples with the file model, the download path and file size.
|
|
565
589
|
"""
|
|
566
590
|
downloads = [] if downloads is None else downloads
|
|
567
591
|
|
|
568
592
|
if len(downloads) > 0:
|
|
569
|
-
# Report progress.
|
|
570
593
|
self.__print(logging.INFO, i18n.t('command.download_files', files=len(downloads)))
|
|
571
|
-
progress_bar = tqdm(total=len(downloads), ncols=100)
|
|
572
594
|
|
|
573
|
-
#
|
|
574
|
-
|
|
595
|
+
# Initialise the progress bar to show bytes downloaded.
|
|
596
|
+
total_size = sum([size for _, _, size in downloads if size is not None])
|
|
597
|
+
progress_bar = tqdm(total=total_size, bar_format=Command.__PROGRESS_BAR_FORMAT, colour=Command.__PROGRESS_BAR_COLOUR_ACTIVE, unit='B', unit_scale=True,
|
|
598
|
+
unit_divisor=1024) # Show progress in bytes.
|
|
575
599
|
|
|
576
|
-
|
|
577
|
-
#
|
|
578
|
-
|
|
579
|
-
file.download(path=path, wait=False)
|
|
600
|
+
try:
|
|
601
|
+
# Do all the downloads in batches. We use small batches so that we do not swamp memory.
|
|
602
|
+
batch_size = 2 * os.cpu_count() # Downloads don't swamp CPU.
|
|
580
603
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
progress_bar.update(1)
|
|
584
|
-
file.download_complete(wait=True)
|
|
604
|
+
for batch in [downloads[i:i + batch_size] for i in range(0, len(downloads), batch_size)]:
|
|
605
|
+
download_files = []
|
|
585
606
|
|
|
586
|
-
|
|
607
|
+
# Start downloading the batch.
|
|
608
|
+
for file, path, size in batch:
|
|
609
|
+
file.download(path=path, wait=False)
|
|
610
|
+
download_files.append((file, False, 0, size))
|
|
611
|
+
|
|
612
|
+
# Check the status of the batch of downloads so that we know when we have finished, updating the progress bar as we go.
|
|
613
|
+
finished = False
|
|
614
|
+
last_total_current_size = 0
|
|
615
|
+
|
|
616
|
+
while not finished:
|
|
617
|
+
# Wait a bit before checking progress again.
|
|
618
|
+
sleep(0.25)
|
|
619
|
+
|
|
620
|
+
# Now work out haw far we've got.
|
|
621
|
+
total_current_size = 0
|
|
622
|
+
count_complete = 0
|
|
623
|
+
|
|
624
|
+
for i in range(len(download_files)):
|
|
625
|
+
file, complete, current_size, download_size = download_files[i]
|
|
626
|
+
|
|
627
|
+
if not complete:
|
|
628
|
+
# Get the current download size.
|
|
629
|
+
_, _, current_size = file.download_progress()
|
|
630
|
+
|
|
631
|
+
# Now check for completion.
|
|
632
|
+
complete = file.download_complete()
|
|
633
|
+
download_files[i] = (file, complete, current_size, download_size)
|
|
634
|
+
|
|
635
|
+
count_complete += 1 if complete else 0
|
|
636
|
+
total_current_size += current_size
|
|
637
|
+
|
|
638
|
+
# Increment the progress bar.
|
|
639
|
+
update_current_size = total_current_size - last_total_current_size
|
|
640
|
+
last_total_current_size = total_current_size
|
|
641
|
+
progress_bar.update(update_current_size)
|
|
642
|
+
|
|
643
|
+
# Check if we have finished.
|
|
644
|
+
finished = count_complete == len(download_files)
|
|
645
|
+
|
|
646
|
+
finally:
|
|
647
|
+
# Make sure the progress bar is closed.
|
|
648
|
+
progress_bar.colour = Command.__PROGRESS_BAR_COLOUR_COMPLETE
|
|
649
|
+
progress_bar.close()
|
|
587
650
|
|
|
588
651
|
def download_process_execution(self, organisation, process_name, download_inputs, download_outputs, download_storage, download_intermediate,
|
|
589
652
|
download_components, save_metrics):
|
|
@@ -645,19 +708,26 @@ class Command(Base):
|
|
|
645
708
|
# Do all the downloads in batches. We use small batches so that we do not swamp memory.
|
|
646
709
|
batch_size = 2 * os.cpu_count() # Downloads don't swamp CPU.
|
|
647
710
|
self.__print(logging.INFO, i18n.t('command.download_executions', executions=len(executions)))
|
|
648
|
-
progress_bar = tqdm(total=len(executions),
|
|
711
|
+
progress_bar = tqdm(total=len(executions), bar_format=Command.__PROGRESS_BAR_FORMAT, colour=Command.__PROGRESS_BAR_COLOUR_ACTIVE,
|
|
712
|
+
unit='execution') # Unit matches keyword argument.
|
|
713
|
+
progress_bar.set_postfix(execution=1)
|
|
649
714
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
715
|
+
try:
|
|
716
|
+
# Now download each execution in batches.
|
|
717
|
+
partial_download_execution = partial(self.__download_execution, process, download_inputs, download_outputs, download_storage, download_intermediate,
|
|
718
|
+
download_components, output_dir)
|
|
719
|
+
results = []
|
|
720
|
+
|
|
721
|
+
with Pool(nodes=batch_size) as pool:
|
|
722
|
+
for i, result in enumerate(pool.uimap(partial_download_execution, executions)):
|
|
723
|
+
progress_bar.set_postfix(execution=i + 1)
|
|
724
|
+
progress_bar.update(1)
|
|
725
|
+
results.append(result)
|
|
659
726
|
|
|
660
|
-
|
|
727
|
+
finally:
|
|
728
|
+
progress_bar.set_postfix()
|
|
729
|
+
progress_bar.colour = Command.__PROGRESS_BAR_COLOUR_COMPLETE
|
|
730
|
+
progress_bar.close()
|
|
661
731
|
|
|
662
732
|
# Combine the results.
|
|
663
733
|
downloads = []
|
|
@@ -1173,9 +1243,14 @@ class Command(Base):
|
|
|
1173
1243
|
# Form the status of the execution.
|
|
1174
1244
|
status = ''
|
|
1175
1245
|
group = ''
|
|
1246
|
+
abort_reason = i18n.t('command.log_execution_abort_reason', abort_reason=execution.abort_reason) if hasattr(execution, Model._FIELD_ABORT_REASON) else ''
|
|
1247
|
+
exit_type = i18n.t('command.log_execution_exit_type', exit_type=execution.exit_type) if hasattr(execution, Model._FIELD_EXIT_TYPE) else ''
|
|
1176
1248
|
|
|
1177
1249
|
if execution.success:
|
|
1178
|
-
|
|
1250
|
+
if len(abort_reason) <= 0:
|
|
1251
|
+
status = i18n.t('command.log_execution_success')
|
|
1252
|
+
else:
|
|
1253
|
+
status = i18n.t('command.log_execution_warning', exit_type=exit_type, abort_reason=abort_reason)
|
|
1179
1254
|
|
|
1180
1255
|
elif execution.stopped:
|
|
1181
1256
|
status = i18n.t('command.log_execution_stopped')
|
|
@@ -1184,9 +1259,6 @@ class Command(Base):
|
|
|
1184
1259
|
status = i18n.t('command.log_execution_progress', progress=execution.progress)
|
|
1185
1260
|
|
|
1186
1261
|
else:
|
|
1187
|
-
abort_reason = i18n.t('command.log_execution_abort_reason', abort_reason=execution.abort_reason) if hasattr(execution,
|
|
1188
|
-
Model._FIELD_ABORT_REASON) else ''
|
|
1189
|
-
exit_type = i18n.t('command.log_execution_exit_type', exit_type=execution.exit_type) if hasattr(execution, Model._FIELD_EXIT_TYPE) else ''
|
|
1190
1262
|
status = i18n.t('command.log_execution_failed', exit_type=exit_type, abort_reason=abort_reason)
|
|
1191
1263
|
|
|
1192
1264
|
started_at = execution.started_at.strftime('%Y-%m-%d %H:%M:%S')
|
|
@@ -1475,78 +1547,97 @@ class Command(Base):
|
|
|
1475
1547
|
Returns:
|
|
1476
1548
|
The found data items in the same order as the input files.
|
|
1477
1549
|
"""
|
|
1478
|
-
data_items = []
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1550
|
+
data_items = [None] * len(input_files)
|
|
1551
|
+
find_items = []
|
|
1552
|
+
create_items = []
|
|
1553
|
+
|
|
1554
|
+
for i, input_file in enumerate(input_files if input_files is not None else []):
|
|
1555
|
+
# The input file can either be a string representing a name or a file, or a dictionary with explicit values for both.
|
|
1556
|
+
input_name = None
|
|
1557
|
+
filename = None
|
|
1558
|
+
extension = None
|
|
1559
|
+
|
|
1560
|
+
if isinstance(input_file, dict):
|
|
1561
|
+
input_name = input_file.get(Model._FIELD_NAME)
|
|
1562
|
+
filename = input_file.get(Model._FIELD_FILE)
|
|
1563
|
+
extension = os.path.splitext(filename.lower())[1] if filename is not None else None
|
|
1564
|
+
else:
|
|
1565
|
+
# An input name is not assumed to have an extension.
|
|
1566
|
+
extension = os.path.splitext(input_file.lower())[1]
|
|
1483
1567
|
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
# The input file can either be a string representing a name or a file, or a dictionary with explicit values for both.
|
|
1487
|
-
input_name = None
|
|
1488
|
-
filename = None
|
|
1489
|
-
extension = None
|
|
1490
|
-
|
|
1491
|
-
if isinstance(input_file, dict):
|
|
1492
|
-
input_name = input_file.get(Model._FIELD_NAME)
|
|
1493
|
-
filename = input_file.get(Model._FIELD_FILE)
|
|
1494
|
-
extension = os.path.splitext(filename.lower())[1]
|
|
1568
|
+
if len(extension) > 0:
|
|
1569
|
+
filename = input_file
|
|
1495
1570
|
else:
|
|
1496
|
-
|
|
1497
|
-
extension =
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
elif extension == '.zip':
|
|
1522
|
-
file_type = fusion_platform.FILE_TYPE_ESRI_SHAPEFILE
|
|
1523
|
-
elif (extension == '.jpeg') or (extension == '.jpg'):
|
|
1524
|
-
file_type = fusion_platform.FILE_TYPE_JPEG
|
|
1525
|
-
elif extension == '.png':
|
|
1526
|
-
file_type = fusion_platform.FILE_TYPE_PNG
|
|
1527
|
-
else:
|
|
1528
|
-
file_type = fusion_platform.FILE_TYPE_OTHER
|
|
1529
|
-
|
|
1530
|
-
input_name = f"{process_name} {i + 1}" if input_name is None else input_name
|
|
1531
|
-
created = organisation.create_data(name=input_name, file_type=file_type, files=[filename], wait=False)
|
|
1532
|
-
data_items.append(created)
|
|
1533
|
-
created_items.append(created)
|
|
1571
|
+
input_name = input_file
|
|
1572
|
+
extension = None
|
|
1573
|
+
|
|
1574
|
+
if extension is not None:
|
|
1575
|
+
# Create a data item for each input.
|
|
1576
|
+
if (extension == '.tif') or (extension == '.tiff'):
|
|
1577
|
+
file_type = fusion_platform.FILE_TYPE_GEOTIFF
|
|
1578
|
+
elif extension == '.jp2':
|
|
1579
|
+
file_type = fusion_platform.FILE_TYPE_JPEG2000
|
|
1580
|
+
elif extension == '.dem':
|
|
1581
|
+
file_type = fusion_platform.FILE_TYPE_DEM
|
|
1582
|
+
elif (extension == '.json') or (extension == '.geojson'):
|
|
1583
|
+
file_type = fusion_platform.FILE_TYPE_GEOJSON
|
|
1584
|
+
elif extension == '.kml':
|
|
1585
|
+
file_type = fusion_platform.FILE_TYPE_KML
|
|
1586
|
+
elif extension == '.kmz':
|
|
1587
|
+
file_type = fusion_platform.FILE_TYPE_KMZ
|
|
1588
|
+
elif extension == '.csv':
|
|
1589
|
+
file_type = fusion_platform.FILE_TYPE_CSV
|
|
1590
|
+
elif extension == '.zip':
|
|
1591
|
+
file_type = fusion_platform.FILE_TYPE_ESRI_SHAPEFILE
|
|
1592
|
+
elif (extension == '.jpeg') or (extension == '.jpg'):
|
|
1593
|
+
file_type = fusion_platform.FILE_TYPE_JPEG
|
|
1594
|
+
elif extension == '.png':
|
|
1595
|
+
file_type = fusion_platform.FILE_TYPE_PNG
|
|
1534
1596
|
else:
|
|
1597
|
+
file_type = fusion_platform.FILE_TYPE_OTHER
|
|
1598
|
+
|
|
1599
|
+
input_name = f"{process_name} {i + 1}" if input_name is None else input_name
|
|
1600
|
+
create_items.append((i, input_name, file_type, filename))
|
|
1601
|
+
else:
|
|
1602
|
+
find_items.append((i, input_name))
|
|
1603
|
+
|
|
1604
|
+
# Find all required items with a progress bar.
|
|
1605
|
+
if len(find_items) > 0:
|
|
1606
|
+
self.__print(logging.INFO, i18n.t('command.find_inputs', inputs=len(find_items)))
|
|
1607
|
+
progress_bar = tqdm(total=len(find_items), bar_format=Command.__PROGRESS_BAR_FORMAT, colour=Command.__PROGRESS_BAR_COLOUR_ACTIVE,
|
|
1608
|
+
unit='input') # Unit matches keyword argument.
|
|
1609
|
+
progress_bar.set_postfix(input=1)
|
|
1610
|
+
k = 1
|
|
1611
|
+
|
|
1612
|
+
try:
|
|
1613
|
+
for i, input_name in find_items:
|
|
1535
1614
|
data_item, _ = organisation.find_data(name=input_name)
|
|
1536
1615
|
|
|
1537
1616
|
if data_item is None:
|
|
1538
1617
|
raise CommandError(i18n.t('command.no_such_input', input_name=input_name))
|
|
1539
1618
|
|
|
1540
|
-
data_items
|
|
1619
|
+
data_items[i] = data_item
|
|
1620
|
+
|
|
1621
|
+
k += 1
|
|
1622
|
+
progress_bar.set_postfix(input=k)
|
|
1541
1623
|
progress_bar.update(1)
|
|
1624
|
+
finally:
|
|
1625
|
+
progress_bar.set_postfix()
|
|
1626
|
+
progress_bar.colour = Command.__PROGRESS_BAR_COLOUR_COMPLETE
|
|
1627
|
+
progress_bar.close()
|
|
1542
1628
|
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1629
|
+
# Create all required items. The upload has a progress bar.
|
|
1630
|
+
if len(create_items) > 0:
|
|
1631
|
+
self.__print(logging.INFO, i18n.t('command.upload_inputs', inputs=len(create_items)))
|
|
1632
|
+
created_items = []
|
|
1547
1633
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1634
|
+
for i, input_name, file_type, filename in create_items:
|
|
1635
|
+
created = organisation.create_data(name=input_name, file_type=file_type, files=[filename], wait=False)
|
|
1636
|
+
created_items.append((created, False, 0, os.path.getsize(filename)))
|
|
1637
|
+
data_items[i] = created
|
|
1638
|
+
|
|
1639
|
+
# Wait for the analysis of any created items to complete.
|
|
1640
|
+
self.__wait_for_uploads(created_items)
|
|
1550
1641
|
|
|
1551
1642
|
return data_items
|
|
1552
1643
|
|
|
@@ -1559,42 +1650,35 @@ class Command(Base):
|
|
|
1559
1650
|
storage_files: The list of storage files to be uploaded.
|
|
1560
1651
|
process: The newly created process.
|
|
1561
1652
|
"""
|
|
1562
|
-
created_items = []
|
|
1563
|
-
|
|
1564
1653
|
self.__print(logging.INFO, i18n.t('command.upload_storage', files=len(storage_files)))
|
|
1565
|
-
|
|
1654
|
+
storage_files = {} if storage_files is None else storage_files
|
|
1655
|
+
created_items = []
|
|
1566
1656
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
storage_name = i18n.t('command.storage_name', process=process.name, group_index=group_index, chain_index=chain_index)
|
|
1657
|
+
for storage_key, storage_file in storage_files.items():
|
|
1658
|
+
# The key represents the values used to generate the new storage id, without the process id, which will be different.
|
|
1659
|
+
elements = storage_key.split('_')
|
|
1660
|
+
group_index = int(elements[0])
|
|
1661
|
+
group_count = int(elements[1])
|
|
1662
|
+
ssd_id = elements[2]
|
|
1663
|
+
chain_index = int(elements[3])
|
|
1664
|
+
storage_name = i18n.t('command.storage_name', process=process.name, group_index=group_index, chain_index=chain_index)
|
|
1576
1665
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1666
|
+
group_index = None if group_count == 1 else group_index # If not in a group, then this needs to be None.
|
|
1667
|
+
group_count = None if group_count == 1 else group_count # If not in a group, then this needs to be None.
|
|
1668
|
+
storage_id = self.__get_storage_id(process.id, group_index, group_count, ssd_id, chain_index)
|
|
1580
1669
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1670
|
+
# The destination filename must be correct for everything to work, so we must make a copy with the right name.
|
|
1671
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
1672
|
+
upload_file = os.path.join(tmp_dir, Command._STORAGE_NAME)
|
|
1673
|
+
shutil.copy(storage_file, upload_file)
|
|
1585
1674
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1675
|
+
created = organisation.create_data(name=storage_name, file_type=fusion_platform.FILE_TYPE_GZIP, files=[upload_file], wait=False, id=storage_id)
|
|
1676
|
+
created_items.append((created, False, 0, os.path.getsize(upload_file)))
|
|
1588
1677
|
|
|
1589
|
-
|
|
1678
|
+
os.remove(upload_file)
|
|
1590
1679
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
created.create_complete(wait=True)
|
|
1594
|
-
progress_bar.update(1)
|
|
1595
|
-
|
|
1596
|
-
finally:
|
|
1597
|
-
progress_bar.close()
|
|
1680
|
+
# Wait for the analysis of any created items to complete.
|
|
1681
|
+
self.__wait_for_uploads(created_items)
|
|
1598
1682
|
|
|
1599
1683
|
def __validate_constrained(self, values, text):
|
|
1600
1684
|
"""
|
|
@@ -1609,6 +1693,60 @@ class Command(Base):
|
|
|
1609
1693
|
"""
|
|
1610
1694
|
return text.lower() in [value.lower() for value in values]
|
|
1611
1695
|
|
|
1696
|
+
def __wait_for_uploads(self, upload_data):
|
|
1697
|
+
"""
|
|
1698
|
+
Waits for all the specified uploads to complete, showing a progress bar of bytes uploaded.
|
|
1699
|
+
|
|
1700
|
+
Args:
|
|
1701
|
+
upload_data: A list of tuples of the data items being uploaded with the data model, if the upload has completed, the upload size and total size.
|
|
1702
|
+
"""
|
|
1703
|
+
|
|
1704
|
+
# Initialise the progress bar to show bytes uploaded.
|
|
1705
|
+
total_size = sum([size for _, _, _, size in upload_data if size is not None])
|
|
1706
|
+
progress_bar = tqdm(total=total_size, bar_format=Command.__PROGRESS_BAR_FORMAT, colour=Command.__PROGRESS_BAR_COLOUR_ACTIVE, unit='B', unit_scale=True,
|
|
1707
|
+
unit_divisor=1024) # Show progress in bytes.
|
|
1708
|
+
|
|
1709
|
+
try:
|
|
1710
|
+
# Check the status of all uploads so that we know when we have finished, updating the progress bar as we go.
|
|
1711
|
+
finished = False
|
|
1712
|
+
last_total_current_size = 0
|
|
1713
|
+
|
|
1714
|
+
while not finished:
|
|
1715
|
+
# Wait a bit before checking progress again.
|
|
1716
|
+
sleep(0.25)
|
|
1717
|
+
|
|
1718
|
+
# Now work out haw far we've got.
|
|
1719
|
+
total_current_size = 0
|
|
1720
|
+
count_complete = 0
|
|
1721
|
+
|
|
1722
|
+
for i in range(len(upload_data)):
|
|
1723
|
+
data, complete, current_size, upload_size = upload_data[i]
|
|
1724
|
+
|
|
1725
|
+
if not complete:
|
|
1726
|
+
# Check every file being uploaded for the data model.
|
|
1727
|
+
upload_progress = data.upload_progress()
|
|
1728
|
+
current_size = sum([size for _, size in upload_progress])
|
|
1729
|
+
|
|
1730
|
+
# Now check for completion.
|
|
1731
|
+
complete = data.create_complete()
|
|
1732
|
+
upload_data[i] = (data, complete, current_size, upload_size)
|
|
1733
|
+
|
|
1734
|
+
count_complete += 1 if complete else 0
|
|
1735
|
+
total_current_size += current_size
|
|
1736
|
+
|
|
1737
|
+
# Increment the progress bar.
|
|
1738
|
+
update_current_size = total_current_size - last_total_current_size
|
|
1739
|
+
last_total_current_size = total_current_size
|
|
1740
|
+
progress_bar.update(update_current_size)
|
|
1741
|
+
|
|
1742
|
+
# Check if we have finished.
|
|
1743
|
+
finished = count_complete == len(upload_data)
|
|
1744
|
+
|
|
1745
|
+
finally:
|
|
1746
|
+
# Make sure the progress bar is closed.
|
|
1747
|
+
progress_bar.colour = Command.__PROGRESS_BAR_COLOUR_COMPLETE
|
|
1748
|
+
progress_bar.close()
|
|
1749
|
+
|
|
1612
1750
|
|
|
1613
1751
|
def main():
|
|
1614
1752
|
"""
|
|
Binary file
|
|
@@ -100,6 +100,7 @@ class Data(Model):
|
|
|
100
100
|
super(Data, self).__init__(session)
|
|
101
101
|
|
|
102
102
|
# Initialise the fields.
|
|
103
|
+
self.__upload_progress = {}
|
|
103
104
|
self.__upload_threads = {}
|
|
104
105
|
|
|
105
106
|
def __add_file(self, file_type, file):
|
|
@@ -138,10 +139,11 @@ class Data(Model):
|
|
|
138
139
|
raise ModelError(i18n.t('models.data.failed_add_file_not_unique'))
|
|
139
140
|
|
|
140
141
|
# Create and start a thread for the upload.
|
|
142
|
+
self.__upload_progress[file] = (file, 0) # Indexed by file path, not file id.
|
|
141
143
|
thread = None
|
|
142
144
|
|
|
143
145
|
try:
|
|
144
|
-
thread = RaiseThread(target=self._session.upload_file, args=(url, file))
|
|
146
|
+
thread = RaiseThread(target=self._session.upload_file, args=(url, file, self.__upload_callback))
|
|
145
147
|
thread.start()
|
|
146
148
|
|
|
147
149
|
finally:
|
|
@@ -393,6 +395,12 @@ class Data(Model):
|
|
|
393
395
|
interval_end = None
|
|
394
396
|
|
|
395
397
|
for item, item_file_name in items:
|
|
398
|
+
# So that we can extract things correctly, turn any mapping proxy into a dict (and relevant sub-mapping proxies).
|
|
399
|
+
item = dict(item)
|
|
400
|
+
|
|
401
|
+
if item.get('properties') is not None:
|
|
402
|
+
item['properties'] = dict(item.get('properties'))
|
|
403
|
+
|
|
396
404
|
# Calculate the spatial extent.
|
|
397
405
|
item_bbox = item.get('bbox')
|
|
398
406
|
|
|
@@ -422,3 +430,30 @@ class Data(Model):
|
|
|
422
430
|
'extent': {'spatial': {'bbox': [bbox]}, 'temporal': {'interval': [[interval_start, interval_end]]}},
|
|
423
431
|
'links': links
|
|
424
432
|
}, collection_file_name
|
|
433
|
+
|
|
434
|
+
def __upload_callback(self, url, source, size):
|
|
435
|
+
"""
|
|
436
|
+
Callback method used to receive progress from upload. Updates the upload progress.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
url: The URL to upload as a file.
|
|
440
|
+
source: The source file path.
|
|
441
|
+
size: The total size in bytes so far uploaded.
|
|
442
|
+
"""
|
|
443
|
+
self.__upload_progress[source] = (source, size)
|
|
444
|
+
|
|
445
|
+
def upload_progress(self):
|
|
446
|
+
"""
|
|
447
|
+
Returns current upload progress.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
A list of tuples of the source and total number of bytes uploaded so far.
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
ModelError: if no upload is in progress.
|
|
454
|
+
"""
|
|
455
|
+
# Make sure at least one upload is in progress.
|
|
456
|
+
if (self.__upload_threads is None) or (len(self.__upload_threads) == 0):
|
|
457
|
+
raise ModelError(i18n.t('models.data.no_upload'))
|
|
458
|
+
|
|
459
|
+
return list(self.__upload_progress.values())
|
|
@@ -39,3 +39,5 @@
|
|
|
39
39
|
* **geojson**: The content of the associated file if it is a GeoJSON file (of limited size).
|
|
40
40
|
* **title**: The title for the selector.
|
|
41
41
|
* **description**: The description of the selector.
|
|
42
|
+
* **stac_item**: The STAC item associated with this file.
|
|
43
|
+
* **stac_item_file**: The name of the STAC item file.
|
|
@@ -93,9 +93,14 @@ class DataFileSchema(Schema):
|
|
|
93
93
|
downloads = fields.Integer(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
|
|
94
94
|
geojson = fields.String(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
|
|
95
95
|
|
|
96
|
+
# Removed detail.
|
|
97
|
+
|
|
96
98
|
title = fields.String(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
|
|
97
99
|
description = fields.String(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
|
|
98
100
|
|
|
101
|
+
stac_item = fields.Dict(allow_none=True) # Added pseudo-parameter.
|
|
102
|
+
stac_item_file = fields.String(allow_none=True) # Added pseudo-parameter.
|
|
103
|
+
|
|
99
104
|
class Meta:
|
|
100
105
|
"""
|
|
101
106
|
When loading an object, make sure we exclude any unknown fields, rather than raising an exception, and put fields in their definition order.
|
|
@@ -293,135 +298,8 @@ class DataFile(Model):
|
|
|
293
298
|
A tuple of the STAC item definition as a dictionary and the item file name used in the definition.
|
|
294
299
|
"""
|
|
295
300
|
|
|
296
|
-
#
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
file_name = self.file_name if hasattr(self, Model._FIELD_FILE_NAME) else None
|
|
300
|
-
size = self.size if hasattr(self, Model._FIELD_SIZE) else None
|
|
301
|
-
|
|
302
|
-
# Attempt to find the file type to get its extension and geospatial type. Default to "Other" if not found.
|
|
303
|
-
found_file_type = DataFile._FILE_TYPES.get(self.file_type)
|
|
304
|
-
|
|
305
|
-
if found_file_type is None:
|
|
306
|
-
found_file_type = DataFile._FILE_TYPES.get(fusion_platform.FILE_TYPE_OTHER)
|
|
307
|
-
|
|
308
|
-
geospatial = found_file_type[1]
|
|
309
|
-
|
|
310
|
-
# Get the file extension.
|
|
311
|
-
extension = os.path.splitext(file_name)[1] if file_name is not None else None
|
|
312
|
-
extension = found_file_type[0] if extension is None else extension
|
|
313
|
-
|
|
314
|
-
if (extension is None) or (extension == '*'):
|
|
315
|
-
extension = ''
|
|
316
|
-
else:
|
|
317
|
-
extension = f".{extension}" if not extension.startswith('.') else extension
|
|
301
|
+
# Extract the STAC item from the model.
|
|
302
|
+
stac_item = self.stac_item if hasattr(self, Model._FIELD_STAC_ITEM) else None
|
|
303
|
+
stac_item_file = self.stac_item_file if hasattr(self, Model._FIELD_STAC_ITEM_FILE) else None
|
|
318
304
|
|
|
319
|
-
|
|
320
|
-
if file_name is None:
|
|
321
|
-
file_name = f"{self.id}{extension}"
|
|
322
|
-
|
|
323
|
-
item_file_name = item_file_name_format.format(file_name=file_name)
|
|
324
|
-
|
|
325
|
-
# Start to build the STAC extensions list.
|
|
326
|
-
stac_extensions = ['https://stac-extensions.github.io/processing/v1.2.0/schema.json'] # For properties#processing.[software,version].
|
|
327
|
-
|
|
328
|
-
# Get the bounding box and geometry, if there is one.
|
|
329
|
-
bbox = []
|
|
330
|
-
geometry = {}
|
|
331
|
-
|
|
332
|
-
if (bounds is not None) and (len(bounds) >= 4):
|
|
333
|
-
bbox = bounds
|
|
334
|
-
geometry = {'type': 'Polygon', 'coordinates': [[[bbox[0], bbox[1]], [bbox[2], bbox[1]], [bbox[2], bbox[3]], [bbox[0], bbox[3]], [bbox[0], bbox[1]]]]}
|
|
335
|
-
|
|
336
|
-
# Get the datetime from the filename, if possible. We try various iterations of sections the file name until we find something.
|
|
337
|
-
for file_part in file_name.split('_'):
|
|
338
|
-
extracted_datetime = datetime_parse(file_part) or datetime_parse(f"{file_part}T00:00:00Z") # Assume UTC.
|
|
339
|
-
|
|
340
|
-
if extracted_datetime is not None:
|
|
341
|
-
break
|
|
342
|
-
|
|
343
|
-
if extracted_datetime is None:
|
|
344
|
-
extracted_datetime = self.created_at
|
|
345
|
-
|
|
346
|
-
# Start to build the properties.
|
|
347
|
-
properties = {'processing.software': i18n.t('fusion_platform.sdk'), 'processing.version': fusion_platform.__version__, 'datetime': extracted_datetime}
|
|
348
|
-
|
|
349
|
-
# Add in the CRS, if there is one.
|
|
350
|
-
if crs is not None:
|
|
351
|
-
properties['proj:epsg'] = crs
|
|
352
|
-
stac_extensions.append('https://stac-extensions.github.io/projection/v1.1.0/schema.json') # For properties#proj:epsg.
|
|
353
|
-
|
|
354
|
-
# Build the links.
|
|
355
|
-
links = [{'rel': 'self', 'href': item_file_name, 'type': 'application/json'}]
|
|
356
|
-
|
|
357
|
-
# Add in the optional owning collection.
|
|
358
|
-
if collection_file_name is not None:
|
|
359
|
-
links.append({'rel': 'root', 'href': collection_file_name, 'type': 'application/json'})
|
|
360
|
-
links.append({'rel': 'parent', 'href': collection_file_name, 'type': 'application/json'})
|
|
361
|
-
|
|
362
|
-
# Start to build the asset from the associated file.
|
|
363
|
-
asset = {'href': file_name, 'roles': []}
|
|
364
|
-
|
|
365
|
-
# Extract out the relevant values depending upon the file type.
|
|
366
|
-
raster_bands = None
|
|
367
|
-
eo_bands = None
|
|
368
|
-
|
|
369
|
-
if (geospatial == DataFile._GEOSPATIAL_RASTER) and hasattr(self, Model._FIELD_SELECTORS) and (self.selectors is not None):
|
|
370
|
-
for selector in self.selectors:
|
|
371
|
-
raster_band = {}
|
|
372
|
-
eo_band = {}
|
|
373
|
-
|
|
374
|
-
if hasattr(self, Model._FIELD_RESOLUTION):
|
|
375
|
-
raster_band['spatial_resolution'] = self.resolution
|
|
376
|
-
|
|
377
|
-
if selector.get(Model._FIELD_UNIT) is not None:
|
|
378
|
-
raster_band['unit'] = selector.get(Model._FIELD_UNIT)
|
|
379
|
-
|
|
380
|
-
if (selector.get(Model._FIELD_MINIMUM) is not None) and (selector.get(Model._FIELD_MAXIMUM) is not None) and (
|
|
381
|
-
selector.get(Model._FIELD_MEAN) is not None) and (selector.get(Model._FIELD_SD) is not None):
|
|
382
|
-
raster_band['statistics'] = {
|
|
383
|
-
'minimum': selector.get(Model._FIELD_MINIMUM),
|
|
384
|
-
'maximum': selector.get(Model._FIELD_MAXIMUM),
|
|
385
|
-
'mean': selector.get(Model._FIELD_MEAN),
|
|
386
|
-
'stddev': selector.get(Model._FIELD_SD)
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (selector.get(Model._FIELD_HISTOGRAM_MINIMUM) is not None) and (selector.get(Model._FIELD_HISTOGRAM_MAXIMUM) is not None) and (
|
|
390
|
-
selector.get(Model._FIELD_HISTOGRAM) is not None):
|
|
391
|
-
raster_band['histogram'] = {
|
|
392
|
-
'count': len(selector.get(Model._FIELD_HISTOGRAM)),
|
|
393
|
-
'min': selector.get(Model._FIELD_HISTOGRAM_MINIMUM),
|
|
394
|
-
'max': selector.get(Model._FIELD_HISTOGRAM_MAXIMUM),
|
|
395
|
-
'buckets': selector.get(Model._FIELD_HISTOGRAM),
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if selector.get(Model._FIELD_CATEGORY) is not None:
|
|
399
|
-
eo_band['name'] = selector.get(Model._FIELD_CATEGORY)
|
|
400
|
-
|
|
401
|
-
raster_bands = [raster_band] if raster_bands is None else [raster_band] + raster_bands
|
|
402
|
-
eo_bands = [eo_band] if eo_bands is None else [eo_band] + eo_bands
|
|
403
|
-
|
|
404
|
-
stac_extensions.append('https://stac-extensions.github.io/eo/v1.1.0/schema.json') # For assets#eo:bands.
|
|
405
|
-
stac_extensions.append('https://stac-extensions.github.io/raster/v1.1.0/schema.json') # For assets#raster:bands.
|
|
406
|
-
|
|
407
|
-
if raster_bands is not None:
|
|
408
|
-
asset['raster:bands'] = raster_bands
|
|
409
|
-
asset['eo:bands'] = eo_bands
|
|
410
|
-
|
|
411
|
-
# Add in the file size, if there is one.
|
|
412
|
-
if size is not None:
|
|
413
|
-
asset['file:size'] = size
|
|
414
|
-
stac_extensions.append('https://stac-extensions.github.io/file/v2.1.0/schema.json') # For assets#file:size.
|
|
415
|
-
|
|
416
|
-
# Build the STAC item.
|
|
417
|
-
return {
|
|
418
|
-
'type': 'Feature',
|
|
419
|
-
'stac_version': '1.0.0',
|
|
420
|
-
'stac_extensions': stac_extensions,
|
|
421
|
-
'id': self.id,
|
|
422
|
-
'bbox': bbox,
|
|
423
|
-
'geometry': geometry,
|
|
424
|
-
'properties': properties,
|
|
425
|
-
'links': links,
|
|
426
|
-
'assets': {'asset': asset}
|
|
427
|
-
}, item_file_name
|
|
305
|
+
return stac_item, stac_item_file
|
|
@@ -96,6 +96,8 @@ class Model(Base):
|
|
|
96
96
|
_FIELD_SELECTOR = 'selector'
|
|
97
97
|
_FIELD_SELECTORS = 'selectors'
|
|
98
98
|
_FIELD_SSD_ID = 'ssd_id'
|
|
99
|
+
_FIELD_STAC_ITEM = 'stac_item'
|
|
100
|
+
_FIELD_STAC_ITEM_FILE = 'stac_item_file'
|
|
99
101
|
_FIELD_STARTED_AT = 'started_at'
|
|
100
102
|
_FIELD_PUBLISHABLE = 'publishable'
|
|
101
103
|
_FIELD_SIZE = 'size'
|
|
@@ -186,13 +186,18 @@ class ProcessExecution(Model):
|
|
|
186
186
|
complete = self.progress >= 100
|
|
187
187
|
|
|
188
188
|
# Raise an exception if the execution failed.
|
|
189
|
+
abort_reason = self.abort_reason if hasattr(self, Model._FIELD_ABORT_REASON) else None
|
|
190
|
+
|
|
189
191
|
if complete and not self.success:
|
|
190
|
-
self._logger.error(i18n.t('models.process_execution.execution_failed'))
|
|
191
|
-
raise ModelError(i18n.t('models.process_execution.execution_failed'))
|
|
192
|
+
self._logger.error(i18n.t('models.process_execution.execution_failed', abort_reason=abort_reason))
|
|
193
|
+
raise ModelError(i18n.t('models.process_execution.execution_failed', abort_reason=abort_reason))
|
|
192
194
|
|
|
193
195
|
if complete:
|
|
194
196
|
self._logger.debug('execution %s is complete', self.id)
|
|
195
197
|
|
|
198
|
+
if abort_reason is not None:
|
|
199
|
+
self._logger.warning(i18n.t('models.process_execution.execution_warning', abort_reason=abort_reason))
|
|
200
|
+
|
|
196
201
|
if (not wait) or complete:
|
|
197
202
|
break
|
|
198
203
|
|
{fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/session.py
RENAMED
|
@@ -13,6 +13,7 @@ import logging
|
|
|
13
13
|
import os
|
|
14
14
|
import requests
|
|
15
15
|
from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_random_exponential
|
|
16
|
+
from tqdm.utils import CallbackIOWrapper
|
|
16
17
|
|
|
17
18
|
import fusion_platform
|
|
18
19
|
from fusion_platform.base import Base
|
|
@@ -47,6 +48,36 @@ class ValueError(SessionError):
|
|
|
47
48
|
pass
|
|
48
49
|
|
|
49
50
|
|
|
51
|
+
class UploadCallback:
|
|
52
|
+
"""
|
|
53
|
+
Provides a callback mechanism for uploads using the CallbackIOWrapper.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, url, source, callback):
|
|
57
|
+
"""
|
|
58
|
+
Initialises the object.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
url: The URL being uploaded to.
|
|
62
|
+
source: The source file path.
|
|
63
|
+
callback: The callback method used to receive upload progress.
|
|
64
|
+
"""
|
|
65
|
+
self.__url = url
|
|
66
|
+
self.__source = source
|
|
67
|
+
self.__callback = callback
|
|
68
|
+
self.__upload_size = 0
|
|
69
|
+
|
|
70
|
+
def callback(self, size):
|
|
71
|
+
"""
|
|
72
|
+
Receives the upload callback every time data is read from the associated file.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
size: The number of bytes which have been read from the associated file.
|
|
76
|
+
"""
|
|
77
|
+
self.__upload_size += size
|
|
78
|
+
self.__callback(self.__url, self.__source, self.__upload_size)
|
|
79
|
+
|
|
80
|
+
|
|
50
81
|
class Session(Base):
|
|
51
82
|
"""
|
|
52
83
|
Provides a session for use in interfacing with the Fusion Platform<sup>®</sup> API.
|
|
@@ -257,19 +288,22 @@ class Session(Base):
|
|
|
257
288
|
# Return the payload.
|
|
258
289
|
return payload
|
|
259
290
|
|
|
260
|
-
def upload_file(self, url, source):
|
|
291
|
+
def upload_file(self, url, source, callback=None):
|
|
261
292
|
"""
|
|
262
293
|
Uploads a file from the source path.
|
|
263
294
|
|
|
264
295
|
Args:
|
|
265
296
|
url: The URL to download as a file.
|
|
266
297
|
source: The source file path.
|
|
298
|
+
callback: The optional callback method used to receive upload progress.
|
|
267
299
|
"""
|
|
268
300
|
try:
|
|
269
301
|
# Upload the file as a data stream.
|
|
270
302
|
self._logger.info('uploading %s -> %s', url, source)
|
|
271
303
|
|
|
272
304
|
with open(source, 'rb') as file:
|
|
305
|
+
# Wrap the file reader with a callback to give progress.
|
|
306
|
+
file = CallbackIOWrapper(UploadCallback(url, source, callback).callback, file, 'read') if callback is not None else file
|
|
273
307
|
response = requests.put(url, data=file)
|
|
274
308
|
|
|
275
309
|
# Raise any errors.
|
|
@@ -24,6 +24,7 @@ i18n.add_translation('command.log_execution_group', ' (group \'%{group_id}\' %{g
|
|
|
24
24
|
i18n.add_translation('command.log_execution_duration', ' - %{minutes} minute(s)', 'en')
|
|
25
25
|
i18n.add_translation('command.log_execution_period', ' (%{started_at}%{ended_at})', 'en')
|
|
26
26
|
i18n.add_translation('command.log_execution_ended_at', ' -> %{ended_at}', 'en')
|
|
27
|
+
i18n.add_translation('command.log_execution_warning', 'warning%{exit_type}%{abort_reason}', 'en')
|
|
27
28
|
i18n.add_translation('command.log_execution_failed', 'failed%{exit_type}%{abort_reason}', 'en')
|
|
28
29
|
i18n.add_translation('command.log_execution_exit_type', ' (%{exit_type})', 'en')
|
|
29
30
|
i18n.add_translation('command.log_execution_abort_reason', ': %{abort_reason}', 'en')
|
|
@@ -50,7 +51,7 @@ i18n.add_translation('command.using_storage', 'Uploading storage \'%{filename}\'
|
|
|
50
51
|
i18n.add_translation('command.using_service', 'Using service \'%{service}\'', 'en')
|
|
51
52
|
i18n.add_translation('command.using_organisation', 'Using organisation \'%{organisation}\'', 'en')
|
|
52
53
|
i18n.add_translation('command.upload_storage', 'Uploading %{files} storage file(s)...', 'en')
|
|
53
|
-
i18n.add_translation('command.upload_inputs', 'Uploading
|
|
54
|
+
i18n.add_translation('command.upload_inputs', 'Uploading %{inputs} input file(s)...', 'en')
|
|
54
55
|
i18n.add_translation('command.storage_name', '%{process} Storage %{group_index} %{chain_index}', 'en')
|
|
55
56
|
i18n.add_translation('command.start_process', 'Starting \'%{process}\' from service \'%{service}\'', 'en')
|
|
56
57
|
i18n.add_translation('command.remove_process', 'Removing input \'%{process}\'', 'en')
|
|
@@ -59,6 +60,7 @@ i18n.add_translation('command.prompt_yes', 'Yes', 'en')
|
|
|
59
60
|
i18n.add_translation('command.prompt_no', 'No', 'en')
|
|
60
61
|
i18n.add_translation('command.password', 'Password', 'en')
|
|
61
62
|
i18n.add_translation('command.organisation', 'Organisation', 'en')
|
|
63
|
+
i18n.add_translation('command.find_inputs', 'Finding %{inputs} input file(s)...', 'en')
|
|
62
64
|
i18n.add_translation('command.executing', 'Executing...', 'en')
|
|
63
65
|
i18n.add_translation('command.email', 'Email', 'en')
|
|
64
66
|
i18n.add_translation('command.download_process_execution', 'Downloading \'%{process}\' (inputs %{inputs}, outputs %{outputs}, storage %{storage}, intermediate %{intermediate}, metrics %{metrics}, components %{components})', 'en')
|
|
@@ -237,13 +239,15 @@ i18n.add_translation('models.data_file.no_download', 'No download is in progress
|
|
|
237
239
|
i18n.add_translation('models.data_file.download_already_in_progress', 'Cannot download file as the download is already in progress', 'en')
|
|
238
240
|
i18n.add_translation('models.data_file.organisation_id.description', 'The owning organisation.', 'en')
|
|
239
241
|
i18n.add_translation('models.data_file.organisation_id.title', 'Organisation', 'en')
|
|
242
|
+
i18n.add_translation('models.data.no_upload', 'No upload is in progress', 'en')
|
|
240
243
|
i18n.add_translation('models.data.no_create', 'No create is in progress', 'en')
|
|
241
244
|
i18n.add_translation('models.data.failed_add_missing_file', 'Failed to add file as the file does not exist: %{file}', 'en')
|
|
242
245
|
i18n.add_translation('models.data.failed_add_file_not_unique', 'Failed to add file as the id is not unique', 'en')
|
|
243
246
|
i18n.add_translation('models.data.failed_add_file_url', 'Failed to get URL from add file response', 'en')
|
|
244
247
|
i18n.add_translation('models.data.failed_add_file_id', 'Failed to get id from add file response', 'en')
|
|
245
248
|
i18n.add_translation('models.data.failed_copy_id', 'Failed to get data id from copy response', 'en')
|
|
246
|
-
i18n.add_translation('models.process_execution.
|
|
249
|
+
i18n.add_translation('models.process_execution.execution_warning', 'Execution has completed successfully but with a warning: %{abort_reason}', 'en')
|
|
250
|
+
i18n.add_translation('models.process_execution.execution_failed', 'Execution has failed: %{abort_reason}', 'en')
|
|
247
251
|
i18n.add_translation('models.fields.uuid.invalid_uuid', 'Not a valid utf-8 string', 'en')
|
|
248
252
|
i18n.add_translation('models.fields.url.invalid_url', 'Not a valid URL', 'en')
|
|
249
253
|
i18n.add_translation('models.fields.tuple.invalid', 'Not a valid tuple', 'en')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: fusion-platform-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.6
|
|
4
4
|
Summary: Python SDK used to interact with the Fusion Platform(r)
|
|
5
5
|
Home-page: https://github.com/d-cat-support/fusion-platform-python-sdk
|
|
6
6
|
Author: Digital Content Analysis Technology Ltd
|
|
@@ -24,6 +24,15 @@ Requires-Dist: python-i18n[YAML]
|
|
|
24
24
|
Requires-Dist: requests
|
|
25
25
|
Requires-Dist: tenacity
|
|
26
26
|
Requires-Dist: tqdm
|
|
27
|
+
Dynamic: author
|
|
28
|
+
Dynamic: author-email
|
|
29
|
+
Dynamic: classifier
|
|
30
|
+
Dynamic: description
|
|
31
|
+
Dynamic: description-content-type
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: license
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: summary
|
|
27
36
|
|
|
28
37
|
# Fusion Platform<sup>®</sup> Python SDK
|
|
29
38
|
|
|
@@ -19,7 +19,7 @@ PACKAGE_DIR = pathlib.Path(__file__).parent
|
|
|
19
19
|
# @formatter:off
|
|
20
20
|
setup(
|
|
21
21
|
name='fusion-platform-python-sdk',
|
|
22
|
-
version='1.
|
|
22
|
+
version='1.16.6',
|
|
23
23
|
description='Python SDK used to interact with the Fusion Platform(r)',
|
|
24
24
|
long_description=(PACKAGE_DIR / 'README.md').read_text(),
|
|
25
25
|
long_description_content_type='text/markdown',
|
{fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/tests/test_session.py
RENAMED
|
@@ -302,3 +302,9 @@ class TestSession(CustomTestCase):
|
|
|
302
302
|
adapter = mock.put(url, status_code=200)
|
|
303
303
|
session.upload_file(url, source)
|
|
304
304
|
self.assertIsNotNone(adapter.last_request.text)
|
|
305
|
+
|
|
306
|
+
def callback(url, source, upload_size):
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
session.upload_file(url, source, callback=callback) # Because the request is mocked, the callback will not be called because there are no reads.
|
|
310
|
+
self.assertIsNotNone(adapter.last_request.text)
|
|
File without changes
|
|
File without changes
|
{fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/__main__.py
RENAMED
|
File without changes
|
{fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/fusion_platform/localise.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/tests/test_command.py
RENAMED
|
File without changes
|
|
File without changes
|
{fusion_platform_python_sdk-1.15.12 → fusion_platform_python_sdk-1.16.6}/tests/test_localise.py
RENAMED
|
File without changes
|