fractal-server 2.13.0__py3-none-any.whl → 2.14.0__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.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +3 -1
- fractal_server/app/models/linkusergroup.py +6 -2
- fractal_server/app/models/v2/__init__.py +11 -1
- fractal_server/app/models/v2/accounting.py +35 -0
- fractal_server/app/models/v2/dataset.py +1 -11
- fractal_server/app/models/v2/history.py +78 -0
- fractal_server/app/models/v2/job.py +10 -3
- fractal_server/app/models/v2/task_group.py +2 -2
- fractal_server/app/models/v2/workflow.py +1 -1
- fractal_server/app/models/v2/workflowtask.py +1 -1
- fractal_server/app/routes/admin/v2/__init__.py +4 -0
- fractal_server/app/routes/admin/v2/accounting.py +98 -0
- fractal_server/app/routes/admin/v2/impersonate.py +35 -0
- fractal_server/app/routes/admin/v2/job.py +5 -13
- fractal_server/app/routes/admin/v2/task.py +1 -1
- fractal_server/app/routes/admin/v2/task_group.py +4 -29
- fractal_server/app/routes/api/__init__.py +1 -1
- fractal_server/app/routes/api/v2/__init__.py +8 -2
- fractal_server/app/routes/api/v2/_aux_functions.py +66 -0
- fractal_server/app/routes/api/v2/_aux_functions_history.py +166 -0
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
- fractal_server/app/routes/api/v2/dataset.py +0 -17
- fractal_server/app/routes/api/v2/history.py +544 -0
- fractal_server/app/routes/api/v2/images.py +31 -43
- fractal_server/app/routes/api/v2/job.py +30 -0
- fractal_server/app/routes/api/v2/project.py +1 -53
- fractal_server/app/routes/api/v2/{status.py → status_legacy.py} +6 -6
- fractal_server/app/routes/api/v2/submit.py +17 -14
- fractal_server/app/routes/api/v2/task.py +3 -10
- fractal_server/app/routes/api/v2/task_collection_custom.py +4 -9
- fractal_server/app/routes/api/v2/task_group.py +2 -22
- fractal_server/app/routes/api/v2/verify_image_types.py +61 -0
- fractal_server/app/routes/api/v2/workflow.py +28 -69
- fractal_server/app/routes/api/v2/workflowtask.py +53 -50
- fractal_server/app/routes/auth/group.py +0 -16
- fractal_server/app/routes/auth/oauth.py +5 -3
- fractal_server/app/routes/aux/__init__.py +0 -20
- fractal_server/app/routes/pagination.py +47 -0
- fractal_server/app/runner/components.py +0 -3
- fractal_server/app/runner/compress_folder.py +57 -29
- fractal_server/app/runner/exceptions.py +4 -0
- fractal_server/app/runner/executors/base_runner.py +157 -0
- fractal_server/app/runner/{v2/_local/_local_config.py → executors/local/get_local_config.py} +7 -9
- fractal_server/app/runner/executors/local/runner.py +248 -0
- fractal_server/app/runner/executors/{slurm → slurm_common}/_batching.py +1 -1
- fractal_server/app/runner/executors/{slurm → slurm_common}/_slurm_config.py +9 -7
- fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py +868 -0
- fractal_server/app/runner/{v2/_slurm_common → executors/slurm_common}/get_slurm_config.py +48 -17
- fractal_server/app/runner/executors/{slurm → slurm_common}/remote.py +36 -47
- fractal_server/app/runner/executors/slurm_common/slurm_job_task_models.py +134 -0
- fractal_server/app/runner/executors/slurm_ssh/runner.py +268 -0
- fractal_server/app/runner/executors/slurm_sudo/__init__.py +0 -0
- fractal_server/app/runner/executors/{slurm/sudo → slurm_sudo}/_subprocess_run_as_user.py +2 -83
- fractal_server/app/runner/executors/slurm_sudo/runner.py +193 -0
- fractal_server/app/runner/extract_archive.py +1 -3
- fractal_server/app/runner/task_files.py +134 -87
- fractal_server/app/runner/v2/__init__.py +0 -395
- fractal_server/app/runner/v2/_local.py +88 -0
- fractal_server/app/runner/v2/{_slurm_ssh/__init__.py → _slurm_ssh.py} +22 -19
- fractal_server/app/runner/v2/{_slurm_sudo/__init__.py → _slurm_sudo.py} +19 -15
- fractal_server/app/runner/v2/db_tools.py +119 -0
- fractal_server/app/runner/v2/runner.py +219 -98
- fractal_server/app/runner/v2/runner_functions.py +491 -189
- fractal_server/app/runner/v2/runner_functions_low_level.py +40 -43
- fractal_server/app/runner/v2/submit_workflow.py +358 -0
- fractal_server/app/runner/v2/task_interface.py +31 -0
- fractal_server/app/schemas/_validators.py +13 -24
- fractal_server/app/schemas/user.py +10 -7
- fractal_server/app/schemas/user_settings.py +9 -21
- fractal_server/app/schemas/v2/__init__.py +10 -1
- fractal_server/app/schemas/v2/accounting.py +18 -0
- fractal_server/app/schemas/v2/dataset.py +12 -94
- fractal_server/app/schemas/v2/dumps.py +26 -9
- fractal_server/app/schemas/v2/history.py +80 -0
- fractal_server/app/schemas/v2/job.py +15 -8
- fractal_server/app/schemas/v2/manifest.py +14 -7
- fractal_server/app/schemas/v2/project.py +9 -7
- fractal_server/app/schemas/v2/status_legacy.py +35 -0
- fractal_server/app/schemas/v2/task.py +72 -77
- fractal_server/app/schemas/v2/task_collection.py +14 -32
- fractal_server/app/schemas/v2/task_group.py +10 -9
- fractal_server/app/schemas/v2/workflow.py +10 -11
- fractal_server/app/schemas/v2/workflowtask.py +2 -21
- fractal_server/app/security/__init__.py +3 -3
- fractal_server/app/security/signup_email.py +2 -2
- fractal_server/config.py +91 -90
- fractal_server/images/tools.py +23 -0
- fractal_server/migrations/versions/47351f8c7ebc_drop_dataset_filters.py +50 -0
- fractal_server/migrations/versions/9db60297b8b2_set_ondelete.py +250 -0
- fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py +57 -0
- fractal_server/migrations/versions/c90a7c76e996_job_id_in_history_run.py +41 -0
- fractal_server/migrations/versions/e81103413827_add_job_type_filters.py +36 -0
- fractal_server/migrations/versions/f37aceb45062_make_historyunit_logfile_required.py +39 -0
- fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py +120 -0
- fractal_server/ssh/_fabric.py +28 -14
- fractal_server/tasks/v2/local/collect.py +2 -2
- fractal_server/tasks/v2/ssh/collect.py +2 -2
- fractal_server/tasks/v2/templates/2_pip_install.sh +1 -1
- fractal_server/tasks/v2/templates/4_pip_show.sh +1 -1
- fractal_server/tasks/v2/utils_background.py +1 -20
- fractal_server/tasks/v2/utils_database.py +30 -17
- fractal_server/tasks/v2/utils_templates.py +6 -0
- {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/METADATA +4 -4
- {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/RECORD +114 -99
- {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/WHEEL +1 -1
- fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +0 -126
- fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +0 -116
- fractal_server/app/runner/executors/slurm/ssh/executor.py +0 -1386
- fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py +0 -71
- fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py +0 -130
- fractal_server/app/runner/executors/slurm/sudo/executor.py +0 -1281
- fractal_server/app/runner/v2/_local/__init__.py +0 -129
- fractal_server/app/runner/v2/_local/_submit_setup.py +0 -52
- fractal_server/app/runner/v2/_local/executor.py +0 -100
- fractal_server/app/runner/v2/_slurm_ssh/_submit_setup.py +0 -83
- fractal_server/app/runner/v2/_slurm_sudo/_submit_setup.py +0 -83
- fractal_server/app/runner/v2/handle_failed_job.py +0 -59
- fractal_server/app/schemas/v2/status.py +0 -16
- /fractal_server/app/{runner/executors/slurm → history}/__init__.py +0 -0
- /fractal_server/app/runner/executors/{slurm/ssh → local}/__init__.py +0 -0
- /fractal_server/app/runner/executors/{slurm/sudo → slurm_common}/__init__.py +0 -0
- /fractal_server/app/runner/executors/{_job_states.py → slurm_common/_job_states.py} +0 -0
- /fractal_server/app/runner/executors/{slurm → slurm_common}/utils_executors.py +0 -0
- /fractal_server/app/runner/{v2/_slurm_common → executors/slurm_ssh}/__init__.py +0 -0
- {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/entry_points.txt +0 -0
@@ -19,7 +19,7 @@ import shlex
|
|
19
19
|
import subprocess # nosec
|
20
20
|
from typing import Optional
|
21
21
|
|
22
|
-
from
|
22
|
+
from fractal_server.logger import set_logger
|
23
23
|
from fractal_server.string_tools import validate_cmd
|
24
24
|
|
25
25
|
logger = set_logger(__name__)
|
@@ -65,10 +65,7 @@ def _run_command_as_user(
|
|
65
65
|
|
66
66
|
if check and not res.returncode == 0:
|
67
67
|
raise RuntimeError(
|
68
|
-
f"{cmd=}\n\n"
|
69
|
-
f"{res.returncode=}\n\n"
|
70
|
-
f"{res.stdout=}\n\n"
|
71
|
-
f"{res.stderr=}\n"
|
68
|
+
f"{cmd=}\n\n{res.returncode=}\n\n{res.stdout=}\n\n{res.stderr=}\n"
|
72
69
|
)
|
73
70
|
|
74
71
|
return res
|
@@ -91,81 +88,3 @@ def _mkdir_as_user(*, folder: str, user: str) -> None:
|
|
91
88
|
|
92
89
|
cmd = f"mkdir -p {folder}"
|
93
90
|
_run_command_as_user(cmd=cmd, user=user, check=True)
|
94
|
-
|
95
|
-
|
96
|
-
def _glob_as_user(
|
97
|
-
*, folder: str, user: str, startswith: Optional[str] = None
|
98
|
-
) -> list[str]:
|
99
|
-
"""
|
100
|
-
Run `ls` in a folder (as a user) and filter results
|
101
|
-
|
102
|
-
Execute `ls` on a folder (impersonating a user, if `user` is not `None`)
|
103
|
-
and select results that start with `startswith` (if not `None`).
|
104
|
-
|
105
|
-
Arguments:
|
106
|
-
folder: Absolute path to the folder
|
107
|
-
user: If not `None`, the user to be impersonated via `sudo -u`
|
108
|
-
startswith: If not `None`, this is used to filter output of `ls`.
|
109
|
-
"""
|
110
|
-
|
111
|
-
res = _run_command_as_user(cmd=f"ls {folder}", user=user, check=True)
|
112
|
-
output = res.stdout.split()
|
113
|
-
if startswith:
|
114
|
-
output = [f for f in output if f.startswith(startswith)]
|
115
|
-
return output
|
116
|
-
|
117
|
-
|
118
|
-
def _glob_as_user_strict(
|
119
|
-
*,
|
120
|
-
folder: str,
|
121
|
-
user: str,
|
122
|
-
startswith: str,
|
123
|
-
) -> list[str]:
|
124
|
-
"""
|
125
|
-
Run `ls` in a folder (as a user) and filter results
|
126
|
-
|
127
|
-
Execute `ls` on a folder (impersonating a user, if `user` is not `None`)
|
128
|
-
and select results that comply with a set of rules. They all start with
|
129
|
-
`startswith` (if not `None`), and they match one of the known filename
|
130
|
-
patterns. See details in
|
131
|
-
https://github.com/fractal-analytics-platform/fractal-server/issues/1240
|
132
|
-
|
133
|
-
|
134
|
-
Arguments:
|
135
|
-
folder: Absolute path to the folder
|
136
|
-
user: If not `None`, the user to be impersonated via `sudo -u`
|
137
|
-
startswith: If not `None`, this is used to filter output of `ls`.
|
138
|
-
"""
|
139
|
-
|
140
|
-
res = _run_command_as_user(cmd=f"ls {folder}", user=user, check=True)
|
141
|
-
output = res.stdout.split()
|
142
|
-
|
143
|
-
new_output = []
|
144
|
-
known_filenames = [
|
145
|
-
f"{startswith}{suffix}"
|
146
|
-
for suffix in [".args.json", ".metadiff.json", ".err", ".out", ".log"]
|
147
|
-
]
|
148
|
-
for filename in output:
|
149
|
-
if filename in known_filenames:
|
150
|
-
new_output.append(filename)
|
151
|
-
elif filename.startswith(f"{startswith}_out_") and filename.endswith(
|
152
|
-
".pickle"
|
153
|
-
):
|
154
|
-
new_output.append(filename)
|
155
|
-
|
156
|
-
return new_output
|
157
|
-
|
158
|
-
|
159
|
-
def _path_exists_as_user(*, path: str, user: Optional[str] = None) -> bool:
|
160
|
-
"""
|
161
|
-
Impersonate a user and check if `path` exists via `ls`
|
162
|
-
|
163
|
-
Arguments:
|
164
|
-
path: Absolute file/folder path
|
165
|
-
user: If not `None`, user to be impersonated
|
166
|
-
"""
|
167
|
-
res = _run_command_as_user(cmd=f"ls {path}", user=user)
|
168
|
-
if res.returncode == 0:
|
169
|
-
return True
|
170
|
-
else:
|
171
|
-
return False
|
@@ -0,0 +1,193 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import shlex
|
4
|
+
import subprocess # nosec
|
5
|
+
import sys
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Optional
|
9
|
+
|
10
|
+
from ..slurm_common.base_slurm_runner import BaseSlurmRunner
|
11
|
+
from ..slurm_common.slurm_job_task_models import SlurmJob
|
12
|
+
from ._subprocess_run_as_user import _mkdir_as_user
|
13
|
+
from ._subprocess_run_as_user import _run_command_as_user
|
14
|
+
from fractal_server.app.runner.exceptions import JobExecutionError
|
15
|
+
from fractal_server.config import get_settings
|
16
|
+
from fractal_server.logger import set_logger
|
17
|
+
from fractal_server.syringe import Inject
|
18
|
+
|
19
|
+
logger = set_logger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
def _subprocess_run_or_raise(
|
23
|
+
full_command: str,
|
24
|
+
) -> Optional[subprocess.CompletedProcess]:
|
25
|
+
try:
|
26
|
+
output = subprocess.run( # nosec
|
27
|
+
shlex.split(full_command),
|
28
|
+
capture_output=True,
|
29
|
+
check=True,
|
30
|
+
encoding="utf-8",
|
31
|
+
)
|
32
|
+
return output
|
33
|
+
except subprocess.CalledProcessError as e:
|
34
|
+
error_msg = (
|
35
|
+
f"Submit command `{full_command}` failed. "
|
36
|
+
f"Original error:\n{str(e)}\n"
|
37
|
+
f"Original stdout:\n{e.stdout}\n"
|
38
|
+
f"Original stderr:\n{e.stderr}\n"
|
39
|
+
)
|
40
|
+
logging.error(error_msg)
|
41
|
+
raise JobExecutionError(info=error_msg)
|
42
|
+
|
43
|
+
|
44
|
+
class SudoSlurmRunner(BaseSlurmRunner):
|
45
|
+
slurm_user: str
|
46
|
+
slurm_account: Optional[str] = None
|
47
|
+
|
48
|
+
def __init__(
|
49
|
+
self,
|
50
|
+
*,
|
51
|
+
# Common
|
52
|
+
root_dir_local: Path,
|
53
|
+
root_dir_remote: Path,
|
54
|
+
common_script_lines: Optional[list[str]] = None,
|
55
|
+
user_cache_dir: Optional[str] = None,
|
56
|
+
poll_interval: Optional[int] = None,
|
57
|
+
# Specific
|
58
|
+
slurm_account: Optional[str] = None,
|
59
|
+
slurm_user: str,
|
60
|
+
) -> None:
|
61
|
+
"""
|
62
|
+
Set parameters that are the same for different Fractal tasks and for
|
63
|
+
different SLURM jobs/tasks.
|
64
|
+
"""
|
65
|
+
|
66
|
+
self.slurm_user = slurm_user
|
67
|
+
self.slurm_account = slurm_account
|
68
|
+
settings = Inject(get_settings)
|
69
|
+
|
70
|
+
super().__init__(
|
71
|
+
slurm_runner_type="sudo",
|
72
|
+
root_dir_local=root_dir_local,
|
73
|
+
root_dir_remote=root_dir_remote,
|
74
|
+
common_script_lines=common_script_lines,
|
75
|
+
user_cache_dir=user_cache_dir,
|
76
|
+
poll_interval=poll_interval,
|
77
|
+
python_worker_interpreter=(
|
78
|
+
settings.FRACTAL_SLURM_WORKER_PYTHON or sys.executable
|
79
|
+
),
|
80
|
+
)
|
81
|
+
|
82
|
+
def _mkdir_local_folder(self, folder: str) -> None:
|
83
|
+
original_umask = os.umask(0)
|
84
|
+
Path(folder).mkdir(parents=True, mode=0o755)
|
85
|
+
os.umask(original_umask)
|
86
|
+
|
87
|
+
def _mkdir_remote_folder(self, folder: str) -> None:
|
88
|
+
_mkdir_as_user(folder=folder, user=self.slurm_user)
|
89
|
+
|
90
|
+
def _fetch_artifacts_single_job(self, job: SlurmJob) -> None:
|
91
|
+
"""
|
92
|
+
Fetch artifacts for a single SLURM jobs.
|
93
|
+
"""
|
94
|
+
logger.debug(
|
95
|
+
f"[_fetch_artifacts_single_job] {job.slurm_job_id=} START"
|
96
|
+
)
|
97
|
+
source_target_list = [
|
98
|
+
(job.slurm_stdout_remote, job.slurm_stdout_local),
|
99
|
+
(job.slurm_stderr_remote, job.slurm_stderr_local),
|
100
|
+
]
|
101
|
+
for task in job.tasks:
|
102
|
+
source_target_list.extend(
|
103
|
+
[
|
104
|
+
(
|
105
|
+
task.output_pickle_file_remote,
|
106
|
+
task.output_pickle_file_local,
|
107
|
+
),
|
108
|
+
(
|
109
|
+
task.task_files.log_file_remote,
|
110
|
+
task.task_files.log_file_local,
|
111
|
+
),
|
112
|
+
(
|
113
|
+
task.task_files.args_file_remote,
|
114
|
+
task.task_files.args_file_local,
|
115
|
+
),
|
116
|
+
(
|
117
|
+
task.task_files.metadiff_file_remote,
|
118
|
+
task.task_files.metadiff_file_local,
|
119
|
+
),
|
120
|
+
]
|
121
|
+
)
|
122
|
+
|
123
|
+
for source, target in source_target_list:
|
124
|
+
# NOTE: By setting encoding=None, we read/write bytes instead
|
125
|
+
# of strings; this is needed to also handle pickle files.
|
126
|
+
try:
|
127
|
+
res = _run_command_as_user(
|
128
|
+
cmd=f"cat {source}",
|
129
|
+
user=self.slurm_user,
|
130
|
+
encoding=None,
|
131
|
+
check=True,
|
132
|
+
)
|
133
|
+
# Write local file
|
134
|
+
with open(target, "wb") as f:
|
135
|
+
f.write(res.stdout)
|
136
|
+
logger.debug(
|
137
|
+
f"[_fetch_artifacts_single_job] Copied {source} into "
|
138
|
+
f"{target}"
|
139
|
+
)
|
140
|
+
except RuntimeError as e:
|
141
|
+
logger.warning(
|
142
|
+
f"SKIP copy {source} into {target}. "
|
143
|
+
f"Original error: {str(e)}"
|
144
|
+
)
|
145
|
+
logger.debug(f"[_fetch_artifacts_single_job] {job.slurm_job_id=} END")
|
146
|
+
|
147
|
+
def _fetch_artifacts(
|
148
|
+
self,
|
149
|
+
finished_slurm_jobs: list[SlurmJob],
|
150
|
+
) -> None:
|
151
|
+
"""
|
152
|
+
Fetch artifacts for a list of SLURM jobs.
|
153
|
+
"""
|
154
|
+
MAX_NUM_THREADS = 4
|
155
|
+
THREAD_NAME_PREFIX = "fetch_artifacts"
|
156
|
+
logger.debug(
|
157
|
+
"[_fetch_artifacts] START "
|
158
|
+
f"({MAX_NUM_THREADS=}, {len(finished_slurm_jobs)=})."
|
159
|
+
)
|
160
|
+
with ThreadPoolExecutor(
|
161
|
+
max_workers=MAX_NUM_THREADS,
|
162
|
+
thread_name_prefix=THREAD_NAME_PREFIX,
|
163
|
+
) as executor:
|
164
|
+
executor.map(
|
165
|
+
self._fetch_artifacts_single_job,
|
166
|
+
finished_slurm_jobs,
|
167
|
+
)
|
168
|
+
logger.debug("[_fetch_artifacts] END.")
|
169
|
+
|
170
|
+
def _run_remote_cmd(self, cmd: str) -> str:
|
171
|
+
res = _run_command_as_user(
|
172
|
+
cmd=cmd,
|
173
|
+
user=self.slurm_user,
|
174
|
+
encoding="utf-8",
|
175
|
+
check=True,
|
176
|
+
)
|
177
|
+
return res.stdout
|
178
|
+
|
179
|
+
def run_squeue(self, job_ids: list[str]) -> str:
|
180
|
+
"""
|
181
|
+
Run `squeue` for a set of SLURM job IDs.
|
182
|
+
"""
|
183
|
+
|
184
|
+
if len(job_ids) == 0:
|
185
|
+
return ""
|
186
|
+
|
187
|
+
job_id_single_str = ",".join([str(j) for j in job_ids])
|
188
|
+
cmd = (
|
189
|
+
"squeue --noheader --format='%i %T' --states=all "
|
190
|
+
f"--jobs {job_id_single_str}"
|
191
|
+
)
|
192
|
+
res = _subprocess_run_or_raise(cmd)
|
193
|
+
return res.stdout
|
@@ -57,9 +57,7 @@ def extract_archive(archive_path: Path):
|
|
57
57
|
|
58
58
|
# Run tar command
|
59
59
|
cmd_tar = (
|
60
|
-
f"tar -xzvf {archive_path} "
|
61
|
-
f"--directory={subfolder_path.as_posix()} "
|
62
|
-
"."
|
60
|
+
f"tar -xzvf {archive_path} --directory={subfolder_path.as_posix()}"
|
63
61
|
)
|
64
62
|
logger.debug(f"{cmd_tar=}")
|
65
63
|
run_subprocess(cmd=cmd_tar, logger_name=logger_name)
|
@@ -2,10 +2,19 @@ from pathlib import Path
|
|
2
2
|
from typing import Optional
|
3
3
|
from typing import Union
|
4
4
|
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from fractal_server.app.runner.components import _index_to_component
|
5
8
|
from fractal_server.string_tools import sanitize_string
|
6
9
|
|
10
|
+
SUBMIT_PREFIX = "non_par"
|
11
|
+
MULTISUBMIT_PREFIX = "par"
|
12
|
+
|
7
13
|
|
8
|
-
def task_subfolder_name(
|
14
|
+
def task_subfolder_name(
|
15
|
+
order: Union[int, str],
|
16
|
+
task_name: str,
|
17
|
+
) -> str:
|
9
18
|
"""
|
10
19
|
Get name of task-specific subfolder.
|
11
20
|
|
@@ -17,108 +26,146 @@ def task_subfolder_name(order: Union[int, str], task_name: str) -> str:
|
|
17
26
|
return f"{order}_{task_name_slug}"
|
18
27
|
|
19
28
|
|
20
|
-
class TaskFiles:
|
29
|
+
class TaskFiles(BaseModel):
|
21
30
|
"""
|
22
|
-
|
31
|
+
Files related to a task.
|
23
32
|
|
24
33
|
Attributes:
|
25
|
-
|
26
|
-
|
27
|
-
files. Note: users cannot write directly to this folder.
|
28
|
-
workflow_dir_remote:
|
29
|
-
User-side directory with the same scope as `workflow_dir_local`,
|
30
|
-
and where a user can write.
|
31
|
-
subfolder_name:
|
32
|
-
Name of task-specific subfolder
|
33
|
-
remote_subfolder:
|
34
|
-
Path to user-side task-specific subfolder
|
34
|
+
root_dir_local:
|
35
|
+
root_dir_remote:
|
35
36
|
task_name:
|
36
|
-
Name of the task
|
37
37
|
task_order:
|
38
|
-
Positional order of the task within a workflow.
|
39
38
|
component:
|
40
|
-
|
41
|
-
executed in parallel over many components).
|
42
|
-
file_prefix:
|
43
|
-
Prefix for all task-related files.
|
44
|
-
args:
|
45
|
-
Path for input json file.
|
46
|
-
metadiff:
|
47
|
-
Path for output json file with metadata update.
|
48
|
-
out:
|
49
|
-
Path for task-execution stdout.
|
50
|
-
err:
|
51
|
-
Path for task-execution stderr.
|
39
|
+
prefix:
|
52
40
|
"""
|
53
41
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
42
|
+
# Parent directory
|
43
|
+
root_dir_local: Path
|
44
|
+
root_dir_remote: Path
|
45
|
+
|
46
|
+
# Per-wftask
|
58
47
|
task_name: str
|
59
|
-
task_order:
|
48
|
+
task_order: int
|
49
|
+
|
50
|
+
# Per-single-component
|
60
51
|
component: Optional[str] = None
|
52
|
+
prefix: Optional[str] = None
|
61
53
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
out: Path
|
66
|
-
err: Path
|
67
|
-
log: Path
|
68
|
-
metadiff: Path
|
69
|
-
|
70
|
-
def __init__(
|
71
|
-
self,
|
72
|
-
workflow_dir_local: Path,
|
73
|
-
workflow_dir_remote: Path,
|
74
|
-
task_name: str,
|
75
|
-
task_order: Optional[int] = None,
|
76
|
-
component: Optional[str] = None,
|
77
|
-
):
|
78
|
-
self.workflow_dir_local = workflow_dir_local
|
79
|
-
self.workflow_dir_remote = workflow_dir_remote
|
80
|
-
self.task_order = task_order
|
81
|
-
self.task_name = task_name
|
82
|
-
self.component = component
|
83
|
-
|
84
|
-
if self.component is not None:
|
85
|
-
component_safe = sanitize_string(str(self.component))
|
86
|
-
component_safe = f"_par_{component_safe}"
|
87
|
-
else:
|
88
|
-
component_safe = ""
|
54
|
+
def _check_component(self):
|
55
|
+
if self.component is None:
|
56
|
+
raise ValueError("`component` cannot be None")
|
89
57
|
|
90
|
-
|
91
|
-
|
58
|
+
@property
|
59
|
+
def subfolder_name(self) -> str:
|
60
|
+
order = str(self.task_order or 0)
|
61
|
+
return task_subfolder_name(
|
62
|
+
order=order,
|
63
|
+
task_name=self.task_name,
|
64
|
+
)
|
65
|
+
|
66
|
+
@property
|
67
|
+
def wftask_subfolder_remote(self) -> Path:
|
68
|
+
return self.root_dir_remote / self.subfolder_name
|
69
|
+
|
70
|
+
@property
|
71
|
+
def wftask_subfolder_local(self) -> Path:
|
72
|
+
return self.root_dir_local / self.subfolder_name
|
73
|
+
|
74
|
+
@property
|
75
|
+
def prefix_component(self):
|
76
|
+
if self.prefix is None:
|
77
|
+
return self.component
|
92
78
|
else:
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
79
|
+
return f"{self.prefix}-{self.component}"
|
80
|
+
|
81
|
+
@property
|
82
|
+
def log_file_local(self) -> str:
|
83
|
+
self._check_component()
|
84
|
+
return (
|
85
|
+
self.wftask_subfolder_local / f"{self.prefix_component}-log.txt"
|
86
|
+
).as_posix()
|
87
|
+
|
88
|
+
@property
|
89
|
+
def log_file_remote_path(self) -> Path:
|
90
|
+
self._check_component()
|
91
|
+
return (
|
92
|
+
self.wftask_subfolder_remote / f"{self.prefix_component}-log.txt"
|
97
93
|
)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
94
|
+
|
95
|
+
@property
|
96
|
+
def log_file_remote(self) -> str:
|
97
|
+
return self.log_file_remote_path.as_posix()
|
98
|
+
|
99
|
+
@property
|
100
|
+
def args_file_local(self) -> str:
|
101
|
+
self._check_component()
|
102
|
+
return (
|
103
|
+
self.wftask_subfolder_local / f"{self.prefix_component}-args.json"
|
104
|
+
).as_posix()
|
105
|
+
|
106
|
+
@property
|
107
|
+
def args_file_remote_path(self) -> Path:
|
108
|
+
self._check_component()
|
109
|
+
return (
|
110
|
+
self.wftask_subfolder_remote / f"{self.prefix_component}-args.json"
|
105
111
|
)
|
106
112
|
|
113
|
+
@property
|
114
|
+
def args_file_remote(self) -> str:
|
115
|
+
return self.args_file_remote_path.as_posix()
|
107
116
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
)
|
117
|
+
@property
|
118
|
+
def metadiff_file_local(self) -> str:
|
119
|
+
self._check_component()
|
120
|
+
return (
|
121
|
+
self.wftask_subfolder_local
|
122
|
+
/ f"{self.prefix_component}-metadiff.json"
|
123
|
+
).as_posix()
|
124
|
+
|
125
|
+
@property
|
126
|
+
def metadiff_file_remote_path(self) -> Path:
|
127
|
+
self._check_component()
|
128
|
+
return (
|
129
|
+
self.wftask_subfolder_remote
|
130
|
+
/ f"{self.prefix_component}-metadiff.json"
|
131
|
+
)
|
132
|
+
|
133
|
+
@property
|
134
|
+
def metadiff_file_remote(self) -> str:
|
135
|
+
return self.metadiff_file_remote_path.as_posix()
|
136
|
+
|
137
|
+
@property
|
138
|
+
def remote_files_dict(self) -> dict[str, str]:
|
139
|
+
return dict(
|
140
|
+
args_file_remote=self.args_file_remote,
|
141
|
+
metadiff_file_remote=self.metadiff_file_remote,
|
142
|
+
log_file_remote=self.log_file_remote,
|
143
|
+
)
|
144
|
+
|
145
|
+
|
146
|
+
def enrich_task_files_multisubmit(
|
147
|
+
*,
|
148
|
+
tot_tasks: int,
|
149
|
+
batch_size: int,
|
150
|
+
base_task_files: TaskFiles,
|
151
|
+
) -> list[TaskFiles]:
|
115
152
|
"""
|
116
|
-
|
153
|
+
Expand `TaskFiles` objects with `component` and `prefix`.
|
117
154
|
"""
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
155
|
+
|
156
|
+
new_list_task_files: list[TaskFiles] = []
|
157
|
+
for absolute_index in range(tot_tasks):
|
158
|
+
ind_batch = absolute_index // batch_size
|
159
|
+
new_list_task_files.append(
|
160
|
+
TaskFiles(
|
161
|
+
**base_task_files.model_dump(
|
162
|
+
exclude={
|
163
|
+
"component",
|
164
|
+
"prefix",
|
165
|
+
}
|
166
|
+
),
|
167
|
+
prefix=f"{MULTISUBMIT_PREFIX}-{ind_batch:06d}",
|
168
|
+
component=_index_to_component(absolute_index),
|
169
|
+
)
|
170
|
+
)
|
171
|
+
return new_list_task_files
|