idmtools-platform-general 0.0.0.dev0__py3-none-any.whl → 0.0.3__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 (39) hide show
  1. idmtools_platform_file/__init__.py +18 -0
  2. idmtools_platform_file/assets/__init__.py +77 -0
  3. idmtools_platform_file/assets/_run.sh.jinja2 +47 -0
  4. idmtools_platform_file/assets/batch.sh.jinja2 +24 -0
  5. idmtools_platform_file/assets/run_simulation.sh +8 -0
  6. idmtools_platform_file/cli/__init__.py +5 -0
  7. idmtools_platform_file/cli/file.py +185 -0
  8. idmtools_platform_file/file_operations/__init__.py +4 -0
  9. idmtools_platform_file/file_operations/file_operations.py +298 -0
  10. idmtools_platform_file/file_operations/operations_interface.py +74 -0
  11. idmtools_platform_file/file_platform.py +288 -0
  12. idmtools_platform_file/platform_operations/__init__.py +5 -0
  13. idmtools_platform_file/platform_operations/asset_collection_operations.py +172 -0
  14. idmtools_platform_file/platform_operations/experiment_operations.py +314 -0
  15. idmtools_platform_file/platform_operations/json_metadata_operations.py +320 -0
  16. idmtools_platform_file/platform_operations/simulation_operations.py +212 -0
  17. idmtools_platform_file/platform_operations/suite_operations.py +243 -0
  18. idmtools_platform_file/platform_operations/utils.py +461 -0
  19. idmtools_platform_file/plugin_info.py +82 -0
  20. idmtools_platform_file/tools/__init__.py +4 -0
  21. idmtools_platform_file/tools/job_history.py +334 -0
  22. idmtools_platform_file/tools/status_report/__init__.py +4 -0
  23. idmtools_platform_file/tools/status_report/status_report.py +222 -0
  24. idmtools_platform_file/tools/status_report/utils.py +159 -0
  25. idmtools_platform_general-0.0.3.dist-info/METADATA +81 -0
  26. idmtools_platform_general-0.0.3.dist-info/RECORD +35 -0
  27. idmtools_platform_general-0.0.3.dist-info/entry_points.txt +6 -0
  28. idmtools_platform_general-0.0.3.dist-info/licenses/LICENSE.TXT +3 -0
  29. idmtools_platform_general-0.0.3.dist-info/top_level.txt +2 -0
  30. idmtools_platform_process/__init__.py +17 -0
  31. idmtools_platform_process/platform_operations/__init__.py +5 -0
  32. idmtools_platform_process/platform_operations/experiment_operations.py +53 -0
  33. idmtools_platform_process/plugin_info.py +80 -0
  34. idmtools_platform_process/process_platform.py +52 -0
  35. idmtools_platform_general/__init__.py +0 -8
  36. idmtools_platform_general-0.0.0.dev0.dist-info/METADATA +0 -41
  37. idmtools_platform_general-0.0.0.dev0.dist-info/RECORD +0 -5
  38. idmtools_platform_general-0.0.0.dev0.dist-info/top_level.txt +0 -1
  39. {idmtools_platform_general-0.0.0.dev0.dist-info → idmtools_platform_general-0.0.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,18 @@
1
+ """
2
+ idmtools file platform.
3
+
4
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
5
+ """
6
+ # flake8: noqa F821
7
+ try:
8
+ from importlib.metadata import version, PackageNotFoundError
9
+ except ImportError:
10
+ # Python < 3.8
11
+ from importlib_metadata import version, PackageNotFoundError
12
+
13
+ try:
14
+ __version__ = version("idmtools-platform-general") # Use your actual package name
15
+ except PackageNotFoundError:
16
+ # Package not installed, use fallback
17
+ __version__ = "0.0.0+unknown"
18
+
@@ -0,0 +1,77 @@
1
+ """
2
+ Utility functions to generate batch scripts.
3
+
4
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
5
+ """
6
+ from pathlib import Path
7
+ from jinja2 import Template
8
+ from typing import TYPE_CHECKING, Optional
9
+ from idmtools.entities.experiment import Experiment
10
+ from idmtools.entities.simulation import Simulation
11
+
12
+ if TYPE_CHECKING:
13
+ from idmtools_platform_file.file_platform import FilePlatform
14
+
15
+ DEFAULT_TEMPLATE_FILE = Path(__file__).parent.joinpath("batch.sh.jinja2")
16
+ DEFAULT_SIMULATION_TEMPLATE = Path(__file__).parent.parent.joinpath("assets/_run.sh.jinja2")
17
+
18
+
19
+ def generate_script(platform: 'FilePlatform', experiment: Experiment, max_job: int = None, run_sequence: bool = None,
20
+ **kwargs) -> None:
21
+ """
22
+ Generate batch file batch.sh.
23
+ Args:
24
+ platform: File Platform
25
+ experiment: idmtools Experiment
26
+ max_job: int
27
+ run_sequence: bool
28
+ kwargs: keyword arguments used to expand functionality
29
+ Returns:
30
+ None
31
+ """
32
+ output_target = platform.get_directory(experiment).joinpath("batch.sh")
33
+ with open(output_target, "w") as tout:
34
+ with open(DEFAULT_TEMPLATE_FILE) as tin:
35
+ t = Template(tin.read())
36
+ tvars = dict(
37
+ platform=platform,
38
+ max_job=max_job if max_job is not None else platform.max_job,
39
+ run_sequence=run_sequence if run_sequence is not None else platform.run_sequence
40
+ )
41
+ if platform.modules:
42
+ tvars['modules'] = platform.modules
43
+ if platform.extra_packages:
44
+ tvars['packages'] = platform.extra_packages
45
+ tout.write(t.render(tvars))
46
+
47
+ # Make executable
48
+ platform.update_script_mode(output_target)
49
+
50
+
51
+ def generate_simulation_script(platform: 'FilePlatform', simulation: Simulation, retries: Optional[int] = None,
52
+ **kwargs) -> None:
53
+ """
54
+ Generate batch file _run.sh.
55
+ Args:
56
+ platform: File Platform
57
+ simulation: idmtools Simulation
58
+ retries: int
59
+ extra_packages: List of extra packages to install
60
+ kwargs: keyword arguments used to expand functionality
61
+ Returns:
62
+ None
63
+ """
64
+ sim_script = platform.get_directory(simulation).joinpath("_run.sh")
65
+ with open(sim_script, "w") as tout:
66
+ with open(DEFAULT_SIMULATION_TEMPLATE) as tin:
67
+ t = Template(tin.read())
68
+ tvars = dict(
69
+ platform=platform,
70
+ simulation=simulation,
71
+ retries=retries if retries else platform.retries,
72
+ ntasks=platform.ntasks
73
+ )
74
+ tout.write(t.render(tvars))
75
+
76
+ # Make executable
77
+ platform.update_script_mode(sim_script)
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+
3
+ # define the handler function
4
+ term_handler()
5
+ {
6
+ # do whatever cleanup you want here
7
+ echo "-1" > job_status.txt
8
+ exit -1
9
+ }
10
+
11
+ # Register cleanup function to handle SIGINT and SIGTERM signals
12
+ trap 'term_handler' SIGINT SIGTERM
13
+
14
+ n=0
15
+ {% set mpi_command = "mpirun -n " + ntasks|string if ntasks > 1 else "" %}
16
+
17
+ until [ "$n" -ge {{retries}} ]
18
+ do
19
+ echo "100" > job_status.txt
20
+ {% if simulation.task.sif_path is defined and simulation.task.sif_path %}
21
+ {% if simulation.task.command.cmd.startswith('singularity') %}
22
+ {{ mpi_command }} {{simulation.task.command.cmd}} &
23
+ {% else %}
24
+ singularity exec {{simulation.task.sif_path}} {{ mpi_command }} {{simulation.task.command.cmd}} &
25
+ {% endif %}
26
+ {% else %}
27
+ exec -a "SIMULATION:{{simulation.id}}" {{ mpi_command }} {{simulation.task.command.cmd}} &
28
+ {% endif %}
29
+
30
+ child_pid=$!
31
+ echo "Running simulation with PID: $child_pid"
32
+ # Wait for the child process to complete
33
+ wait $child_pid
34
+
35
+ RESULT=$?
36
+ if [ $RESULT -eq 0 ]; then
37
+ echo "0" > job_status.txt
38
+ exit 0
39
+ elif [ $RESULT -eq 255 ] || [ $RESULT -eq -1 ]; then # Normalize -1 or 255 to 1 to avoid process abort
40
+ echo "-1" > job_status.txt
41
+ echo "_run.sh exiting with code: $RESULT" >> exit_code.log
42
+ exit 1
43
+ fi
44
+ n=$((n+1))
45
+ done
46
+ echo "-1" > job_status.txt
47
+ exit $RESULT
@@ -0,0 +1,24 @@
1
+ #!/bin/bash
2
+
3
+ chmod +x run_simulation.sh
4
+
5
+ {% if packages is defined and packages is not none and packages|length > 0 %}
6
+ echo "Installing custom packages"
7
+
8
+ {% for package in packages %}
9
+ echo "Installing {{ package }}" 1>> stdout.txt 2>> stderr.txt
10
+ pip install {{ package }} --extra-index-url=https://packages.idmod.org/api/pypi/pypi-production/simple --upgrade 1>> stdout.txt 2>> stderr.txt
11
+ {% endfor %}
12
+ {% endif %}
13
+
14
+ {% if modules is defined and modules is not none and modules|length > 0 %}
15
+ {% for m in modules %}
16
+ module load {{m}}
17
+ {% endfor %}
18
+ {% endif %}
19
+
20
+ {% if run_sequence is defined and run_sequence %}
21
+ find $(pwd) -maxdepth 2 -name "_run.sh" -print0 | xargs -0 -I% dirname % | xargs -d "\n" -I% bash -c 'cd $(pwd) && $(pwd)/run_simulation.sh % 1>> stdout.txt 2>> stderr.txt'
22
+ {% else %}
23
+ find $(pwd) -maxdepth 2 -name "_run.sh" -print0 | xargs -0 -I% dirname % | xargs -d "\n" -P {{ max_job }} -I% bash -c 'cd $(pwd) && $(pwd)/run_simulation.sh % 1>> stdout.txt 2>> stderr.txt'
24
+ {% endif %}
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+
3
+ JOB_DIRECTORY=$1
4
+ echo "enter directory: '$JOB_DIRECTORY'"
5
+ cd $JOB_DIRECTORY
6
+ sed -i 's/\r//g' _run.sh
7
+ chmod +x _run.sh
8
+ bash _run.sh 1> stdout.txt 2> stderr.txt
@@ -0,0 +1,5 @@
1
+ """
2
+ idmtools file cli module.
3
+
4
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
5
+ """
@@ -0,0 +1,185 @@
1
+ """
2
+ idmtools FilePlatform CLI commands.
3
+
4
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
5
+ """
6
+ import json
7
+ import click
8
+ from idmtools.core import ItemType
9
+ from idmtools.core.platform_factory import Platform
10
+ from idmtools_platform_file.tools.status_report.status_report import generate_status_report
11
+ from idmtools_platform_file.tools.status_report.utils import get_latest_experiment, check_status, clear_history
12
+ from logging import getLogger
13
+
14
+ user_logger = getLogger('user')
15
+
16
+
17
+ @click.group(short_help="File platform related commands.")
18
+ @click.argument('job-directory')
19
+ @click.pass_context
20
+ def file(ctx: click.Context, job_directory):
21
+ """
22
+ Commands related to managing the File Platform.
23
+
24
+ job_directory: FilePlatform Working Directory
25
+ """
26
+ ctx.obj = dict(job_directory=job_directory)
27
+
28
+
29
+ @file.command(help="Get simulation's report")
30
+ @click.option('--suite-id', default=None, help="Idmtools Suite id")
31
+ @click.option('--exp-id', default=None, help="Idmtools Experiment id")
32
+ @click.option('--status-filter', type=click.Choice(['0', '-1', '100']), multiple=True, help="list of status")
33
+ @click.option('--sim-filter', multiple=True, help="list of simulations")
34
+ @click.option('--verbose/--no-verbose', default=True, help="Enable verbose output in results")
35
+ @click.option('--display/--no-display', default=True, help="Display with working directory or not")
36
+ @click.option('--display-count', default=20, help="Display Count")
37
+ @click.pass_context
38
+ def status_report(ctx: click.Context, suite_id, exp_id, status_filter, sim_filter, verbose, display,
39
+ display_count):
40
+ """
41
+ Build status report.
42
+ Args:
43
+ ctx: click.Context
44
+ suite_id: suite id
45
+ exp_id: experiment id
46
+ status_filter: status filter
47
+ sim_filter: simulation filter
48
+ verbose: bool True/False
49
+ display: bool True/False
50
+ display_count: how many to display
51
+ Returns:
52
+ None
53
+ """
54
+ job_dir = ctx.obj['job_directory']
55
+ platform = Platform('FILE', job_directory=job_dir)
56
+
57
+ if suite_id is not None:
58
+ scope = (suite_id, ItemType.SUITE)
59
+ elif exp_id is not None:
60
+ scope = (exp_id, ItemType.EXPERIMENT)
61
+ else:
62
+ scope = None
63
+
64
+ generate_status_report(platform=platform, scope=scope,
65
+ status_filter=status_filter if len(status_filter) > 0 else None,
66
+ sim_filter=sim_filter if len(sim_filter) > 0 else None,
67
+ verbose=verbose, display=display, display_count=display_count)
68
+
69
+
70
+ @file.command(help="Get the latest experiment info")
71
+ @click.pass_context
72
+ def get_latest(ctx: click.Context):
73
+ """
74
+ Get the latest experiment directory.
75
+ Args:
76
+ ctx: click.Context
77
+ Returns:
78
+ None
79
+ """
80
+ job_dir = ctx.obj['job_directory']
81
+ platform = Platform('FILE', job_directory=job_dir)
82
+
83
+ result = get_latest_experiment(platform)
84
+ user_logger.info(json.dumps(result, indent=3))
85
+
86
+
87
+ @file.command(help="Get Suite/Experiment/Simulation directory")
88
+ @click.option('--sim-id', default=None, help="Idmtools Simulation id")
89
+ @click.option('--exp-id', default=None, help="Idmtools Experiment id")
90
+ @click.option('--suite-id', default=None, help="Idmtools Suite id")
91
+ @click.pass_context
92
+ def get_path(ctx: click.Context, sim_id, exp_id, suite_id):
93
+ """
94
+ Get entity directory.
95
+ Args:
96
+ ctx: click.Context
97
+ sim_id: simulation id
98
+ exp_id: experiment id
99
+ suite_id: suite id
100
+ Returns:
101
+ None
102
+ """
103
+ job_dir = ctx.obj['job_directory']
104
+ platform = Platform('FILE', job_directory=job_dir)
105
+
106
+ if sim_id is not None:
107
+ item_dir = platform.get_directory_by_id(sim_id, ItemType.SIMULATION)
108
+ elif exp_id is not None:
109
+ item_dir = platform.get_directory_by_id(exp_id, ItemType.EXPERIMENT)
110
+ elif suite_id is not None:
111
+ item_dir = platform.get_directory_by_id(suite_id, ItemType.SUITE)
112
+ else:
113
+ raise Exception('Must provide at least one: suite-id, exp-id or sim-id!')
114
+
115
+ user_logger.info(item_dir)
116
+
117
+
118
+ @file.command(help="Get status of Experiment/Simulation")
119
+ @click.option('--sim-id', default=None, help="Idmtools Simulation id")
120
+ @click.option('--exp-id', default=None, help="Idmtools Experiment id")
121
+ @click.pass_context
122
+ def get_status(ctx: click.Context, sim_id, exp_id):
123
+ """
124
+ Retrieve status.
125
+ Args:
126
+ ctx: click.Context
127
+ sim_id: simulation id
128
+ exp_id: experiment id
129
+ Returns:
130
+ None
131
+ """
132
+ job_dir = ctx.obj['job_directory']
133
+ platform = Platform('FILE', job_directory=job_dir)
134
+
135
+ if sim_id is not None:
136
+ status = platform.get_simulation_status(sim_id)
137
+ elif exp_id is not None:
138
+ exp = platform.get_item(exp_id, ItemType.EXPERIMENT)
139
+ status = exp.status
140
+ else:
141
+ raise Exception('Must provide at least one: exp-id or sim-id!')
142
+
143
+ user_logger.info(status.name if status else None)
144
+
145
+
146
+ @file.command(help="Get simulation's status")
147
+ @click.option('--exp-id', default=None, help="Idmtools Experiment id")
148
+ @click.option('--display/--no-display', default=False, help="Display with working directory or not")
149
+ @click.pass_context
150
+ def status(ctx: click.Context, exp_id, display):
151
+ """
152
+ Get job status.
153
+ Args:
154
+ ctx: click.Context
155
+ exp_id: experiment id
156
+ display: bool True/False
157
+ Returns:
158
+ None
159
+ """
160
+ job_dir = ctx.obj['job_directory']
161
+ platform = Platform('FILE', job_directory=job_dir)
162
+
163
+ check_status(platform=platform, exp_id=exp_id, display=display)
164
+
165
+
166
+ @file.command(help="Clear generated files/folders")
167
+ @click.option('--exp-id', default=None, help="Idmtools Experiment id")
168
+ @click.option('--sim-id', multiple=True, help="Idmtools Simulation id")
169
+ @click.option('--remove', multiple=True, help="list of files/folders to be removed from simulation")
170
+ @click.pass_context
171
+ def clear_files(ctx: click.Context, exp_id, sim_id, remove):
172
+ """
173
+ Clear running history.
174
+ Args:
175
+ ctx: click.Context
176
+ exp_id: experiment id
177
+ sim_id: simulation id
178
+ remove: list of files/folders
179
+ Returns:
180
+ None
181
+ """
182
+ job_dir = ctx.obj['job_directory']
183
+ platform = Platform('FILE', job_directory=job_dir)
184
+
185
+ clear_history(platform=platform, exp_id=exp_id, sim_id=sim_id, remove_list=remove)
@@ -0,0 +1,4 @@
1
+ """idmtools comps utils.
2
+
3
+ Copyright 2025, Gates Foundation. All rights reserved.
4
+ """
@@ -0,0 +1,298 @@
1
+ """
2
+ Here we implement the operations_interface.
3
+
4
+ Copyright 2025, Gates Foundation. All rights reserved.
5
+ """
6
+ import os
7
+ import shlex
8
+ import shutil
9
+ from dataclasses import dataclass
10
+ from logging import getLogger
11
+ from pathlib import Path
12
+ from typing import Union
13
+ from idmtools.core import ItemType, EntityStatus
14
+ from idmtools.entities import Suite
15
+ from idmtools.entities.experiment import Experiment
16
+ from idmtools.entities.simulation import Simulation
17
+ from idmtools_platform_file.assets import generate_script, generate_simulation_script
18
+ from idmtools_platform_file.file_operations.operations_interface import IOperations
19
+ from idmtools_platform_file.platform_operations.utils import FILE_MAPS, validate_file_path_length, \
20
+ clean_item_name, validate_folder_files_path_length, FileExperiment, FileSimulation, FileSuite
21
+ from idmtools.utils.decorators import check_symlink_capabilities, cache_directory
22
+
23
+ logger = getLogger(__name__)
24
+ user_logger = getLogger('user')
25
+
26
+
27
+ @dataclass
28
+ class FileOperations(IOperations):
29
+ """
30
+ Implement operations_interface.
31
+ """
32
+
33
+ def entity_display_name(self, item: Union[Suite, Experiment, Simulation]) -> str:
34
+ """
35
+ Get display name for entity.
36
+ Args:
37
+ item: Suite, Experiment or Simulation
38
+ Returns:
39
+ str
40
+ """
41
+ use_name = getattr(self.platform, "name_directory", False)
42
+ use_sim_name = getattr(self.platform, "sim_name_directory", True)
43
+
44
+ # Determine if we should include name
45
+ if isinstance(item, Simulation) and not use_sim_name:
46
+ use_name = False
47
+
48
+ if use_name and getattr(item, "name", None):
49
+ safe_name = clean_item_name(item.name, maxlen=self.platform.maxlen)
50
+ return f"{safe_name}_{item.id}"
51
+ else:
52
+ return item.id
53
+
54
+ @cache_directory
55
+ def get_directory(self, item: Union[Suite, Experiment, Simulation]) -> Path:
56
+ """
57
+ Get item's path.
58
+ Args:
59
+ item: Suite, Experiment, Simulation
60
+ Returns:
61
+ item file directory
62
+ """
63
+ job_dir = Path(self.platform.job_directory)
64
+ if isinstance(item, (FileSimulation, FileExperiment, FileSuite)):
65
+ item_dir = item.get_directory()
66
+ elif isinstance(item, Suite):
67
+ return job_dir / f"s_{self.entity_display_name(item)}"
68
+ elif isinstance(item, Experiment):
69
+ parent = item.parent
70
+ if parent: # Case 1: Parent suite object is available, build job_dir/suite/experiment path
71
+ suite_dir = job_dir / f"s_{self.entity_display_name(parent)}"
72
+ return suite_dir / f"e_{self.entity_display_name(item)}"
73
+ else: # Case 2: No parent or suite_id — build job_dir/experiment path
74
+ return job_dir / f"e_{self.entity_display_name(item)}"
75
+ elif isinstance(item, Simulation):
76
+ exp = item.parent
77
+ if exp is None:
78
+ raise RuntimeError("Simulation missing parent!")
79
+ exp_dir = self.get_directory(exp)
80
+ return exp_dir / self.entity_display_name(item)
81
+ else:
82
+ raise RuntimeError(f"Get directory is not supported for {type(item)} object on FilePlatform")
83
+
84
+ return item_dir
85
+
86
+ def get_directory_by_id(self, item_id: str, item_type: ItemType) -> Path:
87
+ """
88
+ Get item's path by id.
89
+ Args:
90
+ item_id: entity id (Suite, Experiment, Simulation)
91
+ item_type: the type of items (Suite, Experiment, Simulation)
92
+ Returns:
93
+ item file directory
94
+ """
95
+ metas = self.platform._metas.filter(item_type=item_type, property_filter={'id': str(item_id)})
96
+ if len(metas) > 0:
97
+ return Path(metas[0]['dir'])
98
+ else:
99
+ raise RuntimeError(f"Not found path for item_id: {item_id} with type: {item_type}.")
100
+
101
+ def mk_directory(self, item: Union[Suite, Experiment, Simulation] = None, dest: Union[Path, str] = None,
102
+ exist_ok: bool = True) -> None:
103
+ """
104
+ Make a new directory.
105
+ Args:
106
+ item: Suite/Experiment/Simulation
107
+ dest: the folder path
108
+ exist_ok: True/False
109
+ Returns:
110
+ None
111
+ """
112
+ if dest is not None:
113
+ target = Path(dest)
114
+ elif isinstance(item, (Suite, Experiment, Simulation)):
115
+ target = self.get_directory(item)
116
+ else:
117
+ raise RuntimeError('Only support Suite/Experiment/Simulation or not None dest.')
118
+
119
+ # Validate target path length
120
+ validate_file_path_length(self.platform.job_directory)
121
+ target.mkdir(parents=True, exist_ok=exist_ok)
122
+
123
+ @check_symlink_capabilities
124
+ def link_file(self, target: Union[Path, str], link: Union[Path, str]) -> None:
125
+ """
126
+ Link files.
127
+ Args:
128
+ target: the source file path
129
+ link: the file path
130
+ Returns:
131
+ None
132
+ """
133
+ target = Path(target).absolute()
134
+ link = Path(link).absolute()
135
+ if self.platform.sym_link:
136
+ # Ensure the source folder exists
137
+ if not target.exists():
138
+ raise FileNotFoundError(f"Source folder does not exist: {target}")
139
+
140
+ # Compute the relative path from the destination to the source
141
+ relative_source = os.path.relpath(target, link.parent)
142
+
143
+ # Remove existing symbolic link or file at destination if it exists
144
+ if link.exists() or link.is_symlink():
145
+ link.unlink()
146
+
147
+ # Create the symbolic link
148
+ try:
149
+ link.symlink_to(relative_source, target_is_directory=False)
150
+ except OSError as e:
151
+ user_logger.error(f"\n Failed to create symbolic link: {e}")
152
+ if self.platform.system() == 'Windows':
153
+ user_logger.warning("\n/!\\ WARNING: Please follow the instructions to enable Developer Mode for Windows: ")
154
+ user_logger.warning("https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development. \n")
155
+ exit(-1)
156
+ else:
157
+ shutil.copyfile(target, link)
158
+
159
+ @check_symlink_capabilities
160
+ def link_dir(self, target: Union[Path, str], link: Union[Path, str]) -> None:
161
+ """
162
+ Link directory/files.
163
+ Args:
164
+ target: the source folder path
165
+ link: the folder path
166
+ Returns:
167
+ None
168
+ """
169
+ target = Path(target).absolute()
170
+ link = Path(link).absolute()
171
+
172
+ # Validate file path length
173
+ validate_folder_files_path_length(target, link)
174
+
175
+ if self.platform.sym_link:
176
+ # Ensure the source folder exists
177
+ if not target.exists():
178
+ raise FileNotFoundError(f"Source folder does not exist: {target}")
179
+
180
+ # Compute the relative path from the destination to the source
181
+ relative_source = os.path.relpath(target, link.parent)
182
+
183
+ # Remove existing symbolic link or folder at destination if it exists
184
+ if link.exists() or link.is_symlink():
185
+ if link.is_symlink():
186
+ link.unlink()
187
+ else:
188
+ shutil.rmtree(link)
189
+
190
+ # Create the symbolic link
191
+ try:
192
+ link.symlink_to(relative_source, target_is_directory=True)
193
+ except OSError as e:
194
+ user_logger.error(f"\n Failed to create symbolic link: {e}")
195
+ if self.platform.system() == 'Windows':
196
+ user_logger.warning("\n/!\\ WARNING: Please follow the instructions to enable Developer Mode for Windows: ")
197
+ user_logger.warning("https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development. \n")
198
+ exit(-1)
199
+ else:
200
+ shutil.copytree(target, link, dirs_exist_ok=True)
201
+
202
+ @staticmethod
203
+ def update_script_mode(script_path: Union[Path, str], mode: int = 0o777) -> None:
204
+ """
205
+ Change file mode.
206
+ Args:
207
+ script_path: script path
208
+ mode: permission mode
209
+ Returns:
210
+ None
211
+ """
212
+ script_path = Path(script_path)
213
+ script_path.chmod(mode)
214
+
215
+ def make_command_executable(self, simulation: Simulation) -> None:
216
+ """
217
+ Make simulation command executable.
218
+ Args:
219
+ simulation: idmtools Simulation
220
+ Returns:
221
+ None
222
+ """
223
+ exe = simulation.task.command.executable
224
+ if exe == 'singularity':
225
+ # split the command
226
+ cmd = shlex.split(simulation.task.command.cmd.replace("\\", "/"))
227
+ # get real executable
228
+ exe = cmd[3]
229
+
230
+ sim_dir = self.get_directory(simulation)
231
+ exe_path = sim_dir.joinpath(exe)
232
+
233
+ # see if it is a file
234
+ if exe_path.exists():
235
+ exe = exe_path
236
+ elif shutil.which(exe) is not None:
237
+ exe = Path(shutil.which(exe))
238
+ else:
239
+ logger.debug(f"Failed to find executable: {exe}")
240
+ exe = None
241
+ try:
242
+ if exe and not os.access(exe, os.X_OK):
243
+ self.update_script_mode(exe)
244
+ except:
245
+ logger.debug(f"Failed to change file mode for executable: {exe}")
246
+
247
+ def get_simulation_status(self, sim_id: str, **kwargs) -> EntityStatus:
248
+ """
249
+ Retrieve simulation status.
250
+ Args:
251
+ sim_id: Simulation ID
252
+ kwargs: keyword arguments used to expand functionality
253
+ Returns:
254
+ EntityStatus
255
+ """
256
+ sim_dir = self.get_directory_by_id(sim_id, ItemType.SIMULATION)
257
+
258
+ # Check process status
259
+ job_status_path = sim_dir.joinpath('job_status.txt')
260
+ if job_status_path.exists():
261
+ status = open(job_status_path).read().strip()
262
+ if status in ['100', '0', '-1']:
263
+ status = FILE_MAPS[status]
264
+ else:
265
+ status = FILE_MAPS['100'] # To be safe
266
+ else:
267
+ status = FILE_MAPS['None']
268
+
269
+ return status
270
+
271
+ def create_file(self, file_path: str, content: str) -> None:
272
+ """
273
+ Create a file with given content and file path.
274
+
275
+ Args:
276
+ file_path: the full path of the file to be created
277
+ content: file content
278
+ Returns:
279
+ Nothing
280
+ """
281
+ with open(file_path, 'w') as f:
282
+ f.write(content)
283
+
284
+ def create_batch_file(self, item: Union[Experiment, Simulation], **kwargs) -> None:
285
+ """
286
+ Create batch file.
287
+ Args:
288
+ item: the item to build batch file for
289
+ kwargs: keyword arguments used to expand functionality.
290
+ Returns:
291
+ None
292
+ """
293
+ if isinstance(item, Experiment):
294
+ generate_script(self.platform, item, **kwargs)
295
+ elif isinstance(item, Simulation):
296
+ generate_simulation_script(self.platform, item, **kwargs)
297
+ else:
298
+ raise NotImplementedError(f"{item.__class__.__name__} is not supported for batch creation.")