fractal-server 1.4.10__py3-none-any.whl → 2.0.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 (126) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/__init__.py +3 -7
  3. fractal_server/app/models/linkuserproject.py +9 -0
  4. fractal_server/app/models/security.py +6 -0
  5. fractal_server/app/models/state.py +1 -1
  6. fractal_server/app/models/v1/__init__.py +11 -0
  7. fractal_server/app/models/{dataset.py → v1/dataset.py} +5 -5
  8. fractal_server/app/models/{job.py → v1/job.py} +5 -5
  9. fractal_server/app/models/{project.py → v1/project.py} +5 -5
  10. fractal_server/app/models/{task.py → v1/task.py} +7 -2
  11. fractal_server/app/models/{workflow.py → v1/workflow.py} +5 -5
  12. fractal_server/app/models/v2/__init__.py +20 -0
  13. fractal_server/app/models/v2/dataset.py +55 -0
  14. fractal_server/app/models/v2/job.py +51 -0
  15. fractal_server/app/models/v2/project.py +31 -0
  16. fractal_server/app/models/v2/task.py +93 -0
  17. fractal_server/app/models/v2/workflow.py +43 -0
  18. fractal_server/app/models/v2/workflowtask.py +90 -0
  19. fractal_server/app/routes/{admin.py → admin/v1.py} +42 -42
  20. fractal_server/app/routes/admin/v2.py +274 -0
  21. fractal_server/app/routes/api/v1/__init__.py +7 -7
  22. fractal_server/app/routes/api/v1/_aux_functions.py +2 -2
  23. fractal_server/app/routes/api/v1/dataset.py +37 -37
  24. fractal_server/app/routes/api/v1/job.py +14 -14
  25. fractal_server/app/routes/api/v1/project.py +23 -21
  26. fractal_server/app/routes/api/v1/task.py +24 -14
  27. fractal_server/app/routes/api/v1/task_collection.py +16 -14
  28. fractal_server/app/routes/api/v1/workflow.py +24 -24
  29. fractal_server/app/routes/api/v1/workflowtask.py +10 -10
  30. fractal_server/app/routes/api/v2/__init__.py +28 -0
  31. fractal_server/app/routes/api/v2/_aux_functions.py +497 -0
  32. fractal_server/app/routes/api/v2/dataset.py +309 -0
  33. fractal_server/app/routes/api/v2/images.py +207 -0
  34. fractal_server/app/routes/api/v2/job.py +200 -0
  35. fractal_server/app/routes/api/v2/project.py +202 -0
  36. fractal_server/app/routes/api/v2/submit.py +220 -0
  37. fractal_server/app/routes/api/v2/task.py +222 -0
  38. fractal_server/app/routes/api/v2/task_collection.py +229 -0
  39. fractal_server/app/routes/api/v2/workflow.py +397 -0
  40. fractal_server/app/routes/api/v2/workflowtask.py +269 -0
  41. fractal_server/app/routes/aux/_job.py +1 -1
  42. fractal_server/app/runner/async_wrap.py +27 -0
  43. fractal_server/app/runner/components.py +5 -0
  44. fractal_server/app/runner/exceptions.py +129 -0
  45. fractal_server/app/runner/executors/slurm/__init__.py +3 -0
  46. fractal_server/app/runner/{_slurm → executors/slurm}/_batching.py +1 -1
  47. fractal_server/app/runner/{_slurm → executors/slurm}/_check_jobs_status.py +1 -1
  48. fractal_server/app/runner/{_slurm → executors/slurm}/_executor_wait_thread.py +1 -1
  49. fractal_server/app/runner/{_slurm → executors/slurm}/_slurm_config.py +3 -152
  50. fractal_server/app/runner/{_slurm → executors/slurm}/_subprocess_run_as_user.py +1 -1
  51. fractal_server/app/runner/{_slurm → executors/slurm}/executor.py +32 -19
  52. fractal_server/app/runner/filenames.py +6 -0
  53. fractal_server/app/runner/set_start_and_last_task_index.py +39 -0
  54. fractal_server/app/runner/task_files.py +103 -0
  55. fractal_server/app/runner/{__init__.py → v1/__init__.py} +22 -20
  56. fractal_server/app/runner/{_common.py → v1/_common.py} +13 -120
  57. fractal_server/app/runner/{_local → v1/_local}/__init__.py +5 -5
  58. fractal_server/app/runner/{_local → v1/_local}/_local_config.py +6 -7
  59. fractal_server/app/runner/{_local → v1/_local}/_submit_setup.py +1 -5
  60. fractal_server/app/runner/v1/_slurm/__init__.py +310 -0
  61. fractal_server/app/runner/{_slurm → v1/_slurm}/_submit_setup.py +3 -9
  62. fractal_server/app/runner/v1/_slurm/get_slurm_config.py +163 -0
  63. fractal_server/app/runner/v1/common.py +117 -0
  64. fractal_server/app/runner/{handle_failed_job.py → v1/handle_failed_job.py} +8 -8
  65. fractal_server/app/runner/v2/__init__.py +336 -0
  66. fractal_server/app/runner/v2/_local/__init__.py +167 -0
  67. fractal_server/app/runner/v2/_local/_local_config.py +118 -0
  68. fractal_server/app/runner/v2/_local/_submit_setup.py +52 -0
  69. fractal_server/app/runner/v2/_local/executor.py +100 -0
  70. fractal_server/app/runner/{_slurm → v2/_slurm}/__init__.py +34 -45
  71. fractal_server/app/runner/v2/_slurm/_submit_setup.py +83 -0
  72. fractal_server/app/runner/v2/_slurm/get_slurm_config.py +179 -0
  73. fractal_server/app/runner/v2/deduplicate_list.py +22 -0
  74. fractal_server/app/runner/v2/handle_failed_job.py +156 -0
  75. fractal_server/app/runner/v2/merge_outputs.py +38 -0
  76. fractal_server/app/runner/v2/runner.py +267 -0
  77. fractal_server/app/runner/v2/runner_functions.py +341 -0
  78. fractal_server/app/runner/v2/runner_functions_low_level.py +134 -0
  79. fractal_server/app/runner/v2/task_interface.py +43 -0
  80. fractal_server/app/runner/v2/v1_compat.py +21 -0
  81. fractal_server/app/schemas/__init__.py +4 -42
  82. fractal_server/app/schemas/v1/__init__.py +42 -0
  83. fractal_server/app/schemas/{applyworkflow.py → v1/applyworkflow.py} +18 -18
  84. fractal_server/app/schemas/{dataset.py → v1/dataset.py} +30 -30
  85. fractal_server/app/schemas/{dumps.py → v1/dumps.py} +8 -8
  86. fractal_server/app/schemas/{manifest.py → v1/manifest.py} +5 -5
  87. fractal_server/app/schemas/{project.py → v1/project.py} +9 -9
  88. fractal_server/app/schemas/{task.py → v1/task.py} +12 -12
  89. fractal_server/app/schemas/{task_collection.py → v1/task_collection.py} +7 -7
  90. fractal_server/app/schemas/{workflow.py → v1/workflow.py} +38 -38
  91. fractal_server/app/schemas/v2/__init__.py +34 -0
  92. fractal_server/app/schemas/v2/dataset.py +89 -0
  93. fractal_server/app/schemas/v2/dumps.py +87 -0
  94. fractal_server/app/schemas/v2/job.py +114 -0
  95. fractal_server/app/schemas/v2/manifest.py +159 -0
  96. fractal_server/app/schemas/v2/project.py +37 -0
  97. fractal_server/app/schemas/v2/task.py +120 -0
  98. fractal_server/app/schemas/v2/task_collection.py +105 -0
  99. fractal_server/app/schemas/v2/workflow.py +79 -0
  100. fractal_server/app/schemas/v2/workflowtask.py +119 -0
  101. fractal_server/config.py +5 -4
  102. fractal_server/images/__init__.py +2 -0
  103. fractal_server/images/models.py +50 -0
  104. fractal_server/images/tools.py +85 -0
  105. fractal_server/main.py +11 -3
  106. fractal_server/migrations/env.py +0 -2
  107. fractal_server/migrations/versions/d71e732236cd_v2.py +239 -0
  108. fractal_server/tasks/__init__.py +0 -5
  109. fractal_server/tasks/endpoint_operations.py +13 -19
  110. fractal_server/tasks/utils.py +35 -0
  111. fractal_server/tasks/{_TaskCollectPip.py → v1/_TaskCollectPip.py} +3 -3
  112. fractal_server/tasks/{background_operations.py → v1/background_operations.py} +18 -50
  113. fractal_server/tasks/v1/get_collection_data.py +14 -0
  114. fractal_server/tasks/v2/_TaskCollectPip.py +103 -0
  115. fractal_server/tasks/v2/background_operations.py +381 -0
  116. fractal_server/tasks/v2/get_collection_data.py +14 -0
  117. {fractal_server-1.4.10.dist-info → fractal_server-2.0.0a1.dist-info}/METADATA +1 -1
  118. fractal_server-2.0.0a1.dist-info/RECORD +160 -0
  119. fractal_server/app/runner/_slurm/.gitignore +0 -2
  120. fractal_server/app/runner/common.py +0 -311
  121. fractal_server-1.4.10.dist-info/RECORD +0 -98
  122. /fractal_server/app/runner/{_slurm → executors/slurm}/remote.py +0 -0
  123. /fractal_server/app/runner/{_local → v1/_local}/executor.py +0 -0
  124. {fractal_server-1.4.10.dist-info → fractal_server-2.0.0a1.dist-info}/LICENSE +0 -0
  125. {fractal_server-1.4.10.dist-info → fractal_server-2.0.0a1.dist-info}/WHEEL +0 -0
  126. {fractal_server-1.4.10.dist-info → fractal_server-2.0.0a1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,38 @@
1
+ from copy import copy
2
+
3
+ from fractal_server.app.runner.v2.deduplicate_list import deduplicate_list
4
+ from fractal_server.app.runner.v2.task_interface import TaskOutput
5
+
6
+
7
+ def merge_outputs(task_outputs: list[TaskOutput]) -> TaskOutput:
8
+
9
+ final_image_list_updates = []
10
+ final_image_list_removals = []
11
+ last_new_filters = None
12
+
13
+ for ind, task_output in enumerate(task_outputs):
14
+
15
+ final_image_list_updates.extend(task_output.image_list_updates)
16
+ final_image_list_removals.extend(task_output.image_list_removals)
17
+
18
+ # Check that all filters are the same
19
+ current_new_filters = task_output.filters
20
+ if ind == 0:
21
+ last_new_filters = copy(current_new_filters)
22
+ if current_new_filters != last_new_filters:
23
+ raise ValueError(f"{current_new_filters=} but {last_new_filters=}")
24
+ last_new_filters = copy(current_new_filters)
25
+
26
+ final_image_list_updates = deduplicate_list(final_image_list_updates)
27
+
28
+ additional_args = {}
29
+ if last_new_filters is not None:
30
+ additional_args["filters"] = last_new_filters
31
+
32
+ final_output = TaskOutput(
33
+ image_list_updates=final_image_list_updates,
34
+ image_list_removals=final_image_list_removals,
35
+ **additional_args,
36
+ )
37
+
38
+ return final_output
@@ -0,0 +1,267 @@
1
+ import json
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ from copy import copy
4
+ from copy import deepcopy
5
+ from pathlib import Path
6
+ from typing import Callable
7
+ from typing import Optional
8
+
9
+ from ....images import Filters
10
+ from ....images import SingleImage
11
+ from ....images.tools import filter_image_list
12
+ from ....images.tools import find_image_by_path
13
+ from ....images.tools import match_filter
14
+ from ..filenames import FILTERS_FILENAME
15
+ from ..filenames import HISTORY_FILENAME
16
+ from ..filenames import IMAGES_FILENAME
17
+ from .runner_functions import no_op_submit_setup_call
18
+ from .runner_functions import run_v1_task_parallel
19
+ from .runner_functions import run_v2_task_compound
20
+ from .runner_functions import run_v2_task_non_parallel
21
+ from .runner_functions import run_v2_task_parallel
22
+ from fractal_server.app.models.v2 import DatasetV2
23
+ from fractal_server.app.models.v2 import WorkflowTaskV2
24
+ from fractal_server.app.schemas.v2.dataset import _DatasetHistoryItemV2
25
+ from fractal_server.app.schemas.v2.workflowtask import WorkflowTaskStatusTypeV2
26
+
27
+ # FIXME: define RESERVED_ARGUMENTS = [", ...]
28
+
29
+
30
+ def execute_tasks_v2(
31
+ wf_task_list: list[WorkflowTaskV2],
32
+ dataset: DatasetV2,
33
+ executor: ThreadPoolExecutor,
34
+ workflow_dir: Path,
35
+ workflow_dir_user: Optional[Path] = None,
36
+ logger_name: Optional[str] = None,
37
+ submit_setup_call: Callable = no_op_submit_setup_call,
38
+ ) -> DatasetV2:
39
+
40
+ if not workflow_dir.exists(): # FIXME: this should have already happened
41
+ workflow_dir.mkdir()
42
+
43
+ # Initialize local dataset attributes
44
+ zarr_dir = dataset.zarr_dir
45
+ tmp_images = deepcopy(dataset.images)
46
+ tmp_filters = deepcopy(dataset.filters)
47
+ tmp_history = []
48
+
49
+ for wftask in wf_task_list:
50
+ task = wftask.task
51
+
52
+ # PRE TASK EXECUTION
53
+
54
+ # Get filtered images
55
+ pre_filters = dict(
56
+ types=copy(tmp_filters["types"]),
57
+ attributes=copy(tmp_filters["attributes"]),
58
+ )
59
+ pre_filters["types"].update(wftask.input_filters["types"])
60
+ pre_filters["attributes"].update(wftask.input_filters["attributes"])
61
+ filtered_images = filter_image_list(
62
+ images=tmp_images,
63
+ filters=Filters(**pre_filters),
64
+ )
65
+ # Verify that filtered images comply with task input_types
66
+ for image in filtered_images:
67
+ if not match_filter(image, Filters(types=task.input_types)):
68
+ raise ValueError(
69
+ f"Filtered images include {image.dict()}, which does "
70
+ f"not comply with {task.input_types=}."
71
+ )
72
+
73
+ # TASK EXECUTION (V2)
74
+ if not wftask.is_legacy_task:
75
+ if task.type == "non_parallel":
76
+ current_task_output = run_v2_task_non_parallel(
77
+ images=filtered_images,
78
+ zarr_dir=zarr_dir,
79
+ wftask=wftask,
80
+ task=wftask.task,
81
+ workflow_dir=workflow_dir,
82
+ workflow_dir_user=workflow_dir_user,
83
+ executor=executor,
84
+ logger_name=logger_name,
85
+ submit_setup_call=submit_setup_call,
86
+ )
87
+ elif task.type == "parallel":
88
+ current_task_output = run_v2_task_parallel(
89
+ images=filtered_images,
90
+ wftask=wftask,
91
+ task=wftask.task,
92
+ workflow_dir=workflow_dir,
93
+ workflow_dir_user=workflow_dir_user,
94
+ executor=executor,
95
+ logger_name=logger_name,
96
+ submit_setup_call=submit_setup_call,
97
+ )
98
+ elif task.type == "compound":
99
+ current_task_output = run_v2_task_compound(
100
+ images=filtered_images,
101
+ zarr_dir=zarr_dir,
102
+ wftask=wftask,
103
+ task=wftask.task,
104
+ workflow_dir=workflow_dir,
105
+ workflow_dir_user=workflow_dir_user,
106
+ executor=executor,
107
+ logger_name=logger_name,
108
+ submit_setup_call=submit_setup_call,
109
+ )
110
+ else:
111
+ raise ValueError(f"Invalid {task.type=}.")
112
+ # TASK EXECUTION (V1)
113
+ else:
114
+ current_task_output = run_v1_task_parallel(
115
+ images=filtered_images,
116
+ wftask=wftask,
117
+ task_legacy=wftask.task_legacy,
118
+ executor=executor,
119
+ logger_name=logger_name,
120
+ submit_setup_call=submit_setup_call,
121
+ )
122
+
123
+ # POST TASK EXECUTION
124
+
125
+ # Update image list
126
+ current_task_output.check_paths_are_unique()
127
+ for image_obj in current_task_output.image_list_updates:
128
+ image = image_obj.dict()
129
+ # Edit existing image
130
+ if image["path"] in [_image["path"] for _image in tmp_images]:
131
+ if (
132
+ image["origin"] is not None
133
+ and image["origin"] != image["path"]
134
+ ):
135
+ raise ValueError(
136
+ f"Trying to edit an image with {image['path']=} "
137
+ f"and {image['origin']=}."
138
+ )
139
+ image_search = find_image_by_path(
140
+ images=tmp_images,
141
+ path=image["path"],
142
+ )
143
+ if image_search is None:
144
+ raise ValueError(
145
+ f"Image with path {image['path']} not found, while "
146
+ "updating image list."
147
+ )
148
+ original_img = image_search["image"]
149
+ original_index = image_search["index"]
150
+ updated_attributes = copy(original_img["attributes"])
151
+ updated_types = copy(original_img["types"])
152
+
153
+ # Update image attributes/types with task output and manifest
154
+ updated_attributes.update(image["attributes"])
155
+ updated_types.update(image["types"])
156
+ updated_types.update(task.output_types)
157
+
158
+ # Update image in the dataset image list
159
+ tmp_images[original_index]["attributes"] = updated_attributes
160
+ tmp_images[original_index]["types"] = updated_types
161
+ # Add new image
162
+ else:
163
+ # Check that image['path'] is relative to zarr_dir
164
+ if not image["path"].startswith(zarr_dir):
165
+ raise ValueError(
166
+ f"{zarr_dir} is not a parent directory of "
167
+ f"{image['path']}"
168
+ )
169
+ # Propagate attributes and types from `origin` (if any)
170
+ updated_attributes = {}
171
+ updated_types = {}
172
+ if image["origin"] is not None:
173
+ image_search = find_image_by_path(
174
+ images=tmp_images,
175
+ path=image["origin"],
176
+ )
177
+ if image_search is not None:
178
+ original_img = image_search["image"]
179
+ updated_attributes = copy(original_img["attributes"])
180
+ updated_types = copy(original_img["types"])
181
+ # Update image attributes/types with task output and manifest
182
+ updated_attributes.update(image["attributes"])
183
+ updated_types.update(image["types"])
184
+ updated_types.update(task.output_types)
185
+ new_image = dict(
186
+ path=image["path"],
187
+ origin=image["origin"],
188
+ attributes=updated_attributes,
189
+ types=updated_types,
190
+ )
191
+ # Validate new image
192
+ SingleImage(**new_image)
193
+ # Add image into the dataset image list
194
+ tmp_images.append(new_image)
195
+
196
+ # Remove images from tmp_images
197
+ for image_path in current_task_output.image_list_removals:
198
+ image_search = find_image_by_path(
199
+ images=tmp_images, path=image_path
200
+ )
201
+ if image_search is None:
202
+ raise ValueError(
203
+ f"Cannot remove missing image with path {image_path=}"
204
+ )
205
+ else:
206
+ tmp_images.pop(image_search["index"])
207
+
208
+ # Update filters.attributes:
209
+ # current + (task_output: not really, in current examples..)
210
+ if current_task_output.filters is not None:
211
+ tmp_filters["attributes"].update(
212
+ current_task_output.filters.attributes
213
+ )
214
+
215
+ # Update filters.types: current + (task_output + task_manifest)
216
+ if wftask.is_legacy_task:
217
+ types_from_manifest = {}
218
+ else:
219
+ types_from_manifest = task.output_types
220
+ if current_task_output.filters is not None:
221
+ types_from_task = current_task_output.filters.types
222
+ else:
223
+ types_from_task = {}
224
+ # Check that key sets are disjoint
225
+ set_types_from_manifest = set(types_from_manifest.keys())
226
+ set_types_from_task = set(types_from_task.keys())
227
+ if not set_types_from_manifest.isdisjoint(set_types_from_task):
228
+ overlap = set_types_from_manifest.intersection(set_types_from_task)
229
+ raise ValueError(
230
+ "Both task and task manifest did set the same"
231
+ f"output type. Overlapping keys: {overlap}."
232
+ )
233
+ # Update filters.types
234
+ tmp_filters["types"].update(types_from_manifest)
235
+ tmp_filters["types"].update(types_from_task)
236
+
237
+ # Update history (based on _DatasetHistoryItemV2)
238
+ history_item = _DatasetHistoryItemV2(
239
+ workflowtask=wftask,
240
+ status=WorkflowTaskStatusTypeV2.DONE,
241
+ parallelization=dict(
242
+ # task_type=wftask.task.type, # FIXME: breaks for V1 tasks
243
+ # component_list=fil, #FIXME
244
+ ),
245
+ ).dict()
246
+ tmp_history.append(history_item)
247
+
248
+ # Write current dataset attributes (history, images, filters) into
249
+ # temporary files which can be used (1) to retrieve the latest state
250
+ # when the job fails, (2) from within endpoints that need up-to-date
251
+ # information
252
+ with open(workflow_dir / HISTORY_FILENAME, "w") as f:
253
+ json.dump(tmp_history, f, indent=2)
254
+ with open(workflow_dir / FILTERS_FILENAME, "w") as f:
255
+ json.dump(tmp_filters, f, indent=2)
256
+ with open(workflow_dir / IMAGES_FILENAME, "w") as f:
257
+ json.dump(tmp_images, f, indent=2)
258
+
259
+ # NOTE: tmp_history only contains the newly-added history items (to be
260
+ # appended to the original history), while tmp_filters and tmp_images
261
+ # represent the new attributes (to replace the original ones)
262
+ result = dict(
263
+ history=tmp_history,
264
+ filters=tmp_filters,
265
+ images=tmp_images,
266
+ )
267
+ return result
@@ -0,0 +1,341 @@
1
+ import functools
2
+ import logging
3
+ import traceback
4
+ from concurrent.futures import Executor
5
+ from pathlib import Path
6
+ from typing import Any
7
+ from typing import Callable
8
+ from typing import Literal
9
+ from typing import Optional
10
+
11
+ from .deduplicate_list import deduplicate_list
12
+ from .merge_outputs import merge_outputs
13
+ from .runner_functions_low_level import run_single_task
14
+ from .task_interface import InitTaskOutput
15
+ from .task_interface import TaskOutput
16
+ from .v1_compat import convert_v2_args_into_v1
17
+ from fractal_server.app.models.v1 import Task as TaskV1
18
+ from fractal_server.app.models.v2 import TaskV2
19
+ from fractal_server.app.models.v2 import WorkflowTaskV2
20
+ from fractal_server.app.runner.components import _COMPONENT_KEY_
21
+ from fractal_server.app.runner.components import _index_to_component
22
+
23
+
24
+ __all__ = [
25
+ "run_v2_task_non_parallel",
26
+ "run_v2_task_parallel",
27
+ "run_v2_task_compound",
28
+ "run_v1_task_parallel",
29
+ ]
30
+
31
+ MAX_PARALLELIZATION_LIST_SIZE = 20_000
32
+
33
+
34
+ def no_op_submit_setup_call(
35
+ *,
36
+ wftask: WorkflowTaskV2,
37
+ workflow_dir: Path,
38
+ workflow_dir_user: Path,
39
+ which_type: Literal["non_parallel", "parallel"],
40
+ ) -> dict:
41
+ """
42
+ Default (no-operation) interface of submit_setup_call in V2.
43
+ """
44
+ return {}
45
+
46
+
47
+ # Backend-specific configuration
48
+ def _get_executor_options(
49
+ *,
50
+ wftask: WorkflowTaskV2,
51
+ workflow_dir: Path,
52
+ workflow_dir_user: Path,
53
+ submit_setup_call: Callable,
54
+ which_type: Literal["non_parallel", "parallel"],
55
+ ) -> dict:
56
+ try:
57
+ options = submit_setup_call(
58
+ wftask=wftask,
59
+ workflow_dir=workflow_dir,
60
+ workflow_dir_user=workflow_dir_user,
61
+ which_type=which_type,
62
+ )
63
+ except Exception as e:
64
+ tb = "".join(traceback.format_tb(e.__traceback__))
65
+ raise RuntimeError(
66
+ f"{type(e)} error in {submit_setup_call=}\n"
67
+ f"Original traceback:\n{tb}"
68
+ )
69
+ return options
70
+
71
+
72
+ def _check_parallelization_list_size(my_list):
73
+ if len(my_list) > MAX_PARALLELIZATION_LIST_SIZE:
74
+ raise ValueError(
75
+ "Too many parallelization items.\n"
76
+ f" {len(my_list)}\n"
77
+ f" {MAX_PARALLELIZATION_LIST_SIZE=}\n"
78
+ )
79
+
80
+
81
+ def run_v2_task_non_parallel(
82
+ *,
83
+ images: list[dict[str, Any]],
84
+ zarr_dir: str,
85
+ task: TaskV2,
86
+ wftask: WorkflowTaskV2,
87
+ workflow_dir: Path,
88
+ workflow_dir_user: Optional[Path] = None,
89
+ executor: Executor,
90
+ logger_name: Optional[str] = None,
91
+ submit_setup_call: Callable = no_op_submit_setup_call,
92
+ ) -> TaskOutput:
93
+ """
94
+ This runs server-side (see `executor` argument)
95
+ """
96
+
97
+ if workflow_dir_user is None:
98
+ workflow_dir_user = workflow_dir
99
+ logging.warning(
100
+ "In `run_single_task`, workflow_dir_user=None. Is this right?"
101
+ )
102
+ workflow_dir_user = workflow_dir
103
+
104
+ executor_options = _get_executor_options(
105
+ wftask=wftask,
106
+ workflow_dir=workflow_dir,
107
+ workflow_dir_user=workflow_dir_user,
108
+ submit_setup_call=submit_setup_call,
109
+ which_type="non_parallel",
110
+ )
111
+
112
+ function_kwargs = dict(
113
+ paths=[image["path"] for image in images],
114
+ zarr_dir=zarr_dir,
115
+ **(wftask.args_non_parallel or {}),
116
+ )
117
+ future = executor.submit(
118
+ functools.partial(
119
+ run_single_task,
120
+ wftask=wftask,
121
+ command=task.command_non_parallel,
122
+ workflow_dir=workflow_dir,
123
+ workflow_dir_user=workflow_dir_user,
124
+ ),
125
+ function_kwargs,
126
+ **executor_options,
127
+ )
128
+ output = future.result()
129
+ # FIXME V2: handle validation errors
130
+ if output is None:
131
+ return TaskOutput()
132
+ else:
133
+ validated_output = TaskOutput(**output)
134
+ return validated_output
135
+
136
+
137
+ def run_v2_task_parallel(
138
+ *,
139
+ images: list[dict[str, Any]],
140
+ task: TaskV2,
141
+ wftask: WorkflowTaskV2,
142
+ executor: Executor,
143
+ workflow_dir: Path,
144
+ workflow_dir_user: Optional[Path] = None,
145
+ logger_name: Optional[str] = None,
146
+ submit_setup_call: Callable = no_op_submit_setup_call,
147
+ ) -> TaskOutput:
148
+
149
+ _check_parallelization_list_size(images)
150
+
151
+ executor_options = _get_executor_options(
152
+ wftask=wftask,
153
+ workflow_dir=workflow_dir,
154
+ workflow_dir_user=workflow_dir_user,
155
+ submit_setup_call=submit_setup_call,
156
+ which_type="parallel",
157
+ )
158
+
159
+ list_function_kwargs = []
160
+ for ind, image in enumerate(images):
161
+ list_function_kwargs.append(
162
+ dict(
163
+ path=image["path"],
164
+ **(wftask.args_parallel or {}),
165
+ ),
166
+ )
167
+ list_function_kwargs[-1][_COMPONENT_KEY_] = _index_to_component(ind)
168
+
169
+ results_iterator = executor.map(
170
+ functools.partial(
171
+ run_single_task,
172
+ wftask=wftask,
173
+ command=task.command_parallel,
174
+ workflow_dir=workflow_dir,
175
+ workflow_dir_user=workflow_dir_user,
176
+ ),
177
+ list_function_kwargs,
178
+ **executor_options,
179
+ )
180
+ # Explicitly iterate over the whole list, so that all futures are waited
181
+ outputs = list(results_iterator)
182
+
183
+ # Validate all non-None outputs
184
+ for ind, output in enumerate(outputs):
185
+ if output is None:
186
+ outputs[ind] = TaskOutput()
187
+ else:
188
+ # FIXME: improve handling of validation errors
189
+ validated_output = TaskOutput(**output)
190
+ outputs[ind] = validated_output
191
+
192
+ merged_output = merge_outputs(outputs)
193
+ return merged_output
194
+
195
+
196
+ def run_v2_task_compound(
197
+ *,
198
+ images: list[dict[str, Any]],
199
+ zarr_dir: str,
200
+ task: TaskV2,
201
+ wftask: WorkflowTaskV2,
202
+ executor: Executor,
203
+ workflow_dir: Path,
204
+ workflow_dir_user: Optional[Path] = None,
205
+ logger_name: Optional[str] = None,
206
+ submit_setup_call: Callable = no_op_submit_setup_call,
207
+ ) -> TaskOutput:
208
+
209
+ executor_options_init = _get_executor_options(
210
+ wftask=wftask,
211
+ workflow_dir=workflow_dir,
212
+ workflow_dir_user=workflow_dir_user,
213
+ submit_setup_call=submit_setup_call,
214
+ which_type="non_parallel",
215
+ )
216
+ executor_options_compute = _get_executor_options(
217
+ wftask=wftask,
218
+ workflow_dir=workflow_dir,
219
+ workflow_dir_user=workflow_dir_user,
220
+ submit_setup_call=submit_setup_call,
221
+ which_type="parallel",
222
+ )
223
+
224
+ # 3/A: non-parallel init task
225
+ function_kwargs = dict(
226
+ paths=[image["path"] for image in images],
227
+ zarr_dir=zarr_dir,
228
+ **(wftask.args_non_parallel or {}),
229
+ )
230
+ future = executor.submit(
231
+ functools.partial(
232
+ run_single_task,
233
+ wftask=wftask,
234
+ command=task.command_non_parallel,
235
+ workflow_dir=workflow_dir,
236
+ workflow_dir_user=workflow_dir_user,
237
+ ),
238
+ function_kwargs,
239
+ **executor_options_init,
240
+ )
241
+ output = future.result()
242
+ if output is None:
243
+ init_task_output = InitTaskOutput()
244
+ else:
245
+ init_task_output = InitTaskOutput(**output)
246
+ parallelization_list = init_task_output.parallelization_list
247
+ parallelization_list = deduplicate_list(parallelization_list)
248
+
249
+ # 3/B: parallel part of a compound task
250
+ _check_parallelization_list_size(parallelization_list)
251
+
252
+ list_function_kwargs = []
253
+ for ind, parallelization_item in enumerate(parallelization_list):
254
+ list_function_kwargs.append(
255
+ dict(
256
+ path=parallelization_item.path,
257
+ init_args=parallelization_item.init_args,
258
+ **(wftask.args_parallel or {}),
259
+ ),
260
+ )
261
+ list_function_kwargs[-1][_COMPONENT_KEY_] = _index_to_component(ind)
262
+
263
+ results_iterator = executor.map(
264
+ functools.partial(
265
+ run_single_task,
266
+ wftask=wftask,
267
+ command=task.command_parallel,
268
+ workflow_dir=workflow_dir,
269
+ workflow_dir_user=workflow_dir_user,
270
+ ),
271
+ list_function_kwargs,
272
+ **executor_options_compute,
273
+ )
274
+ # Explicitly iterate over the whole list, so that all futures are waited
275
+ outputs = list(results_iterator)
276
+
277
+ # Validate all non-None outputs
278
+ for ind, output in enumerate(outputs):
279
+ if output is None:
280
+ outputs[ind] = TaskOutput()
281
+ else:
282
+ # FIXME: improve handling of validation errors
283
+ validated_output = TaskOutput(**output)
284
+ outputs[ind] = validated_output
285
+
286
+ merged_output = merge_outputs(outputs)
287
+ return merged_output
288
+
289
+
290
+ def run_v1_task_parallel(
291
+ *,
292
+ images: list[dict[str, Any]],
293
+ task_legacy: TaskV1,
294
+ wftask: WorkflowTaskV2,
295
+ executor: Executor,
296
+ workflow_dir: Path,
297
+ workflow_dir_user: Optional[Path] = None,
298
+ logger_name: Optional[str] = None,
299
+ submit_setup_call: Callable = no_op_submit_setup_call,
300
+ ) -> TaskOutput:
301
+
302
+ _check_parallelization_list_size(images)
303
+
304
+ executor_options = _get_executor_options(
305
+ wftask=wftask,
306
+ workflow_dir=workflow_dir,
307
+ workflow_dir_user=workflow_dir_user,
308
+ submit_setup_call=submit_setup_call,
309
+ which_type="parallel",
310
+ )
311
+
312
+ list_function_kwargs = []
313
+ for ind, image in enumerate(images):
314
+ list_function_kwargs.append(
315
+ convert_v2_args_into_v1(
316
+ dict(
317
+ path=image["path"],
318
+ **(wftask.args_parallel or {}),
319
+ )
320
+ ),
321
+ )
322
+ list_function_kwargs[-1][_COMPONENT_KEY_] = _index_to_component(ind)
323
+
324
+ results_iterator = executor.map(
325
+ functools.partial(
326
+ run_single_task,
327
+ wftask=wftask,
328
+ command=task_legacy.command,
329
+ workflow_dir=workflow_dir,
330
+ workflow_dir_user=workflow_dir_user,
331
+ is_task_v1=True,
332
+ ),
333
+ list_function_kwargs,
334
+ **executor_options,
335
+ )
336
+ # Explicitly iterate over the whole list, so that all futures are waited
337
+ list(results_iterator)
338
+
339
+ # Ignore any output metadata for V1 tasks, and return an empty object
340
+ out = TaskOutput()
341
+ return out