idmtools-platform-comps 0.0.0.dev0__py3-none-any.whl → 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. idmtools_platform_comps/__init__.py +25 -8
  2. idmtools_platform_comps/cli/__init__.py +4 -0
  3. idmtools_platform_comps/cli/cli_functions.py +50 -0
  4. idmtools_platform_comps/cli/comps.py +492 -0
  5. idmtools_platform_comps/comps_cli.py +48 -0
  6. idmtools_platform_comps/comps_operations/__init__.py +6 -0
  7. idmtools_platform_comps/comps_operations/asset_collection_operations.py +263 -0
  8. idmtools_platform_comps/comps_operations/experiment_operations.py +569 -0
  9. idmtools_platform_comps/comps_operations/simulation_operations.py +678 -0
  10. idmtools_platform_comps/comps_operations/suite_operations.py +228 -0
  11. idmtools_platform_comps/comps_operations/workflow_item_operations.py +269 -0
  12. idmtools_platform_comps/comps_platform.py +309 -0
  13. idmtools_platform_comps/plugin_info.py +168 -0
  14. idmtools_platform_comps/ssmt_operations/__init__.py +6 -0
  15. idmtools_platform_comps/ssmt_operations/simulation_operations.py +77 -0
  16. idmtools_platform_comps/ssmt_operations/workflow_item_operations.py +73 -0
  17. idmtools_platform_comps/ssmt_platform.py +44 -0
  18. idmtools_platform_comps/ssmt_work_items/__init__.py +4 -0
  19. idmtools_platform_comps/ssmt_work_items/comps_work_order_task.py +29 -0
  20. idmtools_platform_comps/ssmt_work_items/comps_workitems.py +113 -0
  21. idmtools_platform_comps/ssmt_work_items/icomps_workflowitem.py +71 -0
  22. idmtools_platform_comps/ssmt_work_items/work_order.py +54 -0
  23. idmtools_platform_comps/utils/__init__.py +4 -0
  24. idmtools_platform_comps/utils/assetize_output/__init__.py +4 -0
  25. idmtools_platform_comps/utils/assetize_output/assetize_output.py +125 -0
  26. idmtools_platform_comps/utils/assetize_output/assetize_ssmt_script.py +144 -0
  27. idmtools_platform_comps/utils/base_singularity_work_order.json +6 -0
  28. idmtools_platform_comps/utils/download/__init__.py +4 -0
  29. idmtools_platform_comps/utils/download/download.py +178 -0
  30. idmtools_platform_comps/utils/download/download_ssmt.py +81 -0
  31. idmtools_platform_comps/utils/download_experiment.py +116 -0
  32. idmtools_platform_comps/utils/file_filter_workitem.py +519 -0
  33. idmtools_platform_comps/utils/general.py +358 -0
  34. idmtools_platform_comps/utils/linux_mounts.py +73 -0
  35. idmtools_platform_comps/utils/lookups.py +123 -0
  36. idmtools_platform_comps/utils/package_version.py +489 -0
  37. idmtools_platform_comps/utils/python_requirements_ac/__init__.py +4 -0
  38. idmtools_platform_comps/utils/python_requirements_ac/create_asset_collection.py +155 -0
  39. idmtools_platform_comps/utils/python_requirements_ac/install_requirements.py +109 -0
  40. idmtools_platform_comps/utils/python_requirements_ac/requirements_to_asset_collection.py +374 -0
  41. idmtools_platform_comps/utils/python_version.py +40 -0
  42. idmtools_platform_comps/utils/scheduling.py +154 -0
  43. idmtools_platform_comps/utils/singularity_build.py +491 -0
  44. idmtools_platform_comps/utils/spatial_output.py +76 -0
  45. idmtools_platform_comps/utils/ssmt_utils/__init__.py +6 -0
  46. idmtools_platform_comps/utils/ssmt_utils/common.py +70 -0
  47. idmtools_platform_comps/utils/ssmt_utils/file_filter.py +568 -0
  48. idmtools_platform_comps/utils/sweeping.py +162 -0
  49. idmtools_platform_comps-0.0.2.dist-info/METADATA +100 -0
  50. idmtools_platform_comps-0.0.2.dist-info/RECORD +62 -0
  51. idmtools_platform_comps-0.0.2.dist-info/entry_points.txt +9 -0
  52. idmtools_platform_comps-0.0.2.dist-info/licenses/LICENSE.TXT +3 -0
  53. {idmtools_platform_comps-0.0.0.dev0.dist-info → idmtools_platform_comps-0.0.2.dist-info}/top_level.txt +1 -0
  54. ssmt_image/Dockerfile +52 -0
  55. ssmt_image/Makefile +21 -0
  56. ssmt_image/__init__.py +6 -0
  57. ssmt_image/bootstrap.sh +30 -0
  58. ssmt_image/build_docker_image.py +161 -0
  59. ssmt_image/pip.conf +3 -0
  60. ssmt_image/push_docker_image.py +49 -0
  61. ssmt_image/requirements.txt +9 -0
  62. idmtools_platform_comps-0.0.0.dev0.dist-info/METADATA +0 -41
  63. idmtools_platform_comps-0.0.0.dev0.dist-info/RECORD +0 -5
  64. {idmtools_platform_comps-0.0.0.dev0.dist-info → idmtools_platform_comps-0.0.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,519 @@
1
+ """idmtools FileFilterWorkItem is a interface for SSMT command to act on files using filters in WorkItems.
2
+
3
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
4
+ """
5
+ import copy
6
+ import json
7
+ import os
8
+ from abc import ABC
9
+ from os import PathLike
10
+ from pathlib import PurePath
11
+ from uuid import UUID
12
+ import re
13
+ import inspect
14
+ from dataclasses import dataclass, field
15
+ from logging import getLogger, DEBUG
16
+ from COMPS.Data.CommissionableEntity import CommissionableEntity
17
+ from typing import List, Union, Callable, Dict
18
+ from idmtools import IdmConfigParser
19
+ from idmtools.assets import Asset, AssetCollection
20
+ from idmtools.assets.file_list import FileList
21
+ from idmtools.core.interfaces.irunnable_entity import IRunnableEntity
22
+ from idmtools.entities import CommandLine
23
+ from idmtools.entities.command_task import CommandTask
24
+ from idmtools.entities.experiment import Experiment
25
+ from idmtools.entities.iplatform import IPlatform
26
+ from idmtools.entities.iworkflow_item import IWorkflowItem
27
+ from idmtools.entities.simulation import Simulation
28
+ from idmtools.utils.info import get_help_version_url
29
+ from idmtools_platform_comps.comps_platform import COMPSPlatform
30
+ from idmtools_platform_comps.ssmt_work_items.comps_workitems import SSMTWorkItem
31
+ from idmtools.core.enums import ItemType, EntityStatus
32
+
33
+ EntityFilterFunc = Callable[[CommissionableEntity], bool]
34
+ FilterableSSMTItem = Union[Experiment, Simulation, IWorkflowItem]
35
+ FilenameFormatFunction = Callable[[str], str]
36
+ logger = getLogger(__name__)
37
+ user_logger = getLogger("user")
38
+
39
+ WI_PROPERTY_MAP = dict(
40
+ related_experiments=ItemType.EXPERIMENT,
41
+ related_simulations=ItemType.SIMULATION,
42
+ related_work_items=ItemType.WORKFLOW_ITEM,
43
+ related_asset_collections=ItemType.ASSETCOLLECTION
44
+ )
45
+
46
+ # Default list of ignored files
47
+ DEFAULT_EXCLUDES = ["StdErr.txt", "StdOut.txt", "WorkOrder.json", "*.log"]
48
+
49
+
50
+ class CrossEnvironmentFilterNotSupport(Exception):
51
+ """Defines cross environment error for when a user tried to filter across multiple comps environments."""
52
+ doc_link: str = "platforms/comps/errors.html#errors"
53
+
54
+
55
+ class AtLeastOneItemToWatch(Exception):
56
+ """Defines error for when there are not items being watched by FileFilterWorkItem."""
57
+ doc_link: str = "platforms/comps/errors.html#errors"
58
+
59
+
60
+ @dataclass(repr=False)
61
+ class FileFilterWorkItem(SSMTWorkItem, ABC):
62
+ """
63
+ Defines our filtering workitem base that is used by assetize outputs and download work items.
64
+ """
65
+ #: List of glob patterns. See https://docs.python.org/3.7/library/glob.html for details on the patterns
66
+ file_patterns: List[str] = field(default_factory=list)
67
+ # Exclude patterns.
68
+ exclude_patterns: List[str] = field(default_factory=lambda: copy.copy(DEFAULT_EXCLUDES))
69
+ #: Include Assets directories. This allows patterns to also include items from the assets directory
70
+ include_assets: bool = field(default=False)
71
+ #: Formatting pattern for directory names. Simulations tend to have similar outputs so the workitem puts those in directories using the simulation id by default as the directory name
72
+ simulation_prefix_format_str: str = field(default="{simulation.id}")
73
+ #: WorkFlowItem outputs will not have a folder prefix by default. If you are filtering multiple work items, you may want to set this to "{workflow_item.id}"
74
+ work_item_prefix_format_str: str = field(default=None)
75
+ #: Simulations outputs will not have a folder. Useful when you are filtering a single simulation
76
+ no_simulation_prefix: bool = field(default=False)
77
+ #: Enable verbose
78
+ verbose: bool = field(default=False)
79
+ #: Python Functions that will be ran before Filtering script. The function must be named
80
+ pre_run_functions: List[Callable] = field(default_factory=list)
81
+ #: Python Function to filter entities. This Function should receive a Comps CommissionableEntity. True means include item, false is don't
82
+ entity_filter_function: EntityFilterFunc = field(default=None)
83
+ #: Function to pass a custom function that is called on the name. This can be used to do advanced mapping or renaming of files
84
+ filename_format_function: FilenameFormatFunction = field(default=None)
85
+ #: Enables running jobs without creating executing. It instead produces a file list of what would be includes in the final filter
86
+ dry_run: bool = field(default=False)
87
+
88
+ _ssmt_script: str = field(default=None, repr=False)
89
+ # later change to load all functions in file_filter.py
90
+ _ssmt_depends: List[str] = field(default_factory=lambda: ["common.py", "file_filter.py"], repr=False)
91
+
92
+ def __post_init__(self, item_name: str, asset_collection_id: UUID, asset_files: FileList, user_files: FileList, command: str):
93
+ """
94
+ Initialize the FileFilterWorkItem.
95
+
96
+ Args:
97
+ item_name: ItemName(Workitem)
98
+ asset_collection_id: Asset collection to attach to workitem
99
+ asset_files: Asset collections files to add
100
+ user_files: User files to add
101
+ command: Command to run
102
+
103
+ Returns:
104
+ None
105
+
106
+ Raises:
107
+ ValueError - If the ssmt script is not defined.
108
+ """
109
+ if self._ssmt_script is None:
110
+ raise ValueError("When defining a FileFilterWorkItem, you need an _ssmt_script")
111
+ # Set command to nothing here for now. Eventually this will go away after 1.7.0
112
+ self.task = CommandTask(command=f'python3 Assets/{PurePath(self._ssmt_script).name}')
113
+ super().__post_init__(item_name, asset_collection_id, asset_files, user_files, "")
114
+
115
+ def create_command(self) -> str:
116
+ """
117
+ Builds our command line for the SSMT Job.
118
+
119
+ Returns:
120
+ Command string
121
+ """
122
+ command = f"python3 Assets/{PurePath(self._ssmt_script).name} "
123
+ if self.file_patterns:
124
+ command += '--file-pattern'
125
+ for pattern in self.file_patterns:
126
+ command += f' "{pattern}"'
127
+
128
+ if self.exclude_patterns:
129
+ command += ' --exclude-pattern'
130
+ for pattern in self.exclude_patterns:
131
+ command += f' "{pattern}"'
132
+
133
+ if self.no_simulation_prefix:
134
+ command += ' --no-simulation-prefix'
135
+ else:
136
+ command += f' --simulation-prefix-format-str "{self.simulation_prefix_format_str}"'
137
+
138
+ if self.work_item_prefix_format_str:
139
+ command += f' --work-item-prefix-format-str "{self.work_item_prefix_format_str}"'
140
+
141
+ if self.include_assets:
142
+ command += ' --assets'
143
+
144
+ for pre_run_func in self.pre_run_functions:
145
+ command += f' --pre-run-func {pre_run_func.__name__}'
146
+
147
+ if self.entity_filter_function:
148
+ command += f' --entity-filter-func {self.entity_filter_function.__name__}'
149
+
150
+ if self.filename_format_function:
151
+ command += f' --filename-format-func {self.filename_format_function.__name__}'
152
+
153
+ if self.verbose:
154
+ command += ' --verbose'
155
+
156
+ if self.dry_run:
157
+ command += ' --dry-run'
158
+
159
+ command = self._extra_command_args(command)
160
+
161
+ if logger.isEnabledFor(DEBUG):
162
+ logger.debug(f'Command: {command}')
163
+
164
+ return command
165
+
166
+ def _extra_command_args(self, command: str) -> str:
167
+ """Add extra command arguments."""
168
+ return command
169
+
170
+ def __pickle_pre_run(self):
171
+ """
172
+ Pickles the pre run functions.
173
+
174
+ Returns:
175
+ None
176
+ """
177
+ if self.pre_run_functions:
178
+ source = ""
179
+ for function in self.pre_run_functions:
180
+ new_source = self.__format_function_source(function)
181
+ source += "\n\n" + new_source
182
+ self.assets.add_or_replace_asset(Asset(filename='pre_run.py', content=source))
183
+
184
+ def __pickle_format_func(self):
185
+ """
186
+ Pickle Format filename Function.
187
+
188
+ Returns:
189
+ None
190
+ """
191
+ if self.filename_format_function:
192
+ new_source = self.__format_function_source(self.filename_format_function)
193
+ self.assets.add_or_replace_asset(Asset(filename='filename_format_func.py', content=new_source))
194
+
195
+ def __pickle_filter_func(self):
196
+ """
197
+ Pickle Filter Function.
198
+
199
+ Returns:
200
+ None
201
+ """
202
+ if self.entity_filter_function:
203
+ new_source = self.__format_function_source(self.entity_filter_function)
204
+ self.assets.add_or_replace_asset(Asset(filename='entity_filter_func.py', content=new_source))
205
+
206
+ @staticmethod
207
+ def __format_function_source(function: Callable) -> str:
208
+ """
209
+ Formats the function source. Functions could be indented.
210
+
211
+ Args:
212
+ function: Function to format
213
+
214
+ Returns:
215
+ Formatted function
216
+ """
217
+ source = inspect.getsource(function).splitlines()
218
+ space_base = 0
219
+ while source[0][space_base] == " ":
220
+ space_base += 1
221
+ replace_expr = re.compile("^[ ]{" + str(space_base) + "}")
222
+ new_source = []
223
+ for line in source:
224
+ new_source.append(replace_expr.sub("", line))
225
+ return "\n".join(new_source)
226
+
227
+ def clear_exclude_patterns(self):
228
+ """
229
+ Clear Exclude Patterns will remove all current rules.
230
+
231
+ Returns:
232
+ None
233
+ """
234
+ self.exclude_patterns = []
235
+
236
+ def pre_creation(self, platform: IPlatform) -> None:
237
+ """
238
+ Pre-Creation.
239
+
240
+ Args:
241
+ platform: Platform
242
+
243
+ Returns:
244
+ None
245
+ """
246
+ self._filter_workitem_pre_creation(platform)
247
+ if self.name is None or self.name == "idmtools workflow item":
248
+ self.name = self.__generate_name()
249
+
250
+ current_dir = PurePath(__file__).parent
251
+ # Add our ssm script
252
+ self.assets.add_or_replace_asset(self._ssmt_script)
253
+ utils_dir = current_dir.joinpath("ssmt_utils")
254
+ # Add dependencies
255
+ if self._ssmt_depends:
256
+ for file in self._ssmt_depends:
257
+ self.assets.add_or_replace_asset(utils_dir.joinpath(file))
258
+ self.__pickle_pre_run()
259
+ self.__pickle_format_func()
260
+ self.__pickle_filter_func()
261
+ self.task.command = CommandLine(self.create_command(), is_windows=False)
262
+ if IdmConfigParser.is_output_enabled():
263
+ user_logger.info("Creating Watcher")
264
+
265
+ super().pre_creation(platform)
266
+
267
+ def _filter_workitem_pre_creation(self, platform):
268
+ """
269
+ Filter the workitem before creation.
270
+
271
+ Args:
272
+ platform: Platform
273
+
274
+ Returns:
275
+ None
276
+
277
+ Raises:
278
+ AtLeastOneItemToWatch - If there are not items we are watching, we cannot run our workitem.
279
+ """
280
+ if self.total_items_watched() == 0:
281
+ raise AtLeastOneItemToWatch("You must specify at least one item to watch")
282
+ if len(self.file_patterns) == 0:
283
+ logger.info("No file pattern specified. Setting to default pattern '**' to filter for all outputs")
284
+ self.file_patterns.append("**")
285
+ self.__convert_ids_to_items(platform)
286
+ self.__ensure_all_dependencies_created_and_in_proper_env(platform)
287
+
288
+ def __generate_name(self) -> str:
289
+ """
290
+ Generate Automatic name for the WorkItem.
291
+
292
+ Returns:
293
+ Return generated name
294
+ """
295
+ total_items = 0
296
+ name = None
297
+ first_item_type = None
298
+ for prop, item_type in WI_PROPERTY_MAP.items():
299
+ item_type_count = len(getattr(self, prop))
300
+ total_items += len(getattr(self, prop))
301
+ # get first item as we iterate through
302
+ if name is None and item_type_count:
303
+ item = getattr(self, prop)
304
+ name = item[0].id if hasattr(item[0], 'id') else item[0]
305
+ first_item_type = item_type
306
+
307
+ if total_items > 1:
308
+ return "Filter outputs"
309
+
310
+ return f"Filter outputs for {str(first_item_type).replace('ItemType.', '').lower().capitalize()} {name}"
311
+
312
+ def __convert_ids_to_items(self, platform):
313
+ """
314
+ Convert our ids to items.
315
+
316
+ Args:
317
+ platform: Platform object
318
+
319
+ Returns:
320
+ None
321
+ """
322
+ for prop, item_type in WI_PROPERTY_MAP.items():
323
+ new_items = []
324
+ for item in getattr(self, prop):
325
+ if isinstance(item, (str, PathLike)):
326
+ # first test if the item is a file
327
+ if os.path.exists(item):
328
+ item = platform.get_item_from_id_file(item, item_type)
329
+ else:
330
+ if isinstance(item, PathLike):
331
+ user_logger.warning(f"Could not find a file with name {item}. Attempting to load as an ID")
332
+ item = platform.get_item(item, item_type, force=True)
333
+ new_items.append(item)
334
+ setattr(self, prop, new_items)
335
+
336
+ def __ensure_all_dependencies_created_and_in_proper_env(self, platform: COMPSPlatform):
337
+ """
338
+ Ensures all items we are watching.
339
+
340
+ Args:
341
+ platform:
342
+
343
+ Returns:
344
+ None
345
+
346
+ Raises:
347
+ CrossEnvironmentFilterNotSupport - If items are from multiple environemnts.
348
+ """
349
+ for work_prop, item_type in WI_PROPERTY_MAP.items():
350
+ items = getattr(self, work_prop)
351
+ for item in items:
352
+ if item.status is None:
353
+ if isinstance(item, IRunnableEntity):
354
+ item.run(platform=platform)
355
+ # this should only be sim in this branch
356
+ elif isinstance(item, Simulation):
357
+ item.parent.run(platform=platform)
358
+ elif isinstance(item, AssetCollection):
359
+ platform.create_items(item)
360
+ if item_type in [ItemType.SIMULATION, ItemType.WORKFLOW_ITEM, ItemType.EXPERIMENT]:
361
+ fail = False
362
+ po = item.get_platform_object(platform=platform)
363
+ if item_type in [ItemType.SIMULATION, ItemType.EXPERIMENT]:
364
+ if item_type == ItemType.SIMULATION and po.configuration is None or po.configuration.environment_name is None:
365
+ po = item.parent.get_platform_object(platform=platform)
366
+ if po.configuration is None:
367
+ user_logger.warning(f"Cannot determine environment of item of type {item_type} with id of {item.id}. Running filter against items in other environments will result in an error")
368
+ elif po.configuration.environment_name.lower() != platform.environment.lower():
369
+ fail = True
370
+ elif item_type == ItemType.WORKFLOW_ITEM and po.environment_name.lower() != platform.environment.lower():
371
+ fail = True
372
+ if fail:
373
+ raise CrossEnvironmentFilterNotSupport(f"You cannot filter files between environment. In this case, the {item_type.value} {item.id} is in {po.configuration.environment_name} but you are running your workitem in {platform.environment}")
374
+
375
+ def total_items_watched(self) -> int:
376
+ """
377
+ Returns the number of items watched.
378
+
379
+ Returns:
380
+ Total number of items watched
381
+ """
382
+ total = 0
383
+ for item_type in WI_PROPERTY_MAP.keys():
384
+ total += len(getattr(self, item_type))
385
+ return total
386
+
387
+ def run_after_by_id(self, item_id: str, item_type: ItemType, platform: COMPSPlatform = None):
388
+ """
389
+ Runs the workitem after an existing item finishes.
390
+
391
+ Args:
392
+ item_id: ItemId
393
+ item_type: ItemType
394
+ platform: Platform
395
+
396
+ Returns:
397
+ None
398
+
399
+ Raises:
400
+ ValueError - If item_type is not an experiment, simulation, or workflow item
401
+ """
402
+ if item_type not in [ItemType.EXPERIMENT, ItemType.SIMULATION, ItemType.WORKFLOW_ITEM]:
403
+ raise ValueError("Currently only Experiment, Simuation, and WorkFlowItems can be filtered")
404
+ p = super()._check_for_platform_from_context(platform)
405
+ extra = dict()
406
+ item = p.get_item(item_id=item_id, item_type=item_type, **extra)
407
+ if item:
408
+ self.from_items(item)
409
+ raise FileNotFoundError(f"Cannot find the item with {item_id} of type {item_type}")
410
+
411
+ def from_items(self, item: Union[FilterableSSMTItem, List[FilterableSSMTItem]]):
412
+ """
413
+ Add items to load assets from.
414
+
415
+ Args:
416
+ item: Item or list of items to watch.
417
+
418
+ Returns:
419
+ None
420
+
421
+ Raises:
422
+ ValueError - If any items specified are not an Experiment, Simulation or WorkItem
423
+
424
+ Notes:
425
+ We should add suite support in the future if possible. This should be done in client side by converting suite to list of experiments.
426
+ """
427
+ if not isinstance(item, list):
428
+ items_to_add = [item]
429
+ else:
430
+ items_to_add = item
431
+ for i in items_to_add:
432
+ if isinstance(i, Experiment):
433
+ self.related_experiments.append(i)
434
+ elif isinstance(i, Simulation):
435
+ self.related_simulations.append(i)
436
+ elif isinstance(i, IWorkflowItem):
437
+ self.related_work_items.append(i)
438
+ else:
439
+ raise ValueError("We can only filter the output of experiments, simulations, and workitems")
440
+
441
+ def wait(self, wait_on_done_progress: bool = True, timeout: int = None, refresh_interval=None, platform: 'COMPSPlatform' = None) -> Union[None]:
442
+ """
443
+ Waits on Filter Workitem to finish. This first waits on any dependent items to finish(Experiment/Simulation/WorkItems).
444
+
445
+ Args:
446
+ wait_on_done_progress: When set to true, a progress bar will be shown from the item
447
+ timeout: Timeout for waiting on item. If none, wait will be forever
448
+ refresh_interval: How often to refresh progress
449
+ platform: Platform
450
+
451
+ Returns:
452
+ AssetCollection created if item succeeds
453
+ """
454
+ # wait on related items before we wait on our item
455
+ p = super()._check_for_platform_from_context(platform)
456
+ opts = dict(wait_on_done_progress=wait_on_done_progress, timeout=timeout, refresh_interval=refresh_interval, platform=p)
457
+ self._wait_on_children(**opts)
458
+
459
+ super().wait(**opts)
460
+
461
+ def _wait_on_children(self, **opts):
462
+ """
463
+ Wait on children implementation.
464
+
465
+ Loops through the relations and ensure all are done before waiting on ourselve
466
+ Args:
467
+ **opts:
468
+
469
+ Returns:
470
+ None
471
+ """
472
+ if logger.isEnabledFor(DEBUG):
473
+ logger.debug("Wait on items being watched to finish running")
474
+ for item_type in WI_PROPERTY_MAP.keys():
475
+ items = getattr(self, item_type)
476
+ for item in items:
477
+ # The only two done states in idmtools are SUCCEEDED and FAILED
478
+ if item.status not in [EntityStatus.SUCCEEDED, EntityStatus.FAILED]:
479
+ # We only can wait on Experiments and Workitems. For simulations, we use the parent
480
+ if isinstance(item, IRunnableEntity):
481
+ item.wait(**opts)
482
+ # user simulation's parent to wait
483
+ elif isinstance(item, Simulation):
484
+ if item.parent is None:
485
+ if not item.parent_id:
486
+ raise ValueError(f"Cannot determine simulation {item.id}'s parent and item still in progress. Please wait on it to complete before {self.__class__.name}")
487
+ else:
488
+ item.parent = Experiment.from_id(item.parent_id)
489
+ item.parent.wait(**opts)
490
+ if logger.isEnabledFor(DEBUG):
491
+ logger.debug(f"Done waiting on items watching. Now waiting on {self.__class__.name}: {self.id}")
492
+ super().wait(**opts)
493
+
494
+ def fetch_error(self, print_error: bool = True) -> Union[Dict]:
495
+ """
496
+ Fetches the error from a WorkItem.
497
+
498
+ Args:
499
+ print_error: Should error be printed. If false, error will be returned
500
+
501
+ Returns:
502
+ Error info
503
+ """
504
+ if self.status != EntityStatus.FAILED:
505
+ raise ValueError("You cannot fetch error if the items is not in a failed state")
506
+
507
+ try:
508
+ file = self.platform.get_files(self, ['error_reason.json'])
509
+ file = file['error_reason.json'].decode('utf-8')
510
+ file = json.loads(file)
511
+ if print_error:
512
+ user_logger.error(f'Error from server: {file["args"][0]}')
513
+ if 'doc_link' in file:
514
+ user_logger.error(get_help_version_url(file['doc_link']))
515
+ else:
516
+ user_logger.error(user_logger.error('\n'.join(file['tb'])))
517
+ return file
518
+ except Exception as e:
519
+ logger.exception(e)