fractal-server 2.1.0__py3-none-any.whl → 2.2.0a1__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 (48) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/db/__init__.py +1 -1
  3. fractal_server/app/routes/admin/v1.py +2 -4
  4. fractal_server/app/routes/admin/v2.py +2 -4
  5. fractal_server/app/routes/api/v1/_aux_functions.py +24 -0
  6. fractal_server/app/routes/api/v1/job.py +3 -4
  7. fractal_server/app/routes/api/v1/project.py +28 -18
  8. fractal_server/app/routes/api/v2/_aux_functions.py +35 -12
  9. fractal_server/app/routes/api/v2/job.py +3 -4
  10. fractal_server/app/routes/api/v2/project.py +21 -0
  11. fractal_server/app/routes/api/v2/submit.py +33 -7
  12. fractal_server/app/routes/aux/_job.py +3 -1
  13. fractal_server/app/routes/aux/_runner.py +3 -3
  14. fractal_server/app/runner/executors/slurm/executor.py +157 -68
  15. fractal_server/app/runner/shutdown.py +88 -0
  16. fractal_server/app/runner/task_files.py +59 -27
  17. fractal_server/app/runner/v1/__init__.py +35 -19
  18. fractal_server/app/runner/v1/_common.py +53 -51
  19. fractal_server/app/runner/v1/_local/__init__.py +12 -11
  20. fractal_server/app/runner/v1/_local/_submit_setup.py +4 -4
  21. fractal_server/app/runner/v1/_slurm/__init__.py +16 -16
  22. fractal_server/app/runner/v1/_slurm/_submit_setup.py +11 -10
  23. fractal_server/app/runner/v1/_slurm/get_slurm_config.py +6 -6
  24. fractal_server/app/runner/v2/__init__.py +65 -17
  25. fractal_server/app/runner/v2/_local/__init__.py +12 -11
  26. fractal_server/app/runner/v2/_local/_local_config.py +1 -1
  27. fractal_server/app/runner/v2/_local/_submit_setup.py +4 -4
  28. fractal_server/app/runner/v2/_local_experimental/__init__.py +145 -0
  29. fractal_server/app/runner/v2/_local_experimental/_local_config.py +108 -0
  30. fractal_server/app/runner/v2/_local_experimental/_submit_setup.py +42 -0
  31. fractal_server/app/runner/v2/_local_experimental/executor.py +152 -0
  32. fractal_server/app/runner/v2/_slurm/__init__.py +10 -10
  33. fractal_server/app/runner/v2/_slurm/_submit_setup.py +11 -10
  34. fractal_server/app/runner/v2/_slurm/get_slurm_config.py +6 -6
  35. fractal_server/app/runner/v2/runner.py +17 -15
  36. fractal_server/app/runner/v2/runner_functions.py +38 -38
  37. fractal_server/app/runner/v2/runner_functions_low_level.py +12 -6
  38. fractal_server/config.py +52 -19
  39. fractal_server/gunicorn_fractal.py +40 -0
  40. fractal_server/{logger/__init__.py → logger.py} +2 -2
  41. fractal_server/main.py +24 -1
  42. fractal_server/migrations/env.py +1 -1
  43. {fractal_server-2.1.0.dist-info → fractal_server-2.2.0a1.dist-info}/METADATA +3 -1
  44. {fractal_server-2.1.0.dist-info → fractal_server-2.2.0a1.dist-info}/RECORD +47 -42
  45. fractal_server/logger/gunicorn_logger.py +0 -19
  46. {fractal_server-2.1.0.dist-info → fractal_server-2.2.0a1.dist-info}/LICENSE +0 -0
  47. {fractal_server-2.1.0.dist-info → fractal_server-2.2.0a1.dist-info}/WHEEL +0 -0
  48. {fractal_server-2.1.0.dist-info → fractal_server-2.2.0a1.dist-info}/entry_points.txt +0 -0
@@ -50,8 +50,8 @@ def _process_workflow(
50
50
  input_metadata: dict[str, Any],
51
51
  input_history: list[dict[str, Any]],
52
52
  logger_name: str,
53
- workflow_dir: Path,
54
- workflow_dir_user: Path,
53
+ workflow_dir_local: Path,
54
+ workflow_dir_remote: Path,
55
55
  first_task_index: int,
56
56
  last_task_index: int,
57
57
  slurm_user: Optional[str] = None,
@@ -86,8 +86,8 @@ def _process_workflow(
86
86
  keep_logs=True,
87
87
  slurm_user=slurm_user,
88
88
  user_cache_dir=user_cache_dir,
89
- working_dir=workflow_dir,
90
- working_dir_user=workflow_dir_user,
89
+ workflow_dir_local=workflow_dir_local,
90
+ workflow_dir_remote=workflow_dir_remote,
91
91
  common_script_lines=worker_init,
92
92
  slurm_account=slurm_account,
93
93
  ) as executor:
@@ -102,8 +102,8 @@ def _process_workflow(
102
102
  metadata=input_metadata,
103
103
  history=input_history,
104
104
  ),
105
- workflow_dir=workflow_dir,
106
- workflow_dir_user=workflow_dir_user,
105
+ workflow_dir_local=workflow_dir_local,
106
+ workflow_dir_remote=workflow_dir_remote,
107
107
  submit_setup_call=_slurm_submit_setup,
108
108
  logger_name=logger_name,
109
109
  )
@@ -121,8 +121,8 @@ async def process_workflow(
121
121
  input_metadata: dict[str, Any],
122
122
  input_history: list[dict[str, Any]],
123
123
  logger_name: str,
124
- workflow_dir: Path,
125
- workflow_dir_user: Optional[Path] = None,
124
+ workflow_dir_local: Path,
125
+ workflow_dir_remote: Optional[Path] = None,
126
126
  user_cache_dir: Optional[str] = None,
127
127
  slurm_user: Optional[str] = None,
128
128
  slurm_account: Optional[str] = None,
@@ -152,8 +152,8 @@ async def process_workflow(
152
152
  input_metadata=input_metadata,
153
153
  input_history=input_history,
154
154
  logger_name=logger_name,
155
- workflow_dir=workflow_dir,
156
- workflow_dir_user=workflow_dir_user,
155
+ workflow_dir_local=workflow_dir_local,
156
+ workflow_dir_remote=workflow_dir_remote,
157
157
  slurm_user=slurm_user,
158
158
  slurm_account=slurm_account,
159
159
  user_cache_dir=user_cache_dir,
@@ -166,8 +166,8 @@ async def process_workflow(
166
166
 
167
167
  def get_slurm_config(
168
168
  wftask: WorkflowTask,
169
- workflow_dir: Path,
170
- workflow_dir_user: Path,
169
+ workflow_dir_local: Path,
170
+ workflow_dir_remote: Path,
171
171
  config_path: Optional[Path] = None,
172
172
  ) -> SlurmConfig:
173
173
  """
@@ -187,13 +187,13 @@ def get_slurm_config(
187
187
  wftask:
188
188
  WorkflowTask for which the SLURM configuration is is to be
189
189
  prepared.
190
- workflow_dir:
190
+ workflow_dir_local:
191
191
  Server-owned directory to store all task-execution-related relevant
192
192
  files (inputs, outputs, errors, and all meta files related to the
193
193
  job execution). Note: users cannot write directly to this folder.
194
- workflow_dir_user:
195
- User-side directory with the same scope as `workflow_dir`, and
196
- where a user can write.
194
+ workflow_dir_remote:
195
+ User-side directory with the same scope as `workflow_dir_local`,
196
+ and where a user can write.
197
197
  config_path:
198
198
  Path of aFractal SLURM configuration file; if `None`, use
199
199
  `FRACTAL_SLURM_CONFIG_FILE` variable from settings.
@@ -24,8 +24,8 @@ from fractal_server.app.models.v1 import WorkflowTask
24
24
  def _slurm_submit_setup(
25
25
  *,
26
26
  wftask: WorkflowTask,
27
- workflow_dir: Path,
28
- workflow_dir_user: Path,
27
+ workflow_dir_local: Path,
28
+ workflow_dir_remote: Path,
29
29
  ) -> dict[str, object]:
30
30
  """
31
31
  Collect WorfklowTask-specific configuration parameters from different
@@ -43,13 +43,13 @@ def _slurm_submit_setup(
43
43
  Arguments:
44
44
  wftask:
45
45
  WorkflowTask for which the configuration is to be assembled
46
- workflow_dir:
46
+ workflow_dir_local:
47
47
  Server-owned directory to store all task-execution-related relevant
48
48
  files (inputs, outputs, errors, and all meta files related to the
49
49
  job execution). Note: users cannot write directly to this folder.
50
- workflow_dir_user:
51
- User-side directory with the same scope as `workflow_dir`, and
52
- where a user can write.
50
+ workflow_dir_remote:
51
+ User-side directory with the same scope as `workflow_dir_local`,
52
+ and where a user can write.
53
53
 
54
54
  Returns:
55
55
  submit_setup_dict:
@@ -61,15 +61,16 @@ def _slurm_submit_setup(
61
61
  # Get SlurmConfig object
62
62
  slurm_config = get_slurm_config(
63
63
  wftask=wftask,
64
- workflow_dir=workflow_dir,
65
- workflow_dir_user=workflow_dir_user,
64
+ workflow_dir_local=workflow_dir_local,
65
+ workflow_dir_remote=workflow_dir_remote,
66
66
  )
67
67
 
68
68
  # Get TaskFiles object
69
69
  task_files = get_task_file_paths(
70
- workflow_dir=workflow_dir,
71
- workflow_dir_user=workflow_dir_user,
70
+ workflow_dir_local=workflow_dir_local,
71
+ workflow_dir_remote=workflow_dir_remote,
72
72
  task_order=wftask.order,
73
+ task_name=wftask.task.name,
73
74
  )
74
75
 
75
76
  # Prepare and return output dictionary
@@ -17,8 +17,8 @@ from fractal_server.app.runner.executors.slurm._slurm_config import (
17
17
 
18
18
  def get_slurm_config(
19
19
  wftask: WorkflowTask,
20
- workflow_dir: Path,
21
- workflow_dir_user: Path,
20
+ workflow_dir_local: Path,
21
+ workflow_dir_remote: Path,
22
22
  config_path: Optional[Path] = None,
23
23
  ) -> SlurmConfig:
24
24
  """
@@ -38,13 +38,13 @@ def get_slurm_config(
38
38
  wftask:
39
39
  WorkflowTask for which the SLURM configuration is is to be
40
40
  prepared.
41
- workflow_dir:
41
+ workflow_dir_local:
42
42
  Server-owned directory to store all task-execution-related relevant
43
43
  files (inputs, outputs, errors, and all meta files related to the
44
44
  job execution). Note: users cannot write directly to this folder.
45
- workflow_dir_user:
46
- User-side directory with the same scope as `workflow_dir`, and
47
- where a user can write.
45
+ workflow_dir_remote:
46
+ User-side directory with the same scope as `workflow_dir_local`,
47
+ and where a user can write.
48
48
  config_path:
49
49
  Path of aFractal SLURM configuration file; if `None`, use
50
50
  `FRACTAL_SLURM_CONFIG_FILE` variable from settings.
@@ -25,8 +25,13 @@ from ...models.v2 import WorkflowV2
25
25
  from ...schemas.v2 import JobStatusTypeV2
26
26
  from ..exceptions import JobExecutionError
27
27
  from ..exceptions import TaskExecutionError
28
+ from ..executors.slurm._subprocess_run_as_user import _mkdir_as_user
28
29
  from ..filenames import WORKFLOW_LOG_FILENAME
30
+ from ..task_files import task_subfolder_name
29
31
  from ._local import process_workflow as local_process_workflow
32
+ from ._local_experimental import (
33
+ process_workflow as local_experimental_process_workflow,
34
+ )
30
35
  from ._slurm import process_workflow as slurm_process_workflow
31
36
  from .handle_failed_job import assemble_filters_failed_job
32
37
  from .handle_failed_job import assemble_history_failed_job
@@ -35,6 +40,7 @@ from fractal_server import __VERSION__
35
40
 
36
41
  _backends = {}
37
42
  _backends["local"] = local_process_workflow
43
+ _backends["local_experimental"] = local_experimental_process_workflow
38
44
  _backends["slurm"] = slurm_process_workflow
39
45
 
40
46
 
@@ -78,6 +84,8 @@ async def submit_workflow(
78
84
  FRACTAL_RUNNER_BACKEND = settings.FRACTAL_RUNNER_BACKEND
79
85
  if FRACTAL_RUNNER_BACKEND == "local":
80
86
  process_workflow = local_process_workflow
87
+ elif FRACTAL_RUNNER_BACKEND == "local_experimental":
88
+ process_workflow = local_experimental_process_workflow
81
89
  elif FRACTAL_RUNNER_BACKEND == "slurm":
82
90
  process_workflow = slurm_process_workflow
83
91
  else:
@@ -108,29 +116,67 @@ async def submit_workflow(
108
116
  return
109
117
 
110
118
  # Define and create server-side working folder
111
- WORKFLOW_DIR = Path(job.working_dir)
112
- if WORKFLOW_DIR.exists():
119
+ WORKFLOW_DIR_LOCAL = Path(job.working_dir)
120
+ if WORKFLOW_DIR_LOCAL.exists():
113
121
  job.status = JobStatusTypeV2.FAILED
114
122
  job.end_timestamp = get_timestamp()
115
- job.log = f"Workflow dir {WORKFLOW_DIR} already exists."
123
+ job.log = f"Workflow dir {WORKFLOW_DIR_LOCAL} already exists."
116
124
  db_sync.merge(job)
117
125
  db_sync.commit()
118
126
  db_sync.close()
119
127
  return
120
128
 
121
- # Create WORKFLOW_DIR with 755 permissions
122
- original_umask = os.umask(0)
123
- WORKFLOW_DIR.mkdir(parents=True, mode=0o755)
124
- os.umask(original_umask)
129
+ try:
125
130
 
126
- # Define and create user-side working folder, if needed
127
- WORKFLOW_DIR_USER = Path(job.working_dir_user)
128
- if FRACTAL_RUNNER_BACKEND == "slurm":
129
- from ..executors.slurm._subprocess_run_as_user import (
130
- _mkdir_as_user,
131
- )
131
+ # Create WORKFLOW_DIR
132
+ original_umask = os.umask(0)
133
+ WORKFLOW_DIR_LOCAL.mkdir(parents=True, mode=0o755)
134
+
135
+ os.umask(original_umask)
132
136
 
133
- _mkdir_as_user(folder=str(WORKFLOW_DIR_USER), user=slurm_user)
137
+ # Define and create WORKFLOW_DIR_REMOTE
138
+ if FRACTAL_RUNNER_BACKEND == "local":
139
+ WORKFLOW_DIR_REMOTE = WORKFLOW_DIR_LOCAL
140
+ elif FRACTAL_RUNNER_BACKEND == "local_experimental":
141
+ WORKFLOW_DIR_REMOTE = WORKFLOW_DIR_LOCAL
142
+ elif FRACTAL_RUNNER_BACKEND == "slurm":
143
+ WORKFLOW_DIR_REMOTE = (
144
+ Path(user_cache_dir) / WORKFLOW_DIR_LOCAL.name
145
+ )
146
+ _mkdir_as_user(
147
+ folder=str(WORKFLOW_DIR_REMOTE), user=slurm_user
148
+ )
149
+
150
+ # Create all tasks subfolders
151
+ for order in range(job.first_task_index, job.last_task_index + 1):
152
+ this_wftask = workflow.task_list[order]
153
+ if this_wftask.is_legacy_task:
154
+ task_name = this_wftask.task_legacy.name
155
+ else:
156
+ task_name = this_wftask.task.name
157
+ subfolder_name = task_subfolder_name(
158
+ order=order,
159
+ task_name=task_name,
160
+ )
161
+ original_umask = os.umask(0)
162
+ (WORKFLOW_DIR_LOCAL / subfolder_name).mkdir(mode=0o755)
163
+ os.umask(original_umask)
164
+ if FRACTAL_RUNNER_BACKEND == "slurm":
165
+ _mkdir_as_user(
166
+ folder=str(WORKFLOW_DIR_REMOTE / subfolder_name),
167
+ user=slurm_user,
168
+ )
169
+ except Exception as e:
170
+ job.status = JobStatusTypeV2.FAILED
171
+ job.end_timestamp = get_timestamp()
172
+ job.log = (
173
+ "An error occurred while creating job folder and subfolders.\n"
174
+ f"Original error: {str(e)}"
175
+ )
176
+ db_sync.merge(job)
177
+ db_sync.commit()
178
+ db_sync.close()
179
+ return
134
180
 
135
181
  # After Session.commit() is called, either explicitly or when using a
136
182
  # context manager, all objects associated with the Session are expired.
@@ -145,10 +191,12 @@ async def submit_workflow(
145
191
 
146
192
  db_sync.refresh(dataset)
147
193
  db_sync.refresh(workflow)
194
+ for wftask in workflow.task_list:
195
+ db_sync.refresh(wftask)
148
196
 
149
197
  # Write logs
150
198
  logger_name = f"WF{workflow_id}_job{job_id}"
151
- log_file_path = WORKFLOW_DIR / WORKFLOW_LOG_FILENAME
199
+ log_file_path = WORKFLOW_DIR_LOCAL / WORKFLOW_LOG_FILENAME
152
200
  logger = set_logger(
153
201
  logger_name=logger_name,
154
202
  log_file_path=log_file_path,
@@ -189,8 +237,8 @@ async def submit_workflow(
189
237
  slurm_user=slurm_user,
190
238
  slurm_account=job.slurm_account,
191
239
  user_cache_dir=user_cache_dir,
192
- workflow_dir=WORKFLOW_DIR,
193
- workflow_dir_user=WORKFLOW_DIR_USER,
240
+ workflow_dir_local=WORKFLOW_DIR_LOCAL,
241
+ workflow_dir_remote=WORKFLOW_DIR_REMOTE,
194
242
  logger_name=logger_name,
195
243
  worker_init=worker_init,
196
244
  first_task_index=job.first_task_index,
@@ -36,7 +36,7 @@ def _process_workflow(
36
36
  workflow: WorkflowV2,
37
37
  dataset: DatasetV2,
38
38
  logger_name: str,
39
- workflow_dir: Path,
39
+ workflow_dir_local: Path,
40
40
  first_task_index: int,
41
41
  last_task_index: int,
42
42
  ) -> dict:
@@ -57,8 +57,8 @@ def _process_workflow(
57
57
  ], # noqa
58
58
  dataset=dataset,
59
59
  executor=executor,
60
- workflow_dir=workflow_dir,
61
- workflow_dir_user=workflow_dir,
60
+ workflow_dir_local=workflow_dir_local,
61
+ workflow_dir_remote=workflow_dir_local,
62
62
  logger_name=logger_name,
63
63
  submit_setup_call=_local_submit_setup,
64
64
  )
@@ -69,8 +69,8 @@ async def process_workflow(
69
69
  *,
70
70
  workflow: WorkflowV2,
71
71
  dataset: DatasetV2,
72
- workflow_dir: Path,
73
- workflow_dir_user: Optional[Path] = None,
72
+ workflow_dir_local: Path,
73
+ workflow_dir_remote: Optional[Path] = None,
74
74
  first_task_index: Optional[int] = None,
75
75
  last_task_index: Optional[int] = None,
76
76
  logger_name: str,
@@ -94,12 +94,13 @@ async def process_workflow(
94
94
  The workflow to be run
95
95
  dataset:
96
96
  Initial dataset.
97
- workflow_dir:
97
+ workflow_dir_local:
98
98
  Working directory for this run.
99
- workflow_dir_user:
99
+ workflow_dir_remote:
100
100
  Working directory for this run, on the user side. This argument is
101
101
  present for compatibility with the standard backend interface, but
102
- for the `local` backend it cannot be different from `workflow_dir`.
102
+ for the `local` backend it cannot be different from
103
+ `workflow_dir_local`.
103
104
  first_task_index:
104
105
  Positional index of the first task to execute; if `None`, start
105
106
  from `0`.
@@ -137,10 +138,10 @@ async def process_workflow(
137
138
  of the workflow
138
139
  """
139
140
 
140
- if workflow_dir_user and (workflow_dir_user != workflow_dir):
141
+ if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
141
142
  raise NotImplementedError(
142
143
  "Local backend does not support different directories "
143
- f"{workflow_dir=} and {workflow_dir_user=}"
144
+ f"{workflow_dir_local=} and {workflow_dir_remote=}"
144
145
  )
145
146
 
146
147
  # Set values of first_task_index and last_task_index
@@ -155,7 +156,7 @@ async def process_workflow(
155
156
  workflow=workflow,
156
157
  dataset=dataset,
157
158
  logger_name=logger_name,
158
- workflow_dir=workflow_dir,
159
+ workflow_dir_local=workflow_dir_local,
159
160
  first_task_index=first_task_index,
160
161
  last_task_index=last_task_index,
161
162
  )
@@ -96,7 +96,7 @@ def get_local_backend_config(
96
96
  )
97
97
 
98
98
  if wftask_meta and key in wftask_meta:
99
- parallel_tasks_per_job = wftask.meta[key]
99
+ parallel_tasks_per_job = wftask_meta[key]
100
100
  else:
101
101
  if not config_path:
102
102
  settings = Inject(get_settings)
@@ -22,8 +22,8 @@ from ._local_config import get_local_backend_config
22
22
  def _local_submit_setup(
23
23
  *,
24
24
  wftask: WorkflowTaskV2,
25
- workflow_dir: Optional[Path] = None,
26
- workflow_dir_user: Optional[Path] = None,
25
+ workflow_dir_local: Optional[Path] = None,
26
+ workflow_dir_remote: Optional[Path] = None,
27
27
  which_type: Literal["non_parallel", "parallel"],
28
28
  ) -> dict[str, object]:
29
29
  """
@@ -33,9 +33,9 @@ def _local_submit_setup(
33
33
  Arguments:
34
34
  wftask:
35
35
  WorkflowTask for which the configuration is to be assembled
36
- workflow_dir:
36
+ workflow_dir_local:
37
37
  Not used in this function.
38
- workflow_dir_user:
38
+ workflow_dir_remote:
39
39
  Not used in this function.
40
40
 
41
41
  Returns:
@@ -0,0 +1,145 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+
4
+ from ....models.v2 import DatasetV2
5
+ from ....models.v2 import WorkflowV2
6
+ from ...async_wrap import async_wrap
7
+ from ...filenames import SHUTDOWN_FILENAME
8
+ from ...set_start_and_last_task_index import set_start_and_last_task_index
9
+ from ..runner import execute_tasks_v2
10
+ from ._submit_setup import _local_submit_setup
11
+ from .executor import FractalProcessPoolExecutor
12
+
13
+
14
+ def _process_workflow(
15
+ *,
16
+ workflow: WorkflowV2,
17
+ dataset: DatasetV2,
18
+ logger_name: str,
19
+ workflow_dir_local: Path,
20
+ first_task_index: int,
21
+ last_task_index: int,
22
+ ) -> dict:
23
+ """
24
+ Internal processing routine
25
+
26
+ Schedules the workflow using a `FractalProcessPoolExecutor`.
27
+
28
+ Cf.
29
+ [process_workflow][fractal_server.app.runner.v2._local_experimental.process_workflow]
30
+ for the call signature.
31
+ """
32
+
33
+ with FractalProcessPoolExecutor(
34
+ shutdown_file=workflow_dir_local / SHUTDOWN_FILENAME
35
+ ) as executor:
36
+ new_dataset_attributes = execute_tasks_v2(
37
+ wf_task_list=workflow.task_list[
38
+ first_task_index : (last_task_index + 1) # noqa
39
+ ], # noqa
40
+ dataset=dataset,
41
+ executor=executor,
42
+ workflow_dir_local=workflow_dir_local,
43
+ workflow_dir_remote=workflow_dir_local,
44
+ logger_name=logger_name,
45
+ submit_setup_call=_local_submit_setup,
46
+ )
47
+ return new_dataset_attributes
48
+
49
+
50
+ async def process_workflow(
51
+ *,
52
+ workflow: WorkflowV2,
53
+ dataset: DatasetV2,
54
+ workflow_dir_local: Path,
55
+ workflow_dir_remote: Optional[Path] = None,
56
+ first_task_index: Optional[int] = None,
57
+ last_task_index: Optional[int] = None,
58
+ logger_name: str,
59
+ # Slurm-specific
60
+ user_cache_dir: Optional[str] = None,
61
+ slurm_user: Optional[str] = None,
62
+ slurm_account: Optional[str] = None,
63
+ worker_init: Optional[str] = None,
64
+ ) -> dict:
65
+ """
66
+ Run a workflow
67
+
68
+ This function is responsible for running a workflow on some input data,
69
+ saving the output and taking care of any exception raised during the run.
70
+
71
+ NOTE: This is the `local_experimental` backend's public interface,
72
+ which also works as a reference implementation for other backends.
73
+
74
+ Args:
75
+ workflow:
76
+ The workflow to be run
77
+ dataset:
78
+ Initial dataset.
79
+ workflow_dir_local:
80
+ Working directory for this run.
81
+ workflow_dir_remote:
82
+ Working directory for this run, on the user side. This argument is
83
+ present for compatibility with the standard backend interface, but
84
+ for the `local` backend it cannot be different from
85
+ `workflow_dir_local`.
86
+ first_task_index:
87
+ Positional index of the first task to execute; if `None`, start
88
+ from `0`.
89
+ last_task_index:
90
+ Positional index of the last task to execute; if `None`, proceed
91
+ until the last task.
92
+ logger_name: Logger name
93
+ slurm_user:
94
+ Username to impersonate to run the workflow. This argument is
95
+ present for compatibility with the standard backend interface, but
96
+ is ignored in the `local` backend.
97
+ slurm_account:
98
+ SLURM account to use when running the workflow. This argument is
99
+ present for compatibility with the standard backend interface, but
100
+ is ignored in the `local` backend.
101
+ user_cache_dir:
102
+ Cache directory of the user who will run the workflow. This
103
+ argument is present for compatibility with the standard backend
104
+ interface, but is ignored in the `local` backend.
105
+ worker_init:
106
+ Any additional, usually backend specific, information to be passed
107
+ to the backend executor. This argument is present for compatibility
108
+ with the standard backend interface, but is ignored in the `local`
109
+ backend.
110
+
111
+ Raises:
112
+ TaskExecutionError: wrapper for errors raised during tasks' execution
113
+ (positive exit codes).
114
+ JobExecutionError: wrapper for errors raised by the tasks' executors
115
+ (negative exit codes).
116
+
117
+ Returns:
118
+ output_dataset_metadata:
119
+ The updated metadata for the dataset, as returned by the last task
120
+ of the workflow
121
+ """
122
+
123
+ if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
124
+ raise NotImplementedError(
125
+ "LocalExperimental backend does not support different directories "
126
+ f"{workflow_dir_local=} and {workflow_dir_remote=}"
127
+ )
128
+
129
+ # Set values of first_task_index and last_task_index
130
+ num_tasks = len(workflow.task_list)
131
+ first_task_index, last_task_index = set_start_and_last_task_index(
132
+ num_tasks,
133
+ first_task_index=first_task_index,
134
+ last_task_index=last_task_index,
135
+ )
136
+
137
+ new_dataset_attributes = await async_wrap(_process_workflow)(
138
+ workflow=workflow,
139
+ dataset=dataset,
140
+ logger_name=logger_name,
141
+ workflow_dir_local=workflow_dir_local,
142
+ first_task_index=first_task_index,
143
+ last_task_index=last_task_index,
144
+ )
145
+ return new_dataset_attributes
@@ -0,0 +1,108 @@
1
+ """
2
+ Submodule to handle the local-backend configuration for a WorkflowTask
3
+ """
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Literal
7
+ from typing import Optional
8
+
9
+ from pydantic import BaseModel
10
+ from pydantic import Extra
11
+ from pydantic.error_wrappers import ValidationError
12
+
13
+ from .....config import get_settings
14
+ from .....syringe import Inject
15
+ from ....models.v2 import WorkflowTaskV2
16
+
17
+
18
+ class LocalBackendConfigError(ValueError):
19
+ """
20
+ Local-backend configuration error
21
+ """
22
+
23
+ pass
24
+
25
+
26
+ class LocalBackendConfig(BaseModel, extra=Extra.forbid):
27
+ """
28
+ Specifications of the local-backend configuration
29
+
30
+ Attributes:
31
+ parallel_tasks_per_job:
32
+ Maximum number of tasks to be run in parallel as part of a call to
33
+ `FractalProcessPoolExecutor.map`; if `None`, then all tasks will
34
+ start at the same time.
35
+ """
36
+
37
+ parallel_tasks_per_job: Optional[int]
38
+
39
+
40
+ def get_default_local_backend_config():
41
+ """
42
+ Return a default `LocalBackendConfig` configuration object
43
+ """
44
+ return LocalBackendConfig(parallel_tasks_per_job=None)
45
+
46
+
47
+ def get_local_backend_config(
48
+ wftask: WorkflowTaskV2,
49
+ which_type: Literal["non_parallel", "parallel"],
50
+ config_path: Optional[Path] = None,
51
+ ) -> LocalBackendConfig:
52
+ """
53
+ Prepare a `LocalBackendConfig` configuration object
54
+
55
+ The sources for `parallel_tasks_per_job` attributes, starting from the
56
+ highest-priority one, are
57
+
58
+ 1. Properties in `wftask.meta_parallel` or `wftask.meta_non_parallel`
59
+ (depending on `which_type`);
60
+ 2. The general content of the local-backend configuration file;
61
+ 3. The default value (`None`).
62
+
63
+ Arguments:
64
+ wftask:
65
+ WorkflowTaskV2 for which the backend configuration should
66
+ be prepared.
67
+ config_path:
68
+ Path of local-backend configuration file; if `None`, use
69
+ `FRACTAL_LOCAL_CONFIG_FILE` variable from settings.
70
+
71
+ Returns:
72
+ A local-backend configuration object
73
+ """
74
+
75
+ key = "parallel_tasks_per_job"
76
+ default_value = None
77
+
78
+ if which_type == "non_parallel":
79
+ wftask_meta = wftask.meta_non_parallel
80
+ elif which_type == "parallel":
81
+ wftask_meta = wftask.meta_parallel
82
+ else:
83
+ raise ValueError(
84
+ "`get_local_backend_config` received an invalid argument"
85
+ f" {which_type=}."
86
+ )
87
+
88
+ if wftask_meta and key in wftask_meta:
89
+ parallel_tasks_per_job = wftask_meta[key]
90
+ else:
91
+ if not config_path:
92
+ settings = Inject(get_settings)
93
+ config_path = settings.FRACTAL_LOCAL_CONFIG_FILE
94
+ if config_path is None:
95
+ parallel_tasks_per_job = default_value
96
+ else:
97
+ with config_path.open("r") as f:
98
+ env = json.load(f)
99
+ try:
100
+ _ = LocalBackendConfig(**env)
101
+ except ValidationError as e:
102
+ raise LocalBackendConfigError(
103
+ f"Error while loading {config_path=}. "
104
+ f"Original error:\n{str(e)}"
105
+ )
106
+
107
+ parallel_tasks_per_job = env.get(key, default_value)
108
+ return LocalBackendConfig(parallel_tasks_per_job=parallel_tasks_per_job)