fusion-platform-python-sdk 1.16.6__tar.gz → 1.17.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.
Files changed (57) hide show
  1. {fusion_platform_python_sdk-1.16.6/fusion_platform_python_sdk.egg-info → fusion_platform_python_sdk-1.17.6}/PKG-INFO +3 -3
  2. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/README.md +1 -1
  3. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/__init__.py +2 -2
  4. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/command.py +94 -36
  5. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/fusion_platform_sdk.pdf +0 -0
  6. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/model.py +3 -1
  7. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/process.py +14 -3
  8. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/process_service_execution.md +3 -1
  9. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/process_service_execution.py +2 -0
  10. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/translations.py +15 -1
  11. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6/fusion_platform_python_sdk.egg-info}/PKG-INFO +3 -3
  12. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/setup.py +2 -2
  13. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/tests/test_session.py +1 -1
  14. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/LICENSE.txt +0 -0
  15. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/MANIFEST.in +0 -0
  16. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/__main__.py +0 -0
  17. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/base.py +0 -0
  18. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/common/__init__.py +0 -0
  19. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/common/raise_thread.py +0 -0
  20. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/common/utilities.py +0 -0
  21. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/detailed_example.py +0 -0
  22. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/documentation.py +0 -0
  23. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/glasgow.geojson +0 -0
  24. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/lake_district.geojson +0 -0
  25. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/localisations.py +0 -0
  26. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/localise.py +0 -0
  27. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/__init__.py +0 -0
  28. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/credit.md +0 -0
  29. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/credit.py +0 -0
  30. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/data.md +0 -0
  31. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/data.py +0 -0
  32. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/data_file.md +0 -0
  33. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/data_file.py +0 -0
  34. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/fields.py +0 -0
  35. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/organisation.md +0 -0
  36. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/organisation.py +0 -0
  37. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/process.md +0 -0
  38. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/process_execution.md +0 -0
  39. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/process_execution.py +0 -0
  40. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/process_service_execution_log.md +0 -0
  41. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/process_service_execution_log.py +0 -0
  42. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/service.md +0 -0
  43. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/service.py +0 -0
  44. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/user.md +0 -0
  45. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/models/user.py +0 -0
  46. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/quick_example.py +0 -0
  47. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform/session.py +0 -0
  48. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform_python_sdk.egg-info/SOURCES.txt +0 -0
  49. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform_python_sdk.egg-info/dependency_links.txt +0 -0
  50. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform_python_sdk.egg-info/entry_points.txt +0 -0
  51. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform_python_sdk.egg-info/requires.txt +0 -0
  52. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/fusion_platform_python_sdk.egg-info/top_level.txt +0 -0
  53. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/setup.cfg +0 -0
  54. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/tests/test_base.py +0 -0
  55. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/tests/test_command.py +0 -0
  56. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/tests/test_init.py +0 -0
  57. {fusion_platform_python_sdk-1.16.6 → fusion_platform_python_sdk-1.17.6}/tests/test_localise.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fusion-platform-python-sdk
3
- Version: 1.16.6
3
+ Version: 1.17.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
@@ -8,11 +8,11 @@ Author-email: support@d-cat.co.uk
8
8
  License: MIT
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.8
12
11
  Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE.txt
18
18
  Requires-Dist: marshmallow
@@ -48,7 +48,7 @@ API, and this is defined within the corresponding OpenAPI 3.0 specification, whi
48
48
 
49
49
  ## Installation
50
50
 
51
- The SDK has been built for Python 3, and is tested against Python 3.8, 3.9, 3.10, 3.11 and 3.12. To install the SDK into a suitable Python environment containing
51
+ The SDK has been built for Python 3, and is tested against Python 3.9, 3.10, 3.11, 3.12 and 3.13. To install the SDK into a suitable Python environment containing
52
52
  `pip`, execute the following:
53
53
 
54
54
  ```shell
@@ -12,7 +12,7 @@ API, and this is defined within the corresponding OpenAPI 3.0 specification, whi
12
12
 
13
13
  ## Installation
14
14
 
15
- The SDK has been built for Python 3, and is tested against Python 3.8, 3.9, 3.10, 3.11 and 3.12. To install the SDK into a suitable Python environment containing
15
+ The SDK has been built for Python 3, and is tested against Python 3.9, 3.10, 3.11, 3.12 and 3.13. To install the SDK into a suitable Python environment containing
16
16
  `pip`, execute the following:
17
17
 
18
18
  ```shell
@@ -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.16.6'
15
- __version_date__ = '2025-02-11T12:17:15Z'
14
+ __version__ = '1.17.6'
15
+ __version_date__ = '2025-03-26T16:04:58Z'
16
16
 
17
17
  # Exclude certain sub-modules from documentation.
18
18
  # @formatter:off
@@ -39,7 +39,7 @@ Localise.setup()
39
39
 
40
40
  import fusion_platform
41
41
  from fusion_platform.base import Base
42
- from fusion_platform.common.utilities import json_default
42
+ from fusion_platform.common.utilities import json_default, string_blank
43
43
  from fusion_platform.models.data import Data
44
44
  from fusion_platform.models.model import Model
45
45
  from fusion_platform.session import RequestError
@@ -103,7 +103,9 @@ class Command(Base):
103
103
  _METRIC_S3_INPUT_SIZE = 'input_size'
104
104
  _METRIC_S3_OUTPUT_SIZE = 'output_size'
105
105
  _METRIC_S3_TRANSFER_BYTES = 's3_transfer_bytes'
106
+ _METRIC_GCS_TRANSFER_BYTES = 'gcs_transfer_bytes'
106
107
  _METRIC_EXTERNAL_TRANSFER_BYTES = 'external_transfer_bytes'
108
+ _METRIC_INTERNAL_TRANSFER_BYTES = 'internal_transfer_bytes'
107
109
 
108
110
  # Process other field constants.
109
111
  _MODEL_FIELDS = ['output_storage_period', 'run_type', 'repeat_count', 'repeat_start', 'repeat_end', 'repeat_gap', 'repeat_offset']
@@ -193,6 +195,7 @@ class Command(Base):
193
195
  while not finished:
194
196
  # Either find the dispatchers from what has been loaded from the YAML file or prompt the user.
195
197
  add = False
198
+ dispatch_intermediate = False
196
199
  dispatcher_options = {}
197
200
 
198
201
  if dispatchers is not None:
@@ -204,6 +207,8 @@ class Command(Base):
204
207
  selected_dispatcher = filtered_dispatchers[0]
205
208
  selected_dispatcher[Model._FIELD_USED] = True
206
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
207
212
  dispatcher_options = selected_dispatcher.get(Model._FIELD_OPTIONS) if selected_dispatcher.get(Model._FIELD_OPTIONS) is not None else {}
208
213
 
209
214
  else:
@@ -213,8 +218,29 @@ class Command(Base):
213
218
  if (response is not None) and response.lower().startswith(i18n.t('command.prompt_yes').lower()[0]):
214
219
  add = True
215
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
+
216
228
  for option in dispatcher.options:
217
- option_value = self.__get_input(option.name)
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))))
218
244
 
219
245
  if option_value is not None:
220
246
  dispatcher_options[option.name] = option_value
@@ -224,7 +250,8 @@ class Command(Base):
224
250
  dispatchers_added += 1
225
251
 
226
252
  for option_name, option_value in dispatcher_options.items():
227
- process.update(dispatcher_number=dispatchers_added, option_name=option_name, value=option_value)
253
+ process.update(dispatcher_number=dispatchers_added, option_name=option_name, value=option_value, coerce_value=True,
254
+ dispatch_intermediate=dispatch_intermediate)
228
255
  else:
229
256
  finished = True
230
257
 
@@ -233,6 +260,18 @@ class Command(Base):
233
260
 
234
261
  return process
235
262
 
263
+ def __data_name_to_file_name(self, data_name):
264
+ """
265
+ Converts a data item name into a sane file name (without extension).
266
+
267
+ Args:
268
+ data_name: The data item name to convert.
269
+
270
+ Returns:
271
+ The corresponding file name.
272
+ """
273
+ return re.sub(r"[^A-Za-z_\-]", '_', str(data_name)) if data_name is not None else None
274
+
236
275
  def define_process(self, organisation, process_name, download_inputs=False, download_storage=False):
237
276
  """
238
277
  Obtains the process definition for a process.
@@ -254,12 +293,19 @@ class Command(Base):
254
293
  # Get the underlying service name.
255
294
  service, _ = organisation.find_services(ssd_id=process.ssd_id)
256
295
 
257
- # Get the options and dispatchers.
296
+ # Get the options and dispatchers, optionally add in whether intermediate services are being dispatched.
258
297
  options = {option.name: self.__format_value(option.value, data_type=option.data_type) for option in process.options}
259
- dispatchers = [
260
- {Model._FIELD_NAME: dispatcher.name,
261
- Model._FIELD_OPTIONS: {option.name: self.__format_value(option.value, data_type=option.data_type) for option in dispatcher.options}} for dispatcher in
262
- process.dispatchers]
298
+ dispatchers = []
299
+
300
+ for dispatcher in process.dispatchers:
301
+ found_dispatcher = {Model._FIELD_NAME: dispatcher.name}
302
+
303
+ if hasattr(dispatcher, Model._FIELD_DISPATCH_INTERMEDIATE):
304
+ found_dispatcher[Model._FIELD_DISPATCH_INTERMEDIATE] = dispatcher.dispatch_intermediate
305
+
306
+ found_dispatcher[Model._FIELD_OPTIONS] = {option.name: self.__format_value(option.value, data_type=option.data_type) for option in dispatcher.options}
307
+
308
+ dispatchers.append(found_dispatcher)
263
309
 
264
310
  # Add in the standard options.
265
311
  for field in Command._MODEL_FIELDS:
@@ -418,7 +464,8 @@ class Command(Base):
418
464
 
419
465
  return found
420
466
 
421
- def __download_execution(self, process, download_inputs, download_outputs, download_storage, download_intermediate, download_components, output_dir, execution):
467
+ def __download_execution(self, process, download_inputs, download_outputs, download_storage, download_intermediate, download_components, output_dir, wait,
468
+ execution):
422
469
  """
423
470
  Downloads an execution.
424
471
 
@@ -430,6 +477,7 @@ class Command(Base):
430
477
  download_intermediate: Should intermediate components be downloaded?
431
478
  download_components: The names of the components that should be downloaded? If None or [], then all components are downloaded.
432
479
  output_dir: The output directory.
480
+ wait: Wait for the execution to complete?
433
481
  execution: The execution to be downloaded.
434
482
 
435
483
  Returns:
@@ -441,10 +489,10 @@ class Command(Base):
441
489
  metrics = []
442
490
  downloads = []
443
491
 
444
- # Wait for the execution to complete. We explicitly wait in this method to keep the SDK free.
492
+ # Optionally wait for the execution to complete. We explicitly wait in this method to keep the SDK free.
445
493
  complete = False
446
494
 
447
- while not complete:
495
+ while wait and (not complete):
448
496
  try:
449
497
  complete = execution.check_complete()
450
498
  except:
@@ -480,15 +528,20 @@ class Command(Base):
480
528
  if (started_at is None) or (execution.started_at < started_at):
481
529
  started_at = execution.started_at
482
530
 
483
- if (ended_at is None) or (execution.ended_at > ended_at):
484
- ended_at = execution.ended_at
531
+ execution_ended_at = execution.ended_at if hasattr(execution, Model._FIELD_ENDED_AT) and (execution.ended_at is not None) else datetime.now(timezone.utc)
532
+
533
+ if (ended_at is None) or (execution_ended_at > ended_at):
534
+ ended_at = execution_ended_at
485
535
 
486
536
  for j, component in enumerate(components):
487
537
  # Download the component.
488
538
  component_name = '_'.join([item for item in re.sub(r'[\W]', '_', component.name).split('_') if len(item) > 0])
489
539
  component_dir = f"{str(j + 1)}_{component_name[:25]}"
490
540
  component_options = {item.get(Model._FIELD_NAME): item.get(Model._FIELD_VALUE) for item in component.options} if hasattr(component,
491
- Model._FIELD_OPTIONS) else {}
541
+ Model._FIELD_OPTIONS) and (
542
+ component.options is not None) else {}
543
+ component_success = component.success if hasattr(component, Model._FIELD_SUCCESS) and (component.success is not None) else False
544
+ component_runtime = component.runtime if hasattr(component, Model._FIELD_RUNTIME) and (component.runtime is not None) else 0
492
545
 
493
546
  # Save the metrics for each component in the execution.
494
547
  # @formatter:off
@@ -497,25 +550,22 @@ class Command(Base):
497
550
  Command._METRICS_PROCESS: process.name,
498
551
  Command._METRICS_DURATION: ended_at - started_at if (started_at is not None) and (ended_at is not None) else 0,
499
552
  Command._METRICS_COMPONENT: component.name,
500
- Command._METRICS_SUCCESS: component.success,
553
+ Command._METRICS_SUCCESS: component_success,
501
554
  Command._METRICS_CPU: component.cpu,
502
555
  Command._METRICS_MEMORY: component.memory,
503
- Command._METRICS_RUNTIME: component.runtime
556
+ Command._METRICS_RUNTIME: component_runtime
504
557
  }
505
558
  # @formatter:on
506
559
 
507
560
  # Only D-CAT services have metrics.
508
- s3_transfer_bytes = 0
509
- external_transfer_bytes = 0
510
-
511
561
  if hasattr(component, Command._METRICS_METRICS) and (component.metrics is not None):
512
- s3_transfer_bytes = sum(
513
- [metric[Command._METRIC_S3_TRANSFER_BYTES] for metric in component.metrics if metric[Command._METRIC_S3_TRANSFER_BYTES] is not None])
514
- external_transfer_bytes = sum([metric[Command._METRIC_EXTERNAL_TRANSFER_BYTES] for metric in component.metrics if
515
- metric[Command._METRIC_EXTERNAL_TRANSFER_BYTES] is not None])
516
-
517
- metric[Command._METRIC_S3_TRANSFER_BYTES] = s3_transfer_bytes
518
- metric[Command._METRIC_EXTERNAL_TRANSFER_BYTES] = external_transfer_bytes
562
+ for key in [
563
+ Command._METRIC_S3_TRANSFER_BYTES,
564
+ Command._METRIC_GCS_TRANSFER_BYTES,
565
+ Command._METRIC_EXTERNAL_TRANSFER_BYTES,
566
+ Command._METRIC_INTERNAL_TRANSFER_BYTES
567
+ ]:
568
+ metric[key] = sum([metric[key] for metric in component.metrics if metric.get(key) is not None])
519
569
 
520
570
  # Find each input, output and storage to download. While traversing each, save the input and output size. The size might not be immediately available
521
571
  # for every file, so wait for all items to be analysed.
@@ -543,7 +593,8 @@ class Command(Base):
543
593
  # Download the data item once it is complete.
544
594
  data_item.check_analysis_complete(wait=True)
545
595
 
546
- download_dir = os.path.join(execution_dir, component_dir, f"{data_type}_{str(k)}")
596
+ name = re.sub('_+', '_', f"{data_type.capitalize()}_{str(k)}_{self.__data_name_to_file_name(data_item.name)}").rstrip('_')
597
+ download_dir = os.path.join(execution_dir, component_dir, name)
547
598
  os.makedirs(download_dir, exist_ok=True)
548
599
 
549
600
  # We also create the corresponding STAC definitions.
@@ -574,8 +625,8 @@ class Command(Base):
574
625
  with open(os.path.join(download_dir, stac_file_name), 'w') as stac_file:
575
626
  stac_file.write(json.dumps(stac_definition, default=json_default))
576
627
 
577
- metric[Command._METRIC_S3_INPUT_SIZE] = sum(input_sizes)
578
- metric[Command._METRIC_S3_OUTPUT_SIZE] = sum(output_sizes)
628
+ metric[Command._METRIC_S3_INPUT_SIZE] = sum(input_sizes) if len(input_sizes) > 0 else ''
629
+ metric[Command._METRIC_S3_OUTPUT_SIZE] = sum(output_sizes) if len(output_sizes) > 0 else ''
579
630
  metrics.append(metric)
580
631
 
581
632
  return downloads, metrics
@@ -649,7 +700,7 @@ class Command(Base):
649
700
  progress_bar.close()
650
701
 
651
702
  def download_process_execution(self, organisation, process_name, download_inputs, download_outputs, download_storage, download_intermediate,
652
- download_components, save_metrics):
703
+ download_components, save_metrics, wait_for_execution_to_complete):
653
704
  """
654
705
  Downloads the inputs and/or outputs from a process or execution(s).
655
706
 
@@ -662,6 +713,7 @@ class Command(Base):
662
713
  download_intermediate: Should intermediate components be downloaded?
663
714
  download_components: The names of the components that should be downloaded? If None or [], then all components are downloaded.
664
715
  save_metrics: Save process metrics to file?
716
+ wait_for_execution_to_complete: Optionally wait for completion of the execution?
665
717
 
666
718
  Returns:
667
719
  The process model.
@@ -707,7 +759,8 @@ class Command(Base):
707
759
 
708
760
  # Do all the downloads in batches. We use small batches so that we do not swamp memory.
709
761
  batch_size = 2 * os.cpu_count() # Downloads don't swamp CPU.
710
- self.__print(logging.INFO, i18n.t('command.download_executions', executions=len(executions)))
762
+ wait = i18n.t('command.wait_for_completion') if wait_for_execution_to_complete else ''
763
+ self.__print(logging.INFO, i18n.t('command.download_executions', wait=wait, executions=len(executions)))
711
764
  progress_bar = tqdm(total=len(executions), bar_format=Command.__PROGRESS_BAR_FORMAT, colour=Command.__PROGRESS_BAR_COLOUR_ACTIVE,
712
765
  unit='execution') # Unit matches keyword argument.
713
766
  progress_bar.set_postfix(execution=1)
@@ -715,7 +768,7 @@ class Command(Base):
715
768
  try:
716
769
  # Now download each execution in batches.
717
770
  partial_download_execution = partial(self.__download_execution, process, download_inputs, download_outputs, download_storage, download_intermediate,
718
- download_components, output_dir)
771
+ download_components, output_dir, wait_for_execution_to_complete)
719
772
  results = []
720
773
 
721
774
  with Pool(nodes=batch_size) as pool:
@@ -855,7 +908,7 @@ class Command(Base):
855
908
  The user response.
856
909
  """
857
910
  default = constrained_values[0] if (default is None) and (constrained_values is not None) and (len(constrained_values) > 0) else default
858
- input_prompt = f"{input_prompt}{' [' + default + ']' if default is not None else ''}: "
911
+ input_prompt = f"{input_prompt}{' [' + str(default) + ']' if default is not None else ''}: "
859
912
  constrained_values = [''] + constrained_values if constrained_values is not None else constrained_values
860
913
 
861
914
  # Set up any validation against constrained values.
@@ -1089,7 +1142,8 @@ class Command(Base):
1089
1142
  # Also perform the download, if required.
1090
1143
  if arguments.download:
1091
1144
  process = self.download_process_execution(organisation, process_name, arguments.inputs, arguments.outputs, arguments.storage,
1092
- arguments.intermediate, arguments.component, arguments.metrics)
1145
+ arguments.intermediate, arguments.component, arguments.metrics,
1146
+ not arguments.no_wait_for_completion)
1093
1147
 
1094
1148
  # And optionally remove the process and its inputs.
1095
1149
  if arguments.remove:
@@ -1112,7 +1166,7 @@ class Command(Base):
1112
1166
  for process_name in arguments.process:
1113
1167
  # Download the process.
1114
1168
  process = self.download_process_execution(organisation, process_name, arguments.inputs, arguments.outputs, arguments.storage,
1115
- arguments.intermediate, arguments.component, arguments.metrics)
1169
+ arguments.intermediate, arguments.component, arguments.metrics, not arguments.no_wait_for_completion)
1116
1170
 
1117
1171
  # And optionally remove the process and its inputs.
1118
1172
  if arguments.remove:
@@ -1189,6 +1243,8 @@ class Command(Base):
1189
1243
  default=False, action="store_true")
1190
1244
  parser_start.add_argument(i18n.t('command.start.component_short'), i18n.t('command.start.component_long'), help=i18n.t('command.start.component_help'),
1191
1245
  nargs='*')
1246
+ parser_start.add_argument(i18n.t('command.start.no_wait_for_completion_short'), i18n.t('command.start.no_wait_for_completion_long'),
1247
+ help=i18n.t('command.start.no_wait_for_completion_help'), default=False, action="store_true")
1192
1248
 
1193
1249
  # Define arguments.
1194
1250
  parser_define.add_argument(i18n.t('command.define.process_long'), help=i18n.t('command.define.process_help'), nargs='+')
@@ -1213,6 +1269,8 @@ class Command(Base):
1213
1269
  default=False, action="store_true")
1214
1270
  parser_download.add_argument(i18n.t('command.start.component_short'), i18n.t('command.start.component_long'), help=i18n.t('command.start.component_help'),
1215
1271
  nargs='*')
1272
+ parser_download.add_argument(i18n.t('command.start.no_wait_for_completion_short'), i18n.t('command.start.no_wait_for_completion_long'),
1273
+ help=i18n.t('command.start.no_wait_for_completion_help'), default=False, action="store_true")
1216
1274
 
1217
1275
  return parser.parse_args()
1218
1276
 
@@ -1378,7 +1436,7 @@ class Command(Base):
1378
1436
 
1379
1437
  def __process_name_to_file_name(self, process_name):
1380
1438
  """
1381
- Converts a process name into a same file name (without extension).
1439
+ Converts a process name into a sane file name (without extension).
1382
1440
 
1383
1441
  Args:
1384
1442
  process_name: The process name to convert.
@@ -92,6 +92,7 @@ class Model(Base):
92
92
  _FIELD_OUTPUTS = 'outputs'
93
93
  _FIELD_PROCESS_STATUS = 'process_status'
94
94
  _FIELD_RESOLUTION = 'resolution'
95
+ _FIELD_RUNTIME = 'runtime'
95
96
  _FIELD_SD = 'sd'
96
97
  _FIELD_SELECTOR = 'selector'
97
98
  _FIELD_SELECTORS = 'selectors'
@@ -99,6 +100,7 @@ class Model(Base):
99
100
  _FIELD_STAC_ITEM = 'stac_item'
100
101
  _FIELD_STAC_ITEM_FILE = 'stac_item_file'
101
102
  _FIELD_STARTED_AT = 'started_at'
103
+ _FIELD_SUCCESS = 'success'
102
104
  _FIELD_PUBLISHABLE = 'publishable'
103
105
  _FIELD_SIZE = 'size'
104
106
  _FIELD_UNIT = 'unit'
@@ -770,7 +772,7 @@ class Model(Base):
770
772
  Returns:
771
773
  A string representation of the object.
772
774
  """
773
- return f"{self.__class__.__name__}{self.attributes}"
775
+ return i18n.t('models.model.representation', name=self.__class__.__name__, attributes=self.attributes)
774
776
 
775
777
  def to_csv(self, exclude=None):
776
778
  """
@@ -228,7 +228,12 @@ class Option(Model):
228
228
  Returns:
229
229
  A string representation of the object.
230
230
  """
231
- return f"{self.title} ('{self.name}', {self.data_type}{' ' + str(self.constrained_values) if self.data_type == fusion_platform.DATA_TYPE_CONSTRAINED else ''}{', required' if self.required else ''}) = {self.value}: {self.description}"
231
+ constrained_values = i18n.t('models.process.option.representation_constrained_values',
232
+ constrained_values=self.constrained_values) if self.data_type == fusion_platform.DATA_TYPE_CONSTRAINED else ''
233
+ required = i18n.t('models.process.option.representation_required') if self.required else ''
234
+
235
+ return i18n.t('models.process.option.representation', title=self.title, name=self.name, data_type=self.data_type, constrained_values=constrained_values,
236
+ required=required, value=self.value, description=self.description)
232
237
 
233
238
 
234
239
  # Define a model class for an input which overrides the representation to display more relevant information.
@@ -243,7 +248,10 @@ class Input(Model):
243
248
  Returns:
244
249
  A string representation of the object.
245
250
  """
246
- return f"{self.title} ({self.file_type}) = {self.name + ' (' + str(self.id) + ')' if hasattr(self, self.__class__._FIELD_NAME) and hasattr(self, self.__class__._FIELD_ID) else None}: {self.description}"
251
+ name = i18n.t('models.process.input.representation_name', name=self.name) if hasattr(self, self.__class__._FIELD_NAME) else ''
252
+ id = i18n.t('models.process.input.representation_id', id=self.id) if hasattr(self, self.__class__._FIELD_ID) else ''
253
+
254
+ return i18n.t('models.process.input.representation', title=self.title, file_type=self.file_type, name=name, id=id, description=self.description)
247
255
 
248
256
 
249
257
  # Define a model class for a dispatcher which overrides the representation to display more relevant information.
@@ -268,7 +276,10 @@ class Dispatcher(Model):
268
276
  Returns:
269
277
  A string representation of the object.
270
278
  """
271
- return f"{self.name} ({self.ssd_id}, intermediate {self.dispatch_intermediate if hasattr(self, self.__class__._FIELD_DISPATCH_INTERMEDIATE) else False}): {self.documentation_summary}"
279
+ dispatch_intermediate = self.dispatch_intermediate if hasattr(self, self.__class__._FIELD_DISPATCH_INTERMEDIATE) else False
280
+
281
+ return i18n.t('models.process.dispatcher.representation', name=self.name, ssd_id=self.ssd_id, dispatch_intermediate=dispatch_intermediate,
282
+ documentation_summary=self.documentation_summary)
272
283
 
273
284
 
274
285
  # Define the model schema classes. These are maintained from the API definitions.
@@ -51,5 +51,7 @@
51
51
  * **tmp_total_bytes**: The total temporary disk space in bytes.
52
52
  * **tmp_free_bytes**: The free temporary disk space in bytes.
53
53
  * **s3_transfer_bytes**: The number of bytes transferred in from S3.
54
- * **external_transfer_bytes**: The number of bytes transferred in externally, excluding S3.
54
+ * **gcs_transfer_bytes**: The number of bytes transferred in from Google Cloud Storage.
55
+ * **external_transfer_bytes**: The number of bytes transferred in externally.
56
+ * **internal_transfer_bytes**: The number of bytes transferred in internally.
55
57
  * **comment**: Any comment recorded with the metric.
@@ -93,7 +93,9 @@ class ProcessServiceExecutionMetricSchema(Schema):
93
93
  tmp_total_bytes = fields.Integer(allow_none=True)
94
94
  tmp_free_bytes = fields.Integer(allow_none=True)
95
95
  s3_transfer_bytes = fields.Integer(allow_none=True)
96
+ gcs_transfer_bytes = fields.Integer(allow_none=True)
96
97
  external_transfer_bytes = fields.Integer(allow_none=True)
98
+ internal_transfer_bytes = fields.Integer(allow_none=True)
97
99
  comment = fields.String(allow_none=True)
98
100
 
99
101
  class Meta:
@@ -46,6 +46,7 @@ i18n.add_translation('command.log_field', '%{field}: %{value}', 'en')
46
46
  i18n.add_translation('command.log_subdivider', '................................................................................................', 'en')
47
47
  i18n.add_translation('command.log_divider', '------------------------------------------------------------------------------------------------', 'en')
48
48
  i18n.add_translation('command.log_bookend', '************************************************************************************************', 'en')
49
+ i18n.add_translation('command.wait_for_completion', ' and waiting for completion', 'en')
49
50
  i18n.add_translation('command.wait_for_next', 'Waiting for next execution...', 'en')
50
51
  i18n.add_translation('command.using_storage', 'Uploading storage \'%{filename}\' with id %{storage_id} to \'%{storage_name}\'', 'en')
51
52
  i18n.add_translation('command.using_service', 'Using service \'%{service}\'', 'en')
@@ -58,6 +59,7 @@ i18n.add_translation('command.remove_process', 'Removing input \'%{process}\'',
58
59
  i18n.add_translation('command.remove_input', 'Removing input \'%{input}\'', 'en')
59
60
  i18n.add_translation('command.prompt_yes', 'Yes', 'en')
60
61
  i18n.add_translation('command.prompt_no', 'No', 'en')
62
+ i18n.add_translation('command.prompt_option', '%{title} (\'%{name}\', %{data_type}%{constrained_values}%{required})', 'en')
61
63
  i18n.add_translation('command.password', 'Password', 'en')
62
64
  i18n.add_translation('command.organisation', 'Organisation', 'en')
63
65
  i18n.add_translation('command.find_inputs', 'Finding %{inputs} input file(s)...', 'en')
@@ -66,11 +68,12 @@ i18n.add_translation('command.email', 'Email', 'en')
66
68
  i18n.add_translation('command.download_process_execution', 'Downloading \'%{process}\' (inputs %{inputs}, outputs %{outputs}, storage %{storage}, intermediate %{intermediate}, metrics %{metrics}, components %{components})', 'en')
67
69
  i18n.add_translation('command.download_process', 'Downloading process \'%{process}\' to %{output}', 'en')
68
70
  i18n.add_translation('command.download_files', 'Downloading %{files} file(s)...', 'en')
69
- i18n.add_translation('command.download_executions', 'Gathering files for %{executions} execution(s)...', 'en')
71
+ i18n.add_translation('command.download_executions', 'Gathering files%{wait} for %{executions} execution(s)...', 'en')
70
72
  i18n.add_translation('command.download_execution', 'Downloading execution %{execution} to %{output}', 'en')
71
73
  i18n.add_translation('command.define_storage', 'Defining storage file(s) for %{slices} slice(s)...', 'en')
72
74
  i18n.add_translation('command.define_process', 'Defining \'%{process}\' (inputs %{inputs}, storage %{storage})', 'en')
73
75
  i18n.add_translation('command.define_inputs', 'Defining %{files} input file(s)...', 'en')
76
+ i18n.add_translation('command.dispatch_intermediate', 'Dispatch intermediate components?', 'en')
74
77
  i18n.add_translation('command.add_dispatcher', 'Add %{dispatcher} dispatcher?', 'en')
75
78
  i18n.add_translation('command.download.process_help', 'the name of the process, the process id or execution id to download', 'en')
76
79
  i18n.add_translation('command.download.process_long', 'process', 'en')
@@ -80,6 +83,9 @@ i18n.add_translation('command.define.process_help', 'the name of the process, th
80
83
  i18n.add_translation('command.define.process_long', 'process', 'en')
81
84
  i18n.add_translation('command.define.help', 'outputs a YAML file for each existing process', 'en')
82
85
  i18n.add_translation('command.define.command', 'define', 'en')
86
+ i18n.add_translation('command.start.no_wait_for_completion_help', 'do not wait for the execution to complete (default %%(default)s)\'', 'en')
87
+ i18n.add_translation('command.start.no_wait_for_completion_long', '--no_wait_for_completion', 'en')
88
+ i18n.add_translation('command.start.no_wait_for_completion_short', '-n', 'en')
83
89
  i18n.add_translation('command.start.metrics_help', 'save process metrics to file (default %%(default)s)', 'en')
84
90
  i18n.add_translation('command.start.metrics_long', '--metrics', 'en')
85
91
  i18n.add_translation('command.start.metrics_short', '-t', 'en')
@@ -280,6 +286,7 @@ i18n.add_translation('models.model.no_such_keys', 'No such keys %{keys}', 'en')
280
286
  i18n.add_translation('models.model.readonly_property', 'Property %{property} is read-only and cannot be set', 'en')
281
287
  i18n.add_translation('models.model.not_persisted', 'Model is not persisted in the Fusion Platform(r)', 'en')
282
288
  i18n.add_translation('models.model.already_persisted', 'Model is already persisted in the Fusion Platform(r)', 'en')
289
+ i18n.add_translation('models.model.representation', '%{name}%{attributes}', 'en')
283
290
  i18n.add_translation('models.process.failed_copy', 'Failed to get process from copy response', 'en')
284
291
  i18n.add_translation('models.process.execution_should_have_started', 'Process execution should have started by now', 'en')
285
292
  i18n.add_translation('models.process.execution_stopped', 'Process execution has been stopped', 'en')
@@ -298,4 +305,11 @@ i18n.add_translation('models.process.option.constrained_values.description', 'Th
298
305
  i18n.add_translation('models.process.option.constrained_values.title', 'Constrained Values', 'en')
299
306
  i18n.add_translation('models.process.option.constrained_names.description', 'The constrained value names for the option.', 'en')
300
307
  i18n.add_translation('models.process.option.constrained_names.title', 'Constrained Names', 'en')
308
+ i18n.add_translation('models.process.option.representation_required', ', required', 'en')
309
+ i18n.add_translation('models.process.option.representation_constrained_values', ' %{constrained_values}', 'en')
310
+ i18n.add_translation('models.process.option.representation', '%{title} (\'%{name}\', %{data_type}%{constrained_values}%{required}) = %{value}: %{description}', 'en')
311
+ i18n.add_translation('models.process.input.representation_id', ' (%{id})', 'en')
312
+ i18n.add_translation('models.process.input.representation_name', ' %{name}', 'en')
313
+ i18n.add_translation('models.process.input.representation', '%{title} (%{file_type}) =%{name}%{id}: %{description}', 'en')
314
+ i18n.add_translation('models.process.dispatcher.representation', '%{name} (%{ssd_id}, intermediate %{dispatch_intermediate}): %{documentation_summary}', 'en')
301
315
  # @formatter:on
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: fusion-platform-python-sdk
3
- Version: 1.16.6
3
+ Version: 1.17.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
@@ -8,11 +8,11 @@ Author-email: support@d-cat.co.uk
8
8
  License: MIT
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.8
12
11
  Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE.txt
18
18
  Requires-Dist: marshmallow
@@ -48,7 +48,7 @@ API, and this is defined within the corresponding OpenAPI 3.0 specification, whi
48
48
 
49
49
  ## Installation
50
50
 
51
- The SDK has been built for Python 3, and is tested against Python 3.8, 3.9, 3.10, 3.11 and 3.12. To install the SDK into a suitable Python environment containing
51
+ The SDK has been built for Python 3, and is tested against Python 3.9, 3.10, 3.11, 3.12 and 3.13. To install the SDK into a suitable Python environment containing
52
52
  `pip`, execute the following:
53
53
 
54
54
  ```shell
@@ -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.16.6',
22
+ version='1.17.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',
@@ -30,11 +30,11 @@ setup(
30
30
  classifiers=[
31
31
  'License :: OSI Approved :: MIT License',
32
32
  'Programming Language :: Python :: 3',
33
- 'Programming Language :: Python :: 3.8',
34
33
  'Programming Language :: Python :: 3.9',
35
34
  'Programming Language :: Python :: 3.10',
36
35
  'Programming Language :: Python :: 3.11',
37
36
  'Programming Language :: Python :: 3.12',
37
+ 'Programming Language :: Python :: 3.13',
38
38
  ],
39
39
  packages=find_packages(exclude='tests'),
40
40
  include_package_data=True,
@@ -66,7 +66,7 @@ class TestSession(CustomTestCase):
66
66
  session = Session()
67
67
 
68
68
  with pytest.raises(RequestError):
69
- session.download_file('https://www.d-cat.co.uk/.well-known/acme-challenge/test.html', destination)
69
+ session.download_file('https://www.d-cat.co.uk/assets/test.html', destination)
70
70
 
71
71
  self.assertFalse(os.path.exists(destination))
72
72
  session.download_file('https://www.d-cat.co.uk/home', destination, self.download_callback)