idmtools-platform-general 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.
- idmtools_platform_file/__init__.py +18 -0
- idmtools_platform_file/assets/__init__.py +77 -0
- idmtools_platform_file/assets/_run.sh.jinja2 +47 -0
- idmtools_platform_file/assets/batch.sh.jinja2 +24 -0
- idmtools_platform_file/assets/run_simulation.sh +8 -0
- idmtools_platform_file/cli/__init__.py +5 -0
- idmtools_platform_file/cli/file.py +185 -0
- idmtools_platform_file/file_operations/__init__.py +4 -0
- idmtools_platform_file/file_operations/file_operations.py +298 -0
- idmtools_platform_file/file_operations/operations_interface.py +74 -0
- idmtools_platform_file/file_platform.py +288 -0
- idmtools_platform_file/platform_operations/__init__.py +5 -0
- idmtools_platform_file/platform_operations/asset_collection_operations.py +172 -0
- idmtools_platform_file/platform_operations/experiment_operations.py +314 -0
- idmtools_platform_file/platform_operations/json_metadata_operations.py +320 -0
- idmtools_platform_file/platform_operations/simulation_operations.py +212 -0
- idmtools_platform_file/platform_operations/suite_operations.py +243 -0
- idmtools_platform_file/platform_operations/utils.py +461 -0
- idmtools_platform_file/plugin_info.py +82 -0
- idmtools_platform_file/tools/__init__.py +4 -0
- idmtools_platform_file/tools/job_history.py +334 -0
- idmtools_platform_file/tools/status_report/__init__.py +4 -0
- idmtools_platform_file/tools/status_report/status_report.py +222 -0
- idmtools_platform_file/tools/status_report/utils.py +159 -0
- idmtools_platform_general-0.0.2.dist-info/METADATA +81 -0
- idmtools_platform_general-0.0.2.dist-info/RECORD +36 -0
- idmtools_platform_general-0.0.2.dist-info/entry_points.txt +6 -0
- idmtools_platform_general-0.0.2.dist-info/licenses/LICENSE.TXT +3 -0
- idmtools_platform_general-0.0.2.dist-info/top_level.txt +3 -0
- idmtools_platform_process/__init__.py +17 -0
- idmtools_platform_process/platform_operations/__init__.py +5 -0
- idmtools_platform_process/platform_operations/experiment_operations.py +53 -0
- idmtools_platform_process/plugin_info.py +80 -0
- idmtools_platform_process/process_platform.py +52 -0
- tests/input/hello.sh +2 -0
- idmtools_platform_general/__init__.py +0 -8
- idmtools_platform_general-0.0.0.dev0.dist-info/METADATA +0 -41
- idmtools_platform_general-0.0.0.dev0.dist-info/RECORD +0 -5
- idmtools_platform_general-0.0.0.dev0.dist-info/top_level.txt +0 -1
- {idmtools_platform_general-0.0.0.dev0.dist-info → idmtools_platform_general-0.0.2.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-files") # 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,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,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.")
|