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.
Files changed (127) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +3 -1
  3. fractal_server/app/models/linkusergroup.py +6 -2
  4. fractal_server/app/models/v2/__init__.py +11 -1
  5. fractal_server/app/models/v2/accounting.py +35 -0
  6. fractal_server/app/models/v2/dataset.py +1 -11
  7. fractal_server/app/models/v2/history.py +78 -0
  8. fractal_server/app/models/v2/job.py +10 -3
  9. fractal_server/app/models/v2/task_group.py +2 -2
  10. fractal_server/app/models/v2/workflow.py +1 -1
  11. fractal_server/app/models/v2/workflowtask.py +1 -1
  12. fractal_server/app/routes/admin/v2/__init__.py +4 -0
  13. fractal_server/app/routes/admin/v2/accounting.py +98 -0
  14. fractal_server/app/routes/admin/v2/impersonate.py +35 -0
  15. fractal_server/app/routes/admin/v2/job.py +5 -13
  16. fractal_server/app/routes/admin/v2/task.py +1 -1
  17. fractal_server/app/routes/admin/v2/task_group.py +4 -29
  18. fractal_server/app/routes/api/__init__.py +1 -1
  19. fractal_server/app/routes/api/v2/__init__.py +8 -2
  20. fractal_server/app/routes/api/v2/_aux_functions.py +66 -0
  21. fractal_server/app/routes/api/v2/_aux_functions_history.py +166 -0
  22. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
  23. fractal_server/app/routes/api/v2/dataset.py +0 -17
  24. fractal_server/app/routes/api/v2/history.py +544 -0
  25. fractal_server/app/routes/api/v2/images.py +31 -43
  26. fractal_server/app/routes/api/v2/job.py +30 -0
  27. fractal_server/app/routes/api/v2/project.py +1 -53
  28. fractal_server/app/routes/api/v2/{status.py → status_legacy.py} +6 -6
  29. fractal_server/app/routes/api/v2/submit.py +17 -14
  30. fractal_server/app/routes/api/v2/task.py +3 -10
  31. fractal_server/app/routes/api/v2/task_collection_custom.py +4 -9
  32. fractal_server/app/routes/api/v2/task_group.py +2 -22
  33. fractal_server/app/routes/api/v2/verify_image_types.py +61 -0
  34. fractal_server/app/routes/api/v2/workflow.py +28 -69
  35. fractal_server/app/routes/api/v2/workflowtask.py +53 -50
  36. fractal_server/app/routes/auth/group.py +0 -16
  37. fractal_server/app/routes/auth/oauth.py +5 -3
  38. fractal_server/app/routes/aux/__init__.py +0 -20
  39. fractal_server/app/routes/pagination.py +47 -0
  40. fractal_server/app/runner/components.py +0 -3
  41. fractal_server/app/runner/compress_folder.py +57 -29
  42. fractal_server/app/runner/exceptions.py +4 -0
  43. fractal_server/app/runner/executors/base_runner.py +157 -0
  44. fractal_server/app/runner/{v2/_local/_local_config.py → executors/local/get_local_config.py} +7 -9
  45. fractal_server/app/runner/executors/local/runner.py +248 -0
  46. fractal_server/app/runner/executors/{slurm → slurm_common}/_batching.py +1 -1
  47. fractal_server/app/runner/executors/{slurm → slurm_common}/_slurm_config.py +9 -7
  48. fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py +868 -0
  49. fractal_server/app/runner/{v2/_slurm_common → executors/slurm_common}/get_slurm_config.py +48 -17
  50. fractal_server/app/runner/executors/{slurm → slurm_common}/remote.py +36 -47
  51. fractal_server/app/runner/executors/slurm_common/slurm_job_task_models.py +134 -0
  52. fractal_server/app/runner/executors/slurm_ssh/runner.py +268 -0
  53. fractal_server/app/runner/executors/slurm_sudo/__init__.py +0 -0
  54. fractal_server/app/runner/executors/{slurm/sudo → slurm_sudo}/_subprocess_run_as_user.py +2 -83
  55. fractal_server/app/runner/executors/slurm_sudo/runner.py +193 -0
  56. fractal_server/app/runner/extract_archive.py +1 -3
  57. fractal_server/app/runner/task_files.py +134 -87
  58. fractal_server/app/runner/v2/__init__.py +0 -395
  59. fractal_server/app/runner/v2/_local.py +88 -0
  60. fractal_server/app/runner/v2/{_slurm_ssh/__init__.py → _slurm_ssh.py} +22 -19
  61. fractal_server/app/runner/v2/{_slurm_sudo/__init__.py → _slurm_sudo.py} +19 -15
  62. fractal_server/app/runner/v2/db_tools.py +119 -0
  63. fractal_server/app/runner/v2/runner.py +219 -98
  64. fractal_server/app/runner/v2/runner_functions.py +491 -189
  65. fractal_server/app/runner/v2/runner_functions_low_level.py +40 -43
  66. fractal_server/app/runner/v2/submit_workflow.py +358 -0
  67. fractal_server/app/runner/v2/task_interface.py +31 -0
  68. fractal_server/app/schemas/_validators.py +13 -24
  69. fractal_server/app/schemas/user.py +10 -7
  70. fractal_server/app/schemas/user_settings.py +9 -21
  71. fractal_server/app/schemas/v2/__init__.py +10 -1
  72. fractal_server/app/schemas/v2/accounting.py +18 -0
  73. fractal_server/app/schemas/v2/dataset.py +12 -94
  74. fractal_server/app/schemas/v2/dumps.py +26 -9
  75. fractal_server/app/schemas/v2/history.py +80 -0
  76. fractal_server/app/schemas/v2/job.py +15 -8
  77. fractal_server/app/schemas/v2/manifest.py +14 -7
  78. fractal_server/app/schemas/v2/project.py +9 -7
  79. fractal_server/app/schemas/v2/status_legacy.py +35 -0
  80. fractal_server/app/schemas/v2/task.py +72 -77
  81. fractal_server/app/schemas/v2/task_collection.py +14 -32
  82. fractal_server/app/schemas/v2/task_group.py +10 -9
  83. fractal_server/app/schemas/v2/workflow.py +10 -11
  84. fractal_server/app/schemas/v2/workflowtask.py +2 -21
  85. fractal_server/app/security/__init__.py +3 -3
  86. fractal_server/app/security/signup_email.py +2 -2
  87. fractal_server/config.py +91 -90
  88. fractal_server/images/tools.py +23 -0
  89. fractal_server/migrations/versions/47351f8c7ebc_drop_dataset_filters.py +50 -0
  90. fractal_server/migrations/versions/9db60297b8b2_set_ondelete.py +250 -0
  91. fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py +57 -0
  92. fractal_server/migrations/versions/c90a7c76e996_job_id_in_history_run.py +41 -0
  93. fractal_server/migrations/versions/e81103413827_add_job_type_filters.py +36 -0
  94. fractal_server/migrations/versions/f37aceb45062_make_historyunit_logfile_required.py +39 -0
  95. fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py +120 -0
  96. fractal_server/ssh/_fabric.py +28 -14
  97. fractal_server/tasks/v2/local/collect.py +2 -2
  98. fractal_server/tasks/v2/ssh/collect.py +2 -2
  99. fractal_server/tasks/v2/templates/2_pip_install.sh +1 -1
  100. fractal_server/tasks/v2/templates/4_pip_show.sh +1 -1
  101. fractal_server/tasks/v2/utils_background.py +1 -20
  102. fractal_server/tasks/v2/utils_database.py +30 -17
  103. fractal_server/tasks/v2/utils_templates.py +6 -0
  104. {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/METADATA +4 -4
  105. {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/RECORD +114 -99
  106. {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/WHEEL +1 -1
  107. fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +0 -126
  108. fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +0 -116
  109. fractal_server/app/runner/executors/slurm/ssh/executor.py +0 -1386
  110. fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py +0 -71
  111. fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py +0 -130
  112. fractal_server/app/runner/executors/slurm/sudo/executor.py +0 -1281
  113. fractal_server/app/runner/v2/_local/__init__.py +0 -129
  114. fractal_server/app/runner/v2/_local/_submit_setup.py +0 -52
  115. fractal_server/app/runner/v2/_local/executor.py +0 -100
  116. fractal_server/app/runner/v2/_slurm_ssh/_submit_setup.py +0 -83
  117. fractal_server/app/runner/v2/_slurm_sudo/_submit_setup.py +0 -83
  118. fractal_server/app/runner/v2/handle_failed_job.py +0 -59
  119. fractal_server/app/schemas/v2/status.py +0 -16
  120. /fractal_server/app/{runner/executors/slurm → history}/__init__.py +0 -0
  121. /fractal_server/app/runner/executors/{slurm/ssh → local}/__init__.py +0 -0
  122. /fractal_server/app/runner/executors/{slurm/sudo → slurm_common}/__init__.py +0 -0
  123. /fractal_server/app/runner/executors/{_job_states.py → slurm_common/_job_states.py} +0 -0
  124. /fractal_server/app/runner/executors/{slurm → slurm_common}/utils_executors.py +0 -0
  125. /fractal_server/app/runner/{v2/_slurm_common → executors/slurm_ssh}/__init__.py +0 -0
  126. {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/LICENSE +0 -0
  127. {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 ......logger import set_logger
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(order: Union[int, str], task_name: str) -> str:
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
- Group all file paths pertaining to a task
31
+ Files related to a task.
23
32
 
24
33
  Attributes:
25
- workflow_dir_local:
26
- Server-owned directory to store all task-execution-related relevant
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
- Specific component to run the task for (relevant for tasks to be
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
- workflow_dir_local: Path
55
- workflow_dir_remote: Path
56
- remote_subfolder: Path
57
- subfolder_name: str
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: Optional[int] = None
48
+ task_order: int
49
+
50
+ # Per-single-component
60
51
  component: Optional[str] = None
52
+ prefix: Optional[str] = None
61
53
 
62
- file_prefix: str
63
- file_prefix_with_subfolder: str
64
- args: Path
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
- if self.task_order is not None:
91
- order = str(self.task_order)
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
- order = "0"
94
- self.file_prefix = f"{order}{component_safe}"
95
- self.subfolder_name = task_subfolder_name(
96
- order=order, task_name=self.task_name
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
- self.remote_subfolder = self.workflow_dir_remote / self.subfolder_name
99
- self.args = self.remote_subfolder / f"{self.file_prefix}.args.json"
100
- self.out = self.remote_subfolder / f"{self.file_prefix}.out"
101
- self.err = self.remote_subfolder / f"{self.file_prefix}.err"
102
- self.log = self.remote_subfolder / f"{self.file_prefix}.log"
103
- self.metadiff = (
104
- self.remote_subfolder / f"{self.file_prefix}.metadiff.json"
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
- def get_task_file_paths(
109
- workflow_dir_local: Path,
110
- workflow_dir_remote: Path,
111
- task_name: str,
112
- task_order: Optional[int] = None,
113
- component: Optional[str] = None,
114
- ) -> TaskFiles:
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
- Return the corrisponding TaskFiles object
153
+ Expand `TaskFiles` objects with `component` and `prefix`.
117
154
  """
118
- return TaskFiles(
119
- workflow_dir_local=workflow_dir_local,
120
- workflow_dir_remote=workflow_dir_remote,
121
- task_name=task_name,
122
- task_order=task_order,
123
- component=component,
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