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.
- fractal_server/__init__.py +1 -1
- fractal_server/app/models/__init__.py +3 -7
- fractal_server/app/models/linkuserproject.py +9 -0
- fractal_server/app/models/security.py +6 -0
- fractal_server/app/models/state.py +1 -1
- fractal_server/app/models/v1/__init__.py +11 -0
- fractal_server/app/models/{dataset.py → v1/dataset.py} +5 -5
- fractal_server/app/models/{job.py → v1/job.py} +5 -5
- fractal_server/app/models/{project.py → v1/project.py} +5 -5
- fractal_server/app/models/{task.py → v1/task.py} +7 -2
- fractal_server/app/models/{workflow.py → v1/workflow.py} +5 -5
- fractal_server/app/models/v2/__init__.py +20 -0
- fractal_server/app/models/v2/dataset.py +55 -0
- fractal_server/app/models/v2/job.py +51 -0
- fractal_server/app/models/v2/project.py +31 -0
- fractal_server/app/models/v2/task.py +93 -0
- fractal_server/app/models/v2/workflow.py +43 -0
- fractal_server/app/models/v2/workflowtask.py +90 -0
- fractal_server/app/routes/{admin.py → admin/v1.py} +42 -42
- fractal_server/app/routes/admin/v2.py +274 -0
- fractal_server/app/routes/api/v1/__init__.py +7 -7
- fractal_server/app/routes/api/v1/_aux_functions.py +2 -2
- fractal_server/app/routes/api/v1/dataset.py +37 -37
- fractal_server/app/routes/api/v1/job.py +14 -14
- fractal_server/app/routes/api/v1/project.py +23 -21
- fractal_server/app/routes/api/v1/task.py +24 -14
- fractal_server/app/routes/api/v1/task_collection.py +16 -14
- fractal_server/app/routes/api/v1/workflow.py +24 -24
- fractal_server/app/routes/api/v1/workflowtask.py +10 -10
- fractal_server/app/routes/api/v2/__init__.py +28 -0
- fractal_server/app/routes/api/v2/_aux_functions.py +497 -0
- fractal_server/app/routes/api/v2/dataset.py +309 -0
- fractal_server/app/routes/api/v2/images.py +207 -0
- fractal_server/app/routes/api/v2/job.py +200 -0
- fractal_server/app/routes/api/v2/project.py +202 -0
- fractal_server/app/routes/api/v2/submit.py +220 -0
- fractal_server/app/routes/api/v2/task.py +222 -0
- fractal_server/app/routes/api/v2/task_collection.py +229 -0
- fractal_server/app/routes/api/v2/workflow.py +397 -0
- fractal_server/app/routes/api/v2/workflowtask.py +269 -0
- fractal_server/app/routes/aux/_job.py +1 -1
- fractal_server/app/runner/async_wrap.py +27 -0
- fractal_server/app/runner/components.py +5 -0
- fractal_server/app/runner/exceptions.py +129 -0
- fractal_server/app/runner/executors/slurm/__init__.py +3 -0
- fractal_server/app/runner/{_slurm → executors/slurm}/_batching.py +1 -1
- fractal_server/app/runner/{_slurm → executors/slurm}/_check_jobs_status.py +1 -1
- fractal_server/app/runner/{_slurm → executors/slurm}/_executor_wait_thread.py +1 -1
- fractal_server/app/runner/{_slurm → executors/slurm}/_slurm_config.py +3 -152
- fractal_server/app/runner/{_slurm → executors/slurm}/_subprocess_run_as_user.py +1 -1
- fractal_server/app/runner/{_slurm → executors/slurm}/executor.py +32 -19
- fractal_server/app/runner/filenames.py +6 -0
- fractal_server/app/runner/set_start_and_last_task_index.py +39 -0
- fractal_server/app/runner/task_files.py +103 -0
- fractal_server/app/runner/{__init__.py → v1/__init__.py} +22 -20
- fractal_server/app/runner/{_common.py → v1/_common.py} +13 -120
- fractal_server/app/runner/{_local → v1/_local}/__init__.py +5 -5
- fractal_server/app/runner/{_local → v1/_local}/_local_config.py +6 -7
- fractal_server/app/runner/{_local → v1/_local}/_submit_setup.py +1 -5
- fractal_server/app/runner/v1/_slurm/__init__.py +310 -0
- fractal_server/app/runner/{_slurm → v1/_slurm}/_submit_setup.py +3 -9
- fractal_server/app/runner/v1/_slurm/get_slurm_config.py +163 -0
- fractal_server/app/runner/v1/common.py +117 -0
- fractal_server/app/runner/{handle_failed_job.py → v1/handle_failed_job.py} +8 -8
- fractal_server/app/runner/v2/__init__.py +336 -0
- fractal_server/app/runner/v2/_local/__init__.py +167 -0
- fractal_server/app/runner/v2/_local/_local_config.py +118 -0
- fractal_server/app/runner/v2/_local/_submit_setup.py +52 -0
- fractal_server/app/runner/v2/_local/executor.py +100 -0
- fractal_server/app/runner/{_slurm → v2/_slurm}/__init__.py +34 -45
- fractal_server/app/runner/v2/_slurm/_submit_setup.py +83 -0
- fractal_server/app/runner/v2/_slurm/get_slurm_config.py +179 -0
- fractal_server/app/runner/v2/deduplicate_list.py +22 -0
- fractal_server/app/runner/v2/handle_failed_job.py +156 -0
- fractal_server/app/runner/v2/merge_outputs.py +38 -0
- fractal_server/app/runner/v2/runner.py +267 -0
- fractal_server/app/runner/v2/runner_functions.py +341 -0
- fractal_server/app/runner/v2/runner_functions_low_level.py +134 -0
- fractal_server/app/runner/v2/task_interface.py +43 -0
- fractal_server/app/runner/v2/v1_compat.py +21 -0
- fractal_server/app/schemas/__init__.py +4 -42
- fractal_server/app/schemas/v1/__init__.py +42 -0
- fractal_server/app/schemas/{applyworkflow.py → v1/applyworkflow.py} +18 -18
- fractal_server/app/schemas/{dataset.py → v1/dataset.py} +30 -30
- fractal_server/app/schemas/{dumps.py → v1/dumps.py} +8 -8
- fractal_server/app/schemas/{manifest.py → v1/manifest.py} +5 -5
- fractal_server/app/schemas/{project.py → v1/project.py} +9 -9
- fractal_server/app/schemas/{task.py → v1/task.py} +12 -12
- fractal_server/app/schemas/{task_collection.py → v1/task_collection.py} +7 -7
- fractal_server/app/schemas/{workflow.py → v1/workflow.py} +38 -38
- fractal_server/app/schemas/v2/__init__.py +34 -0
- fractal_server/app/schemas/v2/dataset.py +89 -0
- fractal_server/app/schemas/v2/dumps.py +87 -0
- fractal_server/app/schemas/v2/job.py +114 -0
- fractal_server/app/schemas/v2/manifest.py +159 -0
- fractal_server/app/schemas/v2/project.py +37 -0
- fractal_server/app/schemas/v2/task.py +120 -0
- fractal_server/app/schemas/v2/task_collection.py +105 -0
- fractal_server/app/schemas/v2/workflow.py +79 -0
- fractal_server/app/schemas/v2/workflowtask.py +119 -0
- fractal_server/config.py +5 -4
- fractal_server/images/__init__.py +2 -0
- fractal_server/images/models.py +50 -0
- fractal_server/images/tools.py +85 -0
- fractal_server/main.py +11 -3
- fractal_server/migrations/env.py +0 -2
- fractal_server/migrations/versions/d71e732236cd_v2.py +239 -0
- fractal_server/tasks/__init__.py +0 -5
- fractal_server/tasks/endpoint_operations.py +13 -19
- fractal_server/tasks/utils.py +35 -0
- fractal_server/tasks/{_TaskCollectPip.py → v1/_TaskCollectPip.py} +3 -3
- fractal_server/tasks/{background_operations.py → v1/background_operations.py} +18 -50
- fractal_server/tasks/v1/get_collection_data.py +14 -0
- fractal_server/tasks/v2/_TaskCollectPip.py +103 -0
- fractal_server/tasks/v2/background_operations.py +381 -0
- fractal_server/tasks/v2/get_collection_data.py +14 -0
- {fractal_server-1.4.10.dist-info → fractal_server-2.0.0a1.dist-info}/METADATA +1 -1
- fractal_server-2.0.0a1.dist-info/RECORD +160 -0
- fractal_server/app/runner/_slurm/.gitignore +0 -2
- fractal_server/app/runner/common.py +0 -311
- fractal_server-1.4.10.dist-info/RECORD +0 -98
- /fractal_server/app/runner/{_slurm → executors/slurm}/remote.py +0 -0
- /fractal_server/app/runner/{_local → v1/_local}/executor.py +0 -0
- {fractal_server-1.4.10.dist-info → fractal_server-2.0.0a1.dist-info}/LICENSE +0 -0
- {fractal_server-1.4.10.dist-info → fractal_server-2.0.0a1.dist-info}/WHEEL +0 -0
- {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
|