fusion-platform-python-sdk 1.14.7__tar.gz → 1.15.12__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.14.7/fusion_platform_python_sdk.egg-info → fusion_platform_python_sdk-1.15.12}/PKG-INFO +1 -1
  2. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/__init__.py +3 -2
  3. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/base.py +1 -1
  4. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/command.py +232 -29
  5. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/fusion_platform_sdk.pdf +0 -0
  6. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/model.py +80 -46
  7. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/organisation.py +1 -2
  8. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/process.py +15 -3
  9. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/process_execution.md +1 -0
  10. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/process_execution.py +1 -0
  11. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/service.md +4 -0
  12. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/service.py +17 -0
  13. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/translations.py +46 -14
  14. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12/fusion_platform_python_sdk.egg-info}/PKG-INFO +1 -1
  15. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/setup.py +1 -1
  16. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/tests/test_session.py +3 -5
  17. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/LICENSE.txt +0 -0
  18. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/MANIFEST.in +0 -0
  19. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/README.md +0 -0
  20. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/__main__.py +0 -0
  21. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/common/__init__.py +0 -0
  22. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/common/raise_thread.py +0 -0
  23. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/common/utilities.py +0 -0
  24. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/detailed_example.py +0 -0
  25. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/documentation.py +0 -0
  26. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/glasgow.geojson +0 -0
  27. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/lake_district.geojson +0 -0
  28. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/localisations.py +0 -0
  29. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/localise.py +0 -0
  30. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/__init__.py +0 -0
  31. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/credit.md +0 -0
  32. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/credit.py +0 -0
  33. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/data.md +0 -0
  34. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/data.py +0 -0
  35. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/data_file.md +0 -0
  36. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/data_file.py +0 -0
  37. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/fields.py +0 -0
  38. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/organisation.md +0 -0
  39. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/process.md +0 -0
  40. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/process_service_execution.md +0 -0
  41. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/process_service_execution.py +0 -0
  42. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/process_service_execution_log.md +0 -0
  43. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/process_service_execution_log.py +0 -0
  44. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/user.md +0 -0
  45. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/models/user.py +0 -0
  46. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/quick_example.py +0 -0
  47. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform/session.py +0 -0
  48. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform_python_sdk.egg-info/SOURCES.txt +0 -0
  49. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform_python_sdk.egg-info/dependency_links.txt +0 -0
  50. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform_python_sdk.egg-info/entry_points.txt +0 -0
  51. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform_python_sdk.egg-info/requires.txt +0 -0
  52. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/fusion_platform_python_sdk.egg-info/top_level.txt +0 -0
  53. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/setup.cfg +0 -0
  54. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/tests/test_base.py +0 -0
  55. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/tests/test_command.py +0 -0
  56. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/tests/test_init.py +0 -0
  57. {fusion_platform_python_sdk-1.14.7 → fusion_platform_python_sdk-1.15.12}/tests/test_localise.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fusion-platform-python-sdk
3
- Version: 1.14.7
3
+ Version: 1.15.12
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
@@ -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.14.7'
15
- __version_date__ = '2024-10-29T15:47:22Z'
14
+ __version__ = '1.15.12'
15
+ __version_date__ = '2024-12-20T11:33:56Z'
16
16
 
17
17
  # Exclude certain sub-modules from documentation.
18
18
  # @formatter:off
@@ -73,6 +73,7 @@ __pdoc__ = {
73
73
  'models.service.ServiceInputExpressionSchema': False,
74
74
  'models.service.ServiceOptionExpressionSchema': False,
75
75
  'models.service.ServiceValidationSchema': False,
76
+ 'models.service.ServiceOrganisationChargeExpressionSchema': False,
76
77
  'models.user.UserSchema.Meta': False,
77
78
  'models.user.UserSchema.opts': False,
78
79
  'models.user.UserOrganisationSchema': False,
@@ -31,7 +31,7 @@ class Base:
31
31
  Localise.setup()
32
32
 
33
33
  # Set up logging.
34
- logging.basicConfig(format='%(asctime)s.%(msecs)03d [%(levelname)s] %(pathname)s:%(funcName)s:%(lineno)d - %(message)s')
34
+ logging.basicConfig(format='%(asctime)s.%(msecs)03d [%(levelname)s] %(filename)s:%(funcName)s:%(lineno)d - %(message)s')
35
35
  self._logger = logging.getLogger(fusion_platform.FUSION_PLATFORM_LOGGER)
36
36
  self._logger.debug('sdk %s (%s): %s', fusion_platform.__version__, fusion_platform.__version_date__, self.__class__.__name__)
37
37
 
@@ -305,7 +305,7 @@ class Command(Base):
305
305
  non_aggregator_digits = len(str(non_aggregator_count))
306
306
  non_aggregator_chains = process.chains if hasattr(process, Model._FIELD_CHAINS) else []
307
307
 
308
- self.__print(logging.INFO, i18n.t('command.define_storage', files=non_aggregator_count))
308
+ self.__print(logging.INFO, i18n.t('command.define_storage', slices=non_aggregator_count))
309
309
  progress_bar = tqdm(total=non_aggregator_count, ncols=100)
310
310
 
311
311
  for group_index in range(1, non_aggregator_count + 1):
@@ -345,6 +345,60 @@ class Command(Base):
345
345
 
346
346
  return process_definition
347
347
 
348
+ def display_service(self, organisation, service_name):
349
+ """
350
+ Displays the documentation for the service. If the service does not exist, it will be ignored.
351
+
352
+ Args:
353
+ organisation: The logged in organisation.
354
+ service_name: The name of the service to display.
355
+
356
+ Returns:
357
+ True if a service was displayed.
358
+ """
359
+ found = False
360
+
361
+ # Try to find any existing service, ignoring any error.
362
+ try:
363
+ service = self.get_service(organisation, service_name)
364
+ found = True
365
+
366
+ self.__print_service(service)
367
+
368
+ except CommandError as e:
369
+ self.__print(logging.DEBUG, str(e))
370
+
371
+ return found
372
+
373
+ def display_process(self, organisation, process_name):
374
+ """
375
+ Displays the current configuration of the process. If the process does not exist, it will be ignored.
376
+
377
+ Args:
378
+ organisation: The logged in organisation.
379
+ process_name: The name of the process to display.
380
+
381
+ Returns:
382
+ True if a process was displayed.
383
+ """
384
+ found = False
385
+
386
+ # Try to find any existing process, ignoring any error.
387
+ try:
388
+ process, _ = self.get_process_or_execution(organisation, process_name)
389
+ found = True
390
+
391
+ self.__print_process(process)
392
+
393
+ # Display the status of any execution.
394
+ for execution in process.executions:
395
+ self.__print_execution(execution)
396
+
397
+ except CommandError as e:
398
+ self.__print(logging.DEBUG, str(e))
399
+
400
+ return found
401
+
348
402
  def __download_execution(self, process, download_inputs, download_outputs, download_storage, download_intermediate, download_components, output_dir, execution):
349
403
  """
350
404
  Downloads an execution.
@@ -756,10 +810,13 @@ class Command(Base):
756
810
 
757
811
  Args:
758
812
  organisation: The logged in organisation.
759
- process_name: The name of the process to be downloaded. This can also be the id of a process or an execution.
813
+ process_name: The name of the process. This can also be the id of a process or an execution.
760
814
 
761
815
  Returns:
762
816
  The found process and/or execution.
817
+
818
+ Raises:
819
+ CommandError: if a process or execution could not be found.
763
820
  """
764
821
 
765
822
  # Attempt to find the process.
@@ -782,6 +839,37 @@ class Command(Base):
782
839
 
783
840
  return process, execution
784
841
 
842
+ def get_service(self, organisation, service_name):
843
+ """
844
+ Attempts to find a service using the given service name, service id or SSD id.
845
+
846
+ Args:
847
+ organisation: The logged in organisation.
848
+ service_name: The name of the service. This can also be the id or SSD id of a service.
849
+
850
+ Returns:
851
+ The found service.
852
+
853
+ Raises:
854
+ CommandError: if a service could not be found.
855
+ """
856
+
857
+ # Find the service using its case-sensitive name (starts with).
858
+ service, _ = organisation.find_services(name=service_name)
859
+
860
+ # If the service cannot be found, try to find it by its id or SSD id.
861
+ if service is None:
862
+ service = next((item for item in organisation.services if (str(item.id) == service_name) or (str(item.ssd_id) == service_name)), None)
863
+
864
+ # If the service still cannot be found, then try it as a specific service for the organisation.
865
+ if service is None:
866
+ service = next((item for item in organisation.own_services if (str(item.id) == service_name) or (str(item.ssd_id) == service_name)), None)
867
+
868
+ if service is None:
869
+ raise CommandError(i18n.t('command.no_such_service', service=service_name))
870
+
871
+ return service
872
+
785
873
  def __get_storage_data_item(self, process, group_index, group_count, ssd_id, chain_index):
786
874
  """
787
875
  Gets the storage date item for a process' group and chain index.
@@ -896,7 +984,8 @@ class Command(Base):
896
984
  # Change the print and logging to the desired level.
897
985
  self._logger.setLevel(logging.CRITICAL)
898
986
 
899
- if arguments.verbose:
987
+ if arguments.debug:
988
+ self._logger.setLevel(logging.DEBUG)
900
989
  self.__print_level = logging.DEBUG
901
990
  else:
902
991
  self.__print_level = logging.INFO
@@ -905,8 +994,15 @@ class Command(Base):
905
994
  # Login to the correct deployment and select the organisation.
906
995
  organisation, _, _ = self.login(arguments.deployment, arguments.email, arguments.organisation)
907
996
 
908
- # Perform the start, define or download.
909
- if arguments.command == i18n.t('command.start.command'):
997
+ # Perform the display, start, define or download.
998
+ if arguments.command == i18n.t('command.display.command'):
999
+ # Display a service or process with the specified name or ids. If a service is displayed, we do not display a process.
1000
+ for process_or_service in arguments.process_or_service:
1001
+ if not self.display_service(organisation, process_or_service):
1002
+ if not self.display_process(organisation, process_or_service):
1003
+ self.__print(logging.WARNING, i18n.t('command.no_process_or_service', process_or_service=process_or_service))
1004
+
1005
+ elif arguments.command == i18n.t('command.start.command'):
910
1006
  # Start one or more processes.
911
1007
  for service_or_yaml in arguments.service_or_yaml:
912
1008
  # Extract the command line or YAML file arguments.
@@ -979,20 +1075,27 @@ class Command(Base):
979
1075
 
980
1076
  # Create the subparsers.
981
1077
  subparsers = parser.add_subparsers(help=i18n.t('command.subparser'), dest='command')
1078
+ parser.add_argument(i18n.t('command.version_short'), i18n.t('command.version_long'), help=i18n.t('command.version_help'), action='version',
1079
+ version=i18n.t('command.version_content', version=fusion_platform.__version__, version_date=fusion_platform.__version_date__))
1080
+
982
1081
  subparsers.required = True
1082
+ parser_display = subparsers.add_parser(i18n.t('command.display.command'), help=i18n.t('command.display.help'))
983
1083
  parser_start = subparsers.add_parser(i18n.t('command.start.command'), help=i18n.t('command.start.help'))
984
1084
  parser_define = subparsers.add_parser(i18n.t('command.define.command'), help=i18n.t('command.define.help'))
985
1085
  parser_download = subparsers.add_parser(i18n.t('command.download.command'), help=i18n.t('command.download.help'))
986
1086
 
987
1087
  # Add in the common arguments.
988
- for subparser in [parser_start, parser_define, parser_download]:
1088
+ for subparser in [parser_display, parser_start, parser_define, parser_download]:
989
1089
  subparser.add_argument(i18n.t('command.deployment_short'), i18n.t('command.deployment_long'), help=i18n.t('command.deployment_help'),
990
1090
  default=Command._DEPLOYMENT_PRODUCTION_IRL)
991
1091
  subparser.add_argument(i18n.t('command.email_short'), i18n.t('command.email_long'), help=i18n.t('command.email_help'))
992
1092
  subparser.add_argument(i18n.t('command.organisation_short'), i18n.t('command.organisation_long'), help=i18n.t('command.organisation_help'))
993
- subparser.add_argument(i18n.t('command.verbose_short'), i18n.t('command.verbose_long'), help=i18n.t('command.verbose_help'), default=False,
1093
+ subparser.add_argument(i18n.t('command.debug_short'), i18n.t('command.debug_long'), help=i18n.t('command.debug_help'), default=False,
994
1094
  action="store_true")
995
1095
 
1096
+ # Display arguments.
1097
+ parser_display.add_argument(i18n.t('command.display.service_or_process_long'), help=i18n.t('command.display.service_or_process_help'), nargs='+')
1098
+
996
1099
  # Start arguments.
997
1100
  parser_start.add_argument(i18n.t('command.start.definition_long'), help=i18n.t('command.start.definition_help'), nargs='+')
998
1101
  parser_start.add_argument(i18n.t('command.start.input_list_short'), i18n.t('command.start.input_list_long'), help=i18n.t('command.start.input_list_help'),
@@ -1059,6 +1162,48 @@ class Command(Base):
1059
1162
  else:
1060
1163
  print(message)
1061
1164
 
1165
+ def __print_execution(self, execution):
1166
+ """
1167
+ Prints an execution. This just shows the status in a simple format.
1168
+
1169
+ Args:
1170
+ execution: The execution to be logged.
1171
+ """
1172
+
1173
+ # Form the status of the execution.
1174
+ status = ''
1175
+ group = ''
1176
+
1177
+ if execution.success:
1178
+ status = i18n.t('command.log_execution_success')
1179
+
1180
+ elif execution.stopped:
1181
+ status = i18n.t('command.log_execution_stopped')
1182
+
1183
+ elif execution.progress < 100:
1184
+ status = i18n.t('command.log_execution_progress', progress=execution.progress)
1185
+
1186
+ 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
+ status = i18n.t('command.log_execution_failed', exit_type=exit_type, abort_reason=abort_reason)
1191
+
1192
+ started_at = execution.started_at.strftime('%Y-%m-%d %H:%M:%S')
1193
+ ended_at = i18n.t('command.log_execution_ended_at', ended_at=execution.ended_at.strftime('%Y-%m-%d %H:%M:%S')) if hasattr(execution,
1194
+ Model._FIELD_ENDED_AT) else ''
1195
+ period = i18n.t('command.log_execution_period', started_at=started_at, ended_at=ended_at)
1196
+ duration = i18n.t('command.log_execution_duration', minutes=round((execution.ended_at - execution.started_at).total_seconds() // 60)) if hasattr(execution,
1197
+ Model._FIELD_ENDED_AT) and (
1198
+ execution.ended_at is not None) else ''
1199
+
1200
+ if (hasattr(execution, Model._FIELD_GROUP_ID) and (execution.group_id is not None)) and (
1201
+ hasattr(execution, Model._FIELD_GROUP_INDEX) and (execution.group_index is not None)) and (
1202
+ hasattr(execution, Model._FIELD_GROUP_COUNT) and (execution.group_count is not None)):
1203
+ group = i18n.t('command.log_execution_group', group_id=execution.group_id, group_index=execution.group_index, group_count=execution.group_count)
1204
+
1205
+ self.__print(logging.INFO, i18n.t('command.log_execution', id=execution.id, period=period, status=status, duration=duration, group=group))
1206
+
1062
1207
  def __print_process(self, process):
1063
1208
  """
1064
1209
  Prints a process.
@@ -1073,26 +1218,88 @@ class Command(Base):
1073
1218
  if hasattr(process, field):
1074
1219
  self.__print(logging.INFO, i18n.t('command.log_field', field=field, value=getattr(process, field)))
1075
1220
 
1076
- self.__print(logging.INFO, i18n.t('command.log_divider'))
1077
- self.__print(logging.INFO, i18n.t('command.log_inputs'))
1221
+ process_inputs = [input for input in process.inputs]
1222
+
1223
+ if len(process_inputs) > 0:
1224
+ self.__print(logging.INFO, i18n.t('command.log_divider'))
1225
+ self.__print(logging.INFO, i18n.t('command.log_inputs'))
1226
+
1227
+ for data_item in process_inputs:
1228
+ self.__print(logging.INFO, str(data_item), format=False)
1229
+
1230
+ process_options = [option for option in process.options]
1231
+
1232
+ if len(process_options) > 0:
1233
+ self.__print(logging.INFO, i18n.t('command.log_divider'))
1234
+ self.__print(logging.INFO, i18n.t('command.log_options'))
1235
+
1236
+ for option in process_options:
1237
+ self.__print(logging.INFO, str(option), format=False)
1238
+
1239
+ process_dispatchers = [dispatcher for dispatcher in process.dispatchers]
1240
+
1241
+ if len(process_dispatchers) > 0:
1242
+ self.__print(logging.INFO, i18n.t('command.log_divider'))
1243
+ self.__print(logging.INFO, i18n.t('command.log_dispatchers'))
1244
+
1245
+ for dispatcher in process_dispatchers:
1246
+ self.__print(logging.INFO, i18n.t('command.log_subdivider'))
1247
+ self.__print(logging.INFO, str(dispatcher), format=False)
1078
1248
 
1079
- for data_item in process.inputs:
1080
- self.__print(logging.INFO, str(data_item), format=False)
1249
+ for option in dispatcher.options:
1250
+ self.__print(logging.INFO, str(option), format=False)
1251
+
1252
+ self.__print(logging.INFO, i18n.t('command.log_bookend'))
1253
+
1254
+ def __print_service(self, service):
1255
+ """
1256
+ Prints the documentation for a service.
1257
+
1258
+ Args:
1259
+ service: The service to be logged.
1260
+ """
1261
+ self.__print(logging.INFO, i18n.t('command.log_bookend'))
1262
+ self.__print(logging.INFO, i18n.t('command.log_service', service=service.name))
1263
+
1264
+ self.__print(logging.INFO, i18n.t('command.log_field', field=Model._FIELD_CATEGORIES, value=service.categories))
1265
+ self.__print(logging.INFO, i18n.t('command.log_field', field=Model._FIELD_KEYWORDS, value=service.keywords))
1081
1266
 
1082
1267
  self.__print(logging.INFO, i18n.t('command.log_divider'))
1083
- self.__print(logging.INFO, i18n.t('command.log_options'))
1268
+ self.__print(logging.INFO, i18n.t('command.log_summary'))
1269
+ self.__print(logging.INFO, service.documentation_summary, format=False)
1084
1270
 
1085
- for option in process.options:
1086
- self.__print(logging.INFO, str(option), format=False)
1271
+ self.__print(logging.INFO, i18n.t('command.log_divider'))
1272
+ self.__print(logging.INFO, i18n.t('command.log_description'))
1273
+ self.__print(logging.INFO, service.documentation_description, format=False)
1087
1274
 
1088
1275
  self.__print(logging.INFO, i18n.t('command.log_divider'))
1089
- self.__print(logging.INFO, i18n.t('command.log_dispatchers'))
1276
+ self.__print(logging.INFO, i18n.t('command.log_assumptions'))
1277
+ self.__print(logging.INFO, service.documentation_assumptions, format=False)
1278
+
1279
+ if len(service.documentation_actions.strip()) > 0:
1280
+ self.__print(logging.INFO, i18n.t('command.log_divider'))
1281
+ self.__print(logging.INFO, i18n.t('command.log_actions'))
1282
+ self.__print(logging.INFO, service.documentation_actions, format=False)
1283
+
1284
+ if hasattr(service, Model._FIELD_DOCUMENTATION_INPUTS):
1285
+ self.__print(logging.INFO, i18n.t('command.log_divider'))
1286
+ self.__print(logging.INFO, i18n.t('command.log_inputs'))
1287
+
1288
+ for input in service.documentation_inputs:
1289
+ self.__print(logging.INFO, str(input), format=False)
1090
1290
 
1091
- for dispatcher in process.dispatchers:
1092
- self.__print(logging.INFO, i18n.t('command.log_subdivider'))
1093
- self.__print(logging.INFO, str(dispatcher), format=False)
1291
+ if hasattr(service, Model._FIELD_DOCUMENTATION_OUTPUTS):
1292
+ self.__print(logging.INFO, i18n.t('command.log_divider'))
1293
+ self.__print(logging.INFO, i18n.t('command.log_outputs'))
1094
1294
 
1095
- for option in dispatcher.options:
1295
+ for output in service.documentation_outputs:
1296
+ self.__print(logging.INFO, str(output), format=False)
1297
+
1298
+ if hasattr(service, Model._FIELD_DOCUMENTATION_OPTIONS):
1299
+ self.__print(logging.INFO, i18n.t('command.log_divider'))
1300
+ self.__print(logging.INFO, i18n.t('command.log_options'))
1301
+
1302
+ for option in service.documentation_options:
1096
1303
  self.__print(logging.INFO, str(option), format=False)
1097
1304
 
1098
1305
  self.__print(logging.INFO, i18n.t('command.log_bookend'))
@@ -1234,16 +1441,8 @@ class Command(Base):
1234
1441
  else:
1235
1442
  data_items = []
1236
1443
 
1237
- # Find the service using its case-sensitive name (starts with).
1238
- service, _ = organisation.find_services(name=service_name)
1239
-
1240
- # If the service cannot be found, then try it as a specific service for the organisation.
1241
- if service is None:
1242
- service = next((item for item in organisation.own_services if item.name == service_name), None)
1243
-
1244
- if service is None:
1245
- raise CommandError(i18n.t('command.no_such_service', service=service_name))
1246
-
1444
+ # Find the service.
1445
+ service = self.get_service(organisation, service_name)
1247
1446
  self.__print(logging.DEBUG, i18n.t('command.using_service', service=service.name))
1248
1447
 
1249
1448
  # Create the process with its options and inputs.
@@ -1416,3 +1615,7 @@ def main():
1416
1615
  Main entry point for command line.
1417
1616
  """
1418
1617
  Command().main()
1618
+
1619
+
1620
+ if __name__ == "__main__":
1621
+ main()
@@ -35,14 +35,21 @@ class Model(Base):
35
35
  # Define the standard template model paths. Override these with the correct paths.
36
36
  _PATH_CREATE = None
37
37
  _PATH_DELETE = None
38
+ _PATH_EDIT = None
38
39
  _PATH_GET = None
39
40
  _PATH_NEW = None
40
41
  _PATH_PATCH = None
41
42
 
43
+ # Define the expected model and list extras. Override these with the correct values.
44
+ _EXTRAS_MODEL = None
45
+ _EXTRAS_LIST = None
46
+
42
47
  # Useful fields and templates.
48
+ _FIELD_ABORT_REASON = 'abort_reason'
43
49
  _FIELD_AVAILABLE_DISPATCHERS = 'available_dispatchers'
44
50
  _FIELD_BOUNDS = 'bounds'
45
51
  _FIELD_CATEGORY = 'category'
52
+ _FIELD_CATEGORIES = 'categories'
46
53
  _FIELD_CHAINS = 'chains'
47
54
  _FIELD_CHAIN_INDEX = 'chain_index'
48
55
  _FIELD_CONSTRAINED_NAMES = 'constrained_names'
@@ -53,8 +60,13 @@ class Model(Base):
53
60
  _FIELD_DISPATCHERS = 'dispatchers'
54
61
  _FIELD_DOCUMENTATION_SUMMARY = 'documentation_summary'
55
62
  _FIELD_DOCUMENTATION_DESCRIPTION = 'documentation_description'
63
+ _FIELD_DOCUMENTATION_INPUTS = 'documentation_inputs'
64
+ _FIELD_DOCUMENTATION_OUTPUTS = 'documentation_outputs'
65
+ _FIELD_DOCUMENTATION_OPTIONS = 'documentation_options'
66
+ _FIELD_ENDED_AT = 'ended_at'
56
67
  _FIELD_ERROR = 'error'
57
68
  _FIELD_EXECUTIONS = 'executions'
69
+ _FIELD_EXIT_TYPE = 'exit_type'
58
70
  _FIELD_EXTENSIONS = 'extensions'
59
71
  _FIELD_FILE = 'file'
60
72
  _FIELD_FILE_NAME = 'file_name'
@@ -153,6 +165,27 @@ class Model(Base):
153
165
  self.__model = None
154
166
  self.__persisted = False
155
167
 
168
+ @classmethod
169
+ def _extract_extras(cls, response, extras=None):
170
+ """
171
+ Extracts the optional extras from the response. If the extras are not found, they are not loaded.
172
+
173
+ Args:
174
+ response: The response to be modified.
175
+ extras: The optional list of extras as tuples (key, attribute) to extract.
176
+
177
+ Returns:
178
+ The extracted extras.
179
+ """
180
+ extracted_extras = {}
181
+ model_extras = response.get(Model._RESPONSE_KEY_EXTRAS, {})
182
+
183
+ for key, attribute in (extras if extras is not None else []):
184
+ if (key is not None) and (attribute is not None):
185
+ extracted_extras[attribute] = model_extras.get(key)
186
+
187
+ return extracted_extras
188
+
156
189
  @property
157
190
  def attributes(self):
158
191
  """
@@ -308,9 +341,9 @@ class Model(Base):
308
341
  def get(self, **kwargs):
309
342
  """
310
343
  Gets the model object by loading it from the Fusion Platform<sup>&reg;</sup>. Uses the model's current id and base model id for the get unless explicit
311
- values are provided via keyword arguments. This assumes the model is obtained using a GET RESTful request, and that the model data is held with the expected
312
- dictionary key within the response. The model is then loaded using the supplied schema to obtain the corresponding Python representation of it, before
313
- loading it into the model as a set of read-only attributes.
344
+ values are provided via keyword arguments. Any expected extras are also added. This assumes the model is obtained using a GET RESTful request, and that the
345
+ model data is held with the expected dictionary key within the response. The model is then loaded using the supplied schema to obtain the corresponding
346
+ Python representation of it, before loading it into the model as a set of read-only attributes.
314
347
 
315
348
  Args:
316
349
  kwargs: Any explicit ids to be used.
@@ -431,7 +464,7 @@ class Model(Base):
431
464
  @classmethod
432
465
  def _model_from_api_id(cls, session, **kwargs):
433
466
  """
434
- Loads a model object with the given ids as keyword arguments from the Fusion Platform<sup>&reg;</sup>.
467
+ Loads a model object with the given ids as keyword arguments from the Fusion Platform<sup>&reg;</sup>. Any required extras are also added.
435
468
 
436
469
  Args:
437
470
  session: The linked session object for interfacing with the Fusion Platform<sup>&reg;</sup>.
@@ -455,7 +488,7 @@ class Model(Base):
455
488
  @classmethod
456
489
  def _models_from_api_ids(cls, session, ids):
457
490
  """
458
- Generates an iterator through a series of models using their ids. Each model is loaded using an id.
491
+ Generates an iterator through a series of models using their ids. Each model is loaded using an id. Any extras are optionally added.
459
492
 
460
493
  Args:
461
494
  session: The linked session object for interfacing with the Fusion Platform<sup>&reg;</sup>.
@@ -471,10 +504,10 @@ class Model(Base):
471
504
  return (cls._model_from_api_id(session, **id) for id in ids)
472
505
 
473
506
  @classmethod
474
- def _models_from_api_path(cls, session, path, items_per_request=24, reverse=False, filter=None, search=None, **kwargs):
507
+ def _models_from_api_path(cls, session, path, items_per_request=24, reverse=False, filter=None, search=None, load_extras=True, **kwargs):
475
508
  """
476
- Generates an iterator through a series of models using a path which returns a list of objects. Each model is loaded from the list. Since API lists are
477
- paged, the generator takes into account having to get subsequent pages of results.
509
+ Generates an iterator through a series of models using a path which returns a list of objects. Each model is loaded from the list with its expected extras.
510
+ Since API lists are paged, the generator takes into account having to get subsequent pages of results.
478
511
 
479
512
  Use the items per request, reverse and filter parameters to modify the set of results returned. The items per request parameter determines how many results
480
513
  are returned by each request to the API for a page of results, up to the maximum number of results available. The reverse parameter can be used to return
@@ -508,6 +541,7 @@ class Model(Base):
508
541
  reverse: Whether the list should be reversed or not. Default False.
509
542
  filter: The optional filter to be applied to the results. Default is no filter.
510
543
  search: The optional search term to be applied to the results. Default to no search term.
544
+ load_extras: Should the model extras be automatically loaded? Default True.
511
545
  kwargs: Any additional attributes which are to be set on each model which are not provided in each item from the path.
512
546
 
513
547
  Returns:
@@ -541,55 +575,40 @@ class Model(Base):
541
575
  Model._RESPONSE_KEY_LAST) is not None else {}
542
576
  finished = len(last) <= 0
543
577
 
544
- # Build the generator around all of the returned items, which must all have been persisted.
578
+ # Optionally extract the extras.
579
+ extracted_extras = cls._extract_extras(response, extras=cls._EXTRAS_LIST) if load_extras else {}
580
+
581
+ # Build the generator around all of the returned items, which must all have been persisted. Optional extras are added to each model.
545
582
  for item in response.get(Model._RESPONSE_KEY_LIST, []):
583
+ # Add the optional extras into the item.
584
+ for key, value in extracted_extras.items():
585
+ item[key] = value
586
+
587
+ # Build the model from the modified item dictionary.
546
588
  model = cls(session)
547
589
  model._set_model_from_response(item, **kwargs)
548
590
  model.__persisted = True
549
591
 
550
592
  yield model
551
593
 
552
- def _new(self, query_parameters=None, extras=None, **kwargs):
594
+ def _new(self, query_parameters=None, **kwargs):
553
595
  """
554
- Gets a new template model object by loading it from the Fusion Platform<sup>&reg;</sup>. The explicit base model id value should be provided via a keyword
555
- argument. This assumes the model is obtained using a GET RESTful request from the corresponding path, and that the model data is held with the expected
556
- dictionary key within the response. The template model is then loaded using the supplied schema to obtain the corresponding Python representation of it,
557
- before loading it into the model as a set of read-only attributes. If any extras are provided as a list of tuples (key, attribute), where key refers to the
558
- field in the extras, and attribute is the corresponding model attribute to be set from the field, then each is added to the model.
596
+ Gets a new template model object by loading it from the Fusion Platform<sup>&reg;</sup>. Any expected extras are also added. The explicit base model id
597
+ value should be provided via a keyword argument. This assumes the model is obtained using a GET RESTful request from the corresponding path, and that the
598
+ model data is held with the expected dictionary key within the response. The template model is then loaded using the supplied schema to obtain the
599
+ corresponding Python representation of it, before loading it into the model as a set of read-only attributes.
559
600
 
560
601
  Args:
561
602
  query_parameters: The optional query parameters as a dictionary.
562
- extras: The optional list of extras tuples (key, attribute) which should be added to the model.
563
603
  kwargs: Should include the base model id, if needed.
564
604
 
565
605
  Raises:
566
606
  RequestError: if the new fails.
567
607
  ModelError: if the model could not be loaded or validated from the Fusion Platform<sup>&reg;</sup>.
568
608
  """
569
- # Get the data.
570
- response = self._session.request(path=self._get_path(self.__class__._PATH_NEW, **kwargs), query_parameters=query_parameters)
571
609
 
572
- # Assume that the resulting model is held within the expected key within the resulting dictionary.
573
- if Model._RESPONSE_KEY_MODEL not in response:
574
- raise ModelError(i18n.t('models.model.failed_model_new_model'))
575
-
576
- # If any extras are required, extract them.
577
- if (Model._RESPONSE_KEY_EXTRAS not in response) and (extras is not None):
578
- raise ModelError(i18n.t('models.model.failed_model_new_extras'))
579
-
580
- modified_response = response.get(Model._RESPONSE_KEY_MODEL, {})
581
- model_extras = response.get(Model._RESPONSE_KEY_EXTRAS, {})
582
-
583
- for key, attribute in (extras if extras is not None else []):
584
- if (key is not None) and (attribute is not None):
585
- modified_response[attribute] = model_extras.get(key)
586
-
587
- # Add in the keyword arguments to the response so that they are set in the model as well.
588
- for key, value in kwargs.items():
589
- modified_response[key] = value
590
-
591
- # Load the response into the model. We ignore missing required fields and those which are None.
592
- self._set_model_from_response(modified_response, partial=True)
610
+ # Get and load the data, setting the extras as well.
611
+ self._send_and_load(self._get_path(self.__class__._PATH_NEW, **kwargs), partial=True, query_parameters=query_parameters, **kwargs)
593
612
 
594
613
  # The model is not persisted.
595
614
  self.__persisted = False
@@ -604,25 +623,40 @@ class Model(Base):
604
623
  """
605
624
  return self.__persisted
606
625
 
607
- def _send_and_load(self, path, method=Session.METHOD_GET, body=None, key=_RESPONSE_KEY_MODEL):
626
+ def _send_and_load(self, path, method=Session.METHOD_GET, body=None, key=_RESPONSE_KEY_MODEL, partial=False, query_parameters=None, **kwargs):
608
627
  """
609
- Sends the body to the path using the method and then loads the resulting model.
628
+ Sends the body to the path using the method and then loads the resulting model. Additional query parameters may be specified that should be added to the
629
+ model. Keyword arguments can be used to be added to the model, overriding any values that may have been retrieved.
610
630
 
611
631
  Args:
612
632
  path: The path to send the request to.
613
633
  method: The optional RESTful method for sending. Default is GET.
614
634
  body: The optional body to send. Default is None.
615
635
  key: The optional key to load the model from the response. Default is RESPONSE_KEY_MODEL.
636
+ partial: Skip validation of required fields which are missing? Default False.
637
+ query_parameters: The optional query parameters as a dictionary.
638
+ kwargs: Should include the base model id, if needed.
616
639
  """
617
640
  # Send the request.
618
- response = self._session.request(path=path, method=method, body=body)
641
+ response = self._session.request(path=path, query_parameters=query_parameters, method=method, body=body)
619
642
 
620
643
  # Assume that the resulting model is held within the expected key within the resulting dictionary.
621
644
  if key not in response:
622
645
  raise ModelError(i18n.t('models.model.failed_model_send_and_load'))
623
646
 
624
- # Load the response into the model.
625
- self._set_model_from_response(response.get(key, {}))
647
+ # If any extras are required, extract them and add them to the model.
648
+ modified_response = response.get(key, {}) if key is not None else response
649
+ extracted_extras = self.__class__._extract_extras(response, extras=self.__class__._EXTRAS_MODEL)
650
+
651
+ for key, value in extracted_extras.items():
652
+ modified_response[key] = value
653
+
654
+ # Add in the keyword arguments to the response so that they are set in the model as well.
655
+ for key, value in kwargs.items():
656
+ modified_response[key] = value
657
+
658
+ # Load the response into the model. Optionally ignore missing required fields and those which are None.
659
+ self._set_model_from_response(modified_response, partial=partial)
626
660
 
627
661
  def __setattr__(self, key, value):
628
662
  """
@@ -760,8 +794,8 @@ class Model(Base):
760
794
  def update(self, **kwargs):
761
795
  """
762
796
  Attempts to update the model object with the given values. For models which have not been persisted, the relevant fields are updated without validation,
763
- which will occur when the model is persisted. For models which have been persisted, the update is made via the Fusion Platform<sup>&reg;</sup>. All current model
764
- attributes are sent to the Fusion Platform<sup>&reg;</sup> which will automatically ignore any which are not allowed or are read-only.
797
+ which will occur when the model is persisted. For models which have been persisted, the update is made via the Fusion Platform<sup>&reg;</sup>. All current
798
+ model attributes are sent to the Fusion Platform<sup>&reg;</sup> which will automatically ignore any which are not allowed or are read-only.
765
799
 
766
800
  Args:
767
801
  kwargs: The model attributes which are to be patched.
@@ -301,8 +301,7 @@ class Organisation(Model):
301
301
  """
302
302
  # Get a new template for the process model using the service.
303
303
  process = Process(self._session)
304
- process._new(query_parameters={Model._get_id_name(Service.__name__): service.id},
305
- extras=[(self.__class__._FIELD_DISPATCHERS, self.__class__._FIELD_AVAILABLE_DISPATCHERS)], organisation_id=self.id, name=name)
304
+ process._new(query_parameters={Model._get_id_name(Service.__name__): service.id}, organisation_id=self.id, name=name)
306
305
 
307
306
  return process
308
307
 
@@ -228,7 +228,7 @@ 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}{', required' if self.required else ''}) = {self.value}: {self.description}"
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}"
232
232
 
233
233
 
234
234
  # Define a model class for an input which overrides the representation to display more relevant information.
@@ -319,6 +319,7 @@ class ProcessExecutionStatusSchema(Schema):
319
319
  group_count = fields.Integer(allow_none=True)
320
320
  job_status = fields.String(required=True)
321
321
  abort_reason = fields.String(allow_none=True)
322
+ exit_type = fields.String(allow_none=True)
322
323
  started_at = fields.DateTime(required=True)
323
324
  ended_at = fields.DateTime(allow_none=True)
324
325
  delete_warning_status = fields.String(required=True)
@@ -517,6 +518,10 @@ class Process(Model):
517
518
  _PATH_EXECUTIONS = f"{_PATH_BASE}/executions"
518
519
  _PATH_STOP = f"{_PATH_BASE}/stop"
519
520
 
521
+ # Define the expected model and list extras.
522
+ _EXTRAS_MODEL = [(Model._FIELD_DISPATCHERS, Model._FIELD_AVAILABLE_DISPATCHERS)]
523
+ _EXTRAS_LIST = [(Model._FIELD_DISPATCHERS, Model._FIELD_AVAILABLE_DISPATCHERS)]
524
+
520
525
  # Process status values.
521
526
  _PROCESS_STATUS_EXECUTE = 'execute'
522
527
  _PROCESS_STATUS_STOP = 'stop'
@@ -631,6 +636,12 @@ class Process(Model):
631
636
  modified_response = response.get(Model._RESPONSE_KEY_MODEL, {})
632
637
  modified_response['organisation_id'] = self.organisation_id
633
638
 
639
+ # Extract the extras.
640
+ extracted_extras = self.__class__._extract_extras(response, extras=self.__class__._EXTRAS_LIST)
641
+
642
+ for key, value in extracted_extras.items():
643
+ modified_response[key] = value
644
+
634
645
  # Load the response into the model. We ignore missing required fields and those which are None.
635
646
  process._set_model_from_response(modified_response, partial=True)
636
647
 
@@ -745,7 +756,8 @@ class Process(Model):
745
756
  RequestError: if any get fails.
746
757
  ModelError: if a model could not be loaded or validated from the Fusion Platform<sup>&reg;</sup>.
747
758
  """
748
- return ProcessExecution._models_from_api_path(self._session, self._get_path(self.__class__._PATH_EXECUTIONS), reverse=True) # Most recent first.
759
+ return ProcessExecution._models_from_api_path(self._session, self._get_path(self.__class__._PATH_EXECUTIONS), reverse=True,
760
+ load_extras=False) # Most recent first.
749
761
 
750
762
  def find_executions(self, id=None, group_id=None):
751
763
  """
@@ -766,7 +778,7 @@ class Process(Model):
766
778
  [(self.__class__._FIELD_ID, self.__class__._FILTER_MODIFIER_EQ, id), (self.__class__._FIELD_GROUP_ID, self.__class__._FILTER_MODIFIER_EQ, group_id)])
767
779
 
768
780
  # Build the partial find generator and execute it.
769
- find = partial(ProcessExecution._models_from_api_path, self._session, self._get_path(self.__class__._PATH_EXECUTIONS), filter=filter)
781
+ find = partial(ProcessExecution._models_from_api_path, self._session, self._get_path(self.__class__._PATH_EXECUTIONS), filter=filter, load_extras=False)
770
782
  return self.__class__._first_and_generator(find)
771
783
 
772
784
  @property
@@ -33,6 +33,7 @@
33
33
  * **stopped**: Has the execution been stopped by a user?
34
34
  * **abort**: Has the execution been aborted?
35
35
  * **abort_reason**: The reason for any abort of the execution.
36
+ * **exit_type**: The type of exit experienced by the execution.
36
37
  * **success**: Has the execution completed successfully?
37
38
  * **progress**: Percentage progress of the execution.
38
39
  * **delete_expiry**: When will the execution expire and therefore be deleted?
@@ -106,6 +106,7 @@ class ProcessExecutionSchema(Schema):
106
106
  stopped = fields.Boolean(required=True, metadata={'read_only': True}) # Changed to prevent this being updated.
107
107
  abort = fields.Boolean(required=True, metadata={'read_only': True}) # Changed to prevent this being updated.
108
108
  abort_reason = fields.String(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
109
+ exit_type = fields.String(allow_none=True, metadata={'read_only': True}) # Changed to prevent this being updated.
109
110
  success = fields.Boolean(required=True, metadata={'read_only': True}) # Changed to prevent this being updated.
110
111
  progress = fields.Integer(required=True, metadata={'read_only': True}) # Changed to prevent this being updated.
111
112
  delete_expiry = fields.DateTime(required=True)
@@ -68,6 +68,10 @@
68
68
  * **charge_expression_platform**: The expression used to calculate the charge in FPUs levied for the platform.
69
69
  * **charge_expression_owner**: The expression used to calculate the charge in FPUs levied for the owner of the service.
70
70
  * **organisations**: The optional whitelist of organisations who can use this service.
71
+ * **organisation_charge_expressions**: For the whitelisted organisations, these provide the optional charge expressions to be applied.
72
+ * **id**: The owning organisation.
73
+ * **platform**: The expression used to calculate the charge in FPUs levied for the platform.
74
+ * **owner**: The expression used to calculate the charge in FPUs levied for the owner of the service.
71
75
  * **geographic_regions**: The optional list of geographic regions from which this service can be used.
72
76
  * **documentation_summary**: The documentation summary of the service.
73
77
  * **documentation_description**: The documentation description of the service.
@@ -184,6 +184,22 @@ class ServiceValidationSchema(Schema):
184
184
  ordered = True
185
185
 
186
186
 
187
+ class ServiceOrganisationChargeExpressionSchema(Schema):
188
+ """
189
+ Nested schema class for service organisation.
190
+ """
191
+ id = fields.UUID(required=True)
192
+ platform = fields.String(allow_none=True)
193
+ owner = fields.String(allow_none=True)
194
+
195
+ class Meta:
196
+ """
197
+ When loading an object, make sure we exclude any unknown fields, rather than raising an exception, and put fields in their definition order.
198
+ """
199
+ unknown = EXCLUDE
200
+ ordered = True
201
+
202
+
187
203
  class ServiceSchema(Schema):
188
204
  """
189
205
  Schema class for service model.
@@ -230,6 +246,7 @@ class ServiceSchema(Schema):
230
246
  charge_expression_owner = fields.String(allow_none=True) # Changed to optional.
231
247
 
232
248
  organisations = fields.List(fields.UUID(required=True), allow_none=True)
249
+ organisation_charge_expressions = fields.List(fields.Nested(ServiceOrganisationChargeExpressionSchema()), allow_none=True)
233
250
  geographic_regions = fields.List(fields.String(required=True), allow_none=True)
234
251
 
235
252
  # Removed creator.
@@ -13,19 +13,37 @@ import i18n
13
13
  # @formatter:off
14
14
  i18n.add_translation('command.validate_constrained_error', 'Not one of %{constrained}', 'en')
15
15
  i18n.add_translation('command.unknown_command', 'Unknown command \'%{command}\'', 'en')
16
+ i18n.add_translation('command.no_process_or_service', 'No such service, process or execution \'%{process_or_service}\'', 'en')
16
17
  i18n.add_translation('command.no_such_service', 'No such service \'%{service}\'', 'en')
17
18
  i18n.add_translation('command.no_such_process', 'No such process or execution \'%{process}\'', 'en')
18
19
  i18n.add_translation('command.no_such_organisation', 'No such organisation \'%{organisation}\'', 'en')
19
20
  i18n.add_translation('command.no_such_input', 'No such input \'%{input_name}\'', 'en')
20
21
  i18n.add_translation('command.no_executions', 'No executions for process \'%{process}\'', 'en')
21
22
  i18n.add_translation('command.invalid_login_response', 'Incorrect username or password', 'en')
22
- i18n.add_translation('command.log_subdivider', '................................................................................................', 'en')
23
- i18n.add_translation('command.log_process', 'Process \'%{process}\'', 'en')
23
+ i18n.add_translation('command.log_execution_group', ' (group \'%{group_id}\' %{group_index} of %{group_count})', 'en')
24
+ i18n.add_translation('command.log_execution_duration', ' - %{minutes} minute(s)', 'en')
25
+ i18n.add_translation('command.log_execution_period', ' (%{started_at}%{ended_at})', 'en')
26
+ i18n.add_translation('command.log_execution_ended_at', ' -> %{ended_at}', 'en')
27
+ i18n.add_translation('command.log_execution_failed', 'failed%{exit_type}%{abort_reason}', 'en')
28
+ i18n.add_translation('command.log_execution_exit_type', ' (%{exit_type})', 'en')
29
+ i18n.add_translation('command.log_execution_abort_reason', ': %{abort_reason}', 'en')
30
+ i18n.add_translation('command.log_execution_stopped', 'stopped', 'en')
31
+ i18n.add_translation('command.log_execution_progress', 'processing %{progress}%%', 'en')
32
+ i18n.add_translation('command.log_execution_success', 'success', 'en')
33
+ i18n.add_translation('command.log_actions', 'Actions:', 'en')
34
+ i18n.add_translation('command.log_assumptions', 'Assumptions:', 'en')
35
+ i18n.add_translation('command.log_description', 'Description:', 'en')
36
+ i18n.add_translation('command.log_summary', 'Summary:', 'en')
37
+ i18n.add_translation('command.log_dispatchers', 'Dispatchers:', 'en')
24
38
  i18n.add_translation('command.log_options', 'Options:', 'en')
39
+ i18n.add_translation('command.log_outputs', 'Outputs:', 'en')
25
40
  i18n.add_translation('command.log_inputs', 'Inputs:', 'en')
41
+ i18n.add_translation('command.log_execution', 'Execution \'%{id}\'%{period}: %{status}%{duration}%{group}', 'en')
42
+ i18n.add_translation('command.log_service', 'Service \'%{service}\'', 'en')
43
+ i18n.add_translation('command.log_process', 'Process \'%{process}\'', 'en')
26
44
  i18n.add_translation('command.log_field', '%{field}: %{value}', 'en')
45
+ i18n.add_translation('command.log_subdivider', '................................................................................................', 'en')
27
46
  i18n.add_translation('command.log_divider', '------------------------------------------------------------------------------------------------', 'en')
28
- i18n.add_translation('command.log_dispatchers', 'Dispatchers:', 'en')
29
47
  i18n.add_translation('command.log_bookend', '************************************************************************************************', 'en')
30
48
  i18n.add_translation('command.wait_for_next', 'Waiting for next execution...', 'en')
31
49
  i18n.add_translation('command.using_storage', 'Uploading storage \'%{filename}\' with id %{storage_id} to \'%{storage_name}\'', 'en')
@@ -48,7 +66,7 @@ i18n.add_translation('command.download_process', 'Downloading process \'%{proces
48
66
  i18n.add_translation('command.download_files', 'Downloading %{files} file(s)...', 'en')
49
67
  i18n.add_translation('command.download_executions', 'Gathering files for %{executions} execution(s)...', 'en')
50
68
  i18n.add_translation('command.download_execution', 'Downloading execution %{execution} to %{output}', 'en')
51
- i18n.add_translation('command.define_storage', 'Defining %{files} storage file(s)...', 'en')
69
+ i18n.add_translation('command.define_storage', 'Defining storage file(s) for %{slices} slice(s)...', 'en')
52
70
  i18n.add_translation('command.define_process', 'Defining \'%{process}\' (inputs %{inputs}, storage %{storage})', 'en')
53
71
  i18n.add_translation('command.define_inputs', 'Defining %{files} input file(s)...', 'en')
54
72
  i18n.add_translation('command.add_dispatcher', 'Add %{dispatcher} dispatcher?', 'en')
@@ -97,10 +115,18 @@ i18n.add_translation('command.start.definition_help', 'the names of the service
97
115
  i18n.add_translation('command.start.definition_long', 'service_or_yaml', 'en')
98
116
  i18n.add_translation('command.start.help', 'starts a process', 'en')
99
117
  i18n.add_translation('command.start.command', 'start', 'en')
100
- i18n.add_translation('command.subparser', 'starts, defines or downloads a process', 'en')
101
- i18n.add_translation('command.verbose_help', 'show verbose output (default \'%%(default)s\')', 'en')
102
- i18n.add_translation('command.verbose_long', '--verbose', 'en')
103
- i18n.add_translation('command.verbose_short', '-v', 'en')
118
+ i18n.add_translation('command.display.service_or_process_help', 'the names of the service, id, SSD id, process, process id or associated execution id that are to be displayed', 'en')
119
+ i18n.add_translation('command.display.service_or_process_long', 'process_or_service', 'en')
120
+ i18n.add_translation('command.display.help', 'displays service information or a configured process', 'en')
121
+ i18n.add_translation('command.display.command', 'display', 'en')
122
+ i18n.add_translation('command.subparser', 'displays a service or process, or starts, defines or downloads a process', 'en')
123
+ i18n.add_translation('command.version_content', '%%(prog)s %{version} (%{version_date})', 'en')
124
+ i18n.add_translation('command.version_help', 'show the version information and exit', 'en')
125
+ i18n.add_translation('command.version_long', '--version', 'en')
126
+ i18n.add_translation('command.version_short', '-v', 'en')
127
+ i18n.add_translation('command.debug_help', 'show debug output (default \'%%(default)s\')', 'en')
128
+ i18n.add_translation('command.debug_long', '--debug', 'en')
129
+ i18n.add_translation('command.debug_short', '-b', 'en')
104
130
  i18n.add_translation('command.organisation_help', 'the organisation to be used', 'en')
105
131
  i18n.add_translation('command.organisation_long', '--organisation', 'en')
106
132
  i18n.add_translation('command.organisation_short', '-g', 'en')
@@ -113,18 +139,26 @@ i18n.add_translation('command.deployment_short', '-y', 'en')
113
139
  i18n.add_translation('command.epilog', '''
114
140
  For more detailed options, use:
115
141
 
142
+ fusion_platform display --help
116
143
  fusion_platform start --help
117
144
  fusion_platform define --help
118
145
  fusion_platform download --help
119
146
 
120
147
  ''', 'en')
121
148
  i18n.add_translation('command.description', '''
122
- Use this command to execute any service (by case-sensitive name) or to retrieve its inputs and outputs. The service can be selected via command line options or
123
- defined by a YAML file. This command can also be used to build a YAML file from an existing process.
149
+ Use this command to display service information and processes, and to start, download or define processes. Processes can be selected via command line options or
150
+ defined by a YAML file.
124
151
 
125
152
  Usage:
126
- The following will attempt to create one or more processes from the given \'service_name\' to use the inputs (in order) and options and execute them. All processes
127
- will use the same inputs and options:
153
+ The following will display information about one or more services or processes from the given \'service_or_process_name\'.
154
+
155
+ fusion_platform display <service_or_process_name> ...
156
+
157
+ For a service, this will display the full service documentation, including its expected inputs and options. For a process, it will show the current process
158
+ configuration.
159
+
160
+ The following will attempt to create one or more processes from the given \'service_name\' to use the inputs (in order) and options and execute them. All
161
+ processes will use the same inputs and options:
128
162
 
129
163
  fusion_platform start <service_name> ... -l <input_file|input_name> ... -p <option=value> ...
130
164
 
@@ -237,8 +271,6 @@ i18n.add_translation('models.fields.boolean.invalid', 'Not a valid boolean', 'en
237
271
  i18n.add_translation('models.model.update_empty_body', 'Update cannot be requested as there are no attributes to be used (read-only attributes have been removed)', 'en')
238
272
  i18n.add_translation('models.model.create_empty_body', 'Create cannot be requested as there are no attributes to be used (read-only attributes have been removed)', 'en')
239
273
  i18n.add_translation('models.model.failed_model_validation', 'Failed to validate model: %{message}', 'en')
240
- i18n.add_translation('models.model.failed_model_new_extras', 'Failed to get model extras from response', 'en')
241
- i18n.add_translation('models.model.failed_model_new_model', 'Failed to get model template from response', 'en')
242
274
  i18n.add_translation('models.model.failed_model_send_and_load', 'Failed to request and load model', 'en')
243
275
  i18n.add_translation('models.model.no_such_keys', 'No such keys %{keys}', 'en')
244
276
  i18n.add_translation('models.model.readonly_property', 'Property %{property} is read-only and cannot be set', 'en')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fusion-platform-python-sdk
3
- Version: 1.14.7
3
+ Version: 1.15.12
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
@@ -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.14.7',
22
+ version='1.15.12',
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',
@@ -61,17 +61,15 @@ class TestSession(CustomTestCase):
61
61
  """
62
62
 
63
63
  with tempfile.TemporaryDirectory() as dir:
64
- destination = os.path.join(dir, 'file.pdf')
64
+ destination = os.path.join(dir, 'file.html')
65
65
 
66
66
  session = Session()
67
67
 
68
68
  with pytest.raises(RequestError):
69
- session.download_file('https://www.d-cat.co.uk/rails/active_storage/blobs/rubbish/Water%20RA%20Webinar%20IoT%20September%202020.pdf?', destination)
69
+ session.download_file('https://www.d-cat.co.uk/.well-known/acme-challenge/test.html', destination)
70
70
 
71
71
  self.assertFalse(os.path.exists(destination))
72
- session.download_file(
73
- 'https://www.d-cat.co.uk/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBkUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--8a6deba9596edd3e9f0c341d274ae1317da59bbc/Water%20RA%20Webinar%20IoT%20September%202020.pdf?',
74
- destination, self.download_callback)
72
+ session.download_file('https://www.d-cat.co.uk/home', destination, self.download_callback)
75
73
  self.assertTrue(os.path.exists(destination))
76
74
  self.assertIsNotNone(self._download_size)
77
75