fractal-server 2.14.0a2__py3-none-any.whl → 2.14.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +3 -1
- fractal_server/app/history/__init__.py +4 -4
- fractal_server/app/history/image_updates.py +124 -142
- fractal_server/app/history/status_enum.py +2 -2
- fractal_server/app/models/v2/__init__.py +6 -4
- fractal_server/app/models/v2/history.py +44 -20
- fractal_server/app/routes/admin/v2/task.py +1 -1
- fractal_server/app/routes/api/__init__.py +1 -1
- fractal_server/app/routes/api/v2/__init__.py +4 -0
- fractal_server/app/routes/api/v2/_aux_functions_history.py +49 -0
- fractal_server/app/routes/api/v2/dataset.py +0 -12
- fractal_server/app/routes/api/v2/history.py +302 -176
- fractal_server/app/routes/api/v2/project.py +1 -26
- fractal_server/app/routes/api/v2/status_legacy.py +168 -0
- fractal_server/app/routes/api/v2/workflow.py +2 -17
- fractal_server/app/routes/api/v2/workflowtask.py +41 -71
- fractal_server/app/routes/auth/oauth.py +5 -3
- fractal_server/app/runner/executors/base_runner.py +2 -1
- fractal_server/app/runner/executors/local/_submit_setup.py +5 -13
- fractal_server/app/runner/executors/local/runner.py +10 -55
- fractal_server/app/runner/executors/slurm_common/_slurm_config.py +1 -1
- fractal_server/app/runner/executors/slurm_common/get_slurm_config.py +1 -1
- fractal_server/app/runner/executors/slurm_common/remote.py +1 -1
- fractal_server/app/runner/executors/slurm_sudo/runner.py +171 -108
- fractal_server/app/runner/v2/__init__.py +2 -22
- fractal_server/app/runner/v2/_slurm_ssh.py +1 -1
- fractal_server/app/runner/v2/_slurm_sudo.py +1 -1
- fractal_server/app/runner/v2/runner.py +47 -59
- fractal_server/app/runner/v2/runner_functions.py +185 -69
- fractal_server/app/schemas/_validators.py +13 -24
- fractal_server/app/schemas/user.py +10 -7
- fractal_server/app/schemas/user_settings.py +9 -21
- fractal_server/app/schemas/v2/dataset.py +8 -6
- fractal_server/app/schemas/v2/job.py +9 -5
- fractal_server/app/schemas/v2/manifest.py +3 -7
- fractal_server/app/schemas/v2/project.py +9 -7
- fractal_server/app/schemas/v2/task.py +41 -77
- fractal_server/app/schemas/v2/task_collection.py +14 -32
- fractal_server/app/schemas/v2/task_group.py +10 -9
- fractal_server/app/schemas/v2/workflow.py +10 -11
- fractal_server/app/security/__init__.py +3 -3
- fractal_server/app/security/signup_email.py +2 -2
- fractal_server/config.py +33 -34
- fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py +120 -0
- fractal_server/tasks/v2/templates/2_pip_install.sh +1 -1
- fractal_server/tasks/v2/templates/4_pip_show.sh +1 -1
- fractal_server/tasks/v2/utils_templates.py +6 -0
- {fractal_server-2.14.0a2.dist-info → fractal_server-2.14.0a4.dist-info}/METADATA +1 -1
- {fractal_server-2.14.0a2.dist-info → fractal_server-2.14.0a4.dist-info}/RECORD +53 -54
- fractal_server/app/runner/executors/slurm_sudo/_executor_wait_thread.py +0 -130
- fractal_server/app/schemas/v2/history.py +0 -23
- fractal_server/migrations/versions/87cd72a537a2_add_historyitem_table.py +0 -68
- fractal_server/migrations/versions/954ddc64425a_image_status.py +0 -63
- {fractal_server-2.14.0a2.dist-info → fractal_server-2.14.0a4.dist-info}/LICENSE +0 -0
- {fractal_server-2.14.0a2.dist-info → fractal_server-2.14.0a4.dist-info}/WHEEL +0 -0
- {fractal_server-2.14.0a2.dist-info → fractal_server-2.14.0a4.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,3 @@
|
|
1
|
-
import json
|
2
1
|
import logging
|
3
2
|
from copy import copy
|
4
3
|
from copy import deepcopy
|
@@ -7,6 +6,7 @@ from typing import Callable
|
|
7
6
|
from typing import Optional
|
8
7
|
|
9
8
|
from sqlalchemy.orm.attributes import flag_modified
|
9
|
+
from sqlmodel import update
|
10
10
|
|
11
11
|
from ....images import SingleImage
|
12
12
|
from ....images.tools import filter_image_list
|
@@ -18,11 +18,10 @@ from .runner_functions import run_v2_task_non_parallel
|
|
18
18
|
from .runner_functions import run_v2_task_parallel
|
19
19
|
from .task_interface import TaskOutput
|
20
20
|
from fractal_server.app.db import get_sync_db
|
21
|
-
from fractal_server.app.history.status_enum import
|
21
|
+
from fractal_server.app.history.status_enum import XXXStatus
|
22
22
|
from fractal_server.app.models.v2 import AccountingRecord
|
23
23
|
from fractal_server.app.models.v2 import DatasetV2
|
24
|
-
from fractal_server.app.models.v2 import
|
25
|
-
from fractal_server.app.models.v2 import ImageStatus
|
24
|
+
from fractal_server.app.models.v2 import HistoryRun
|
26
25
|
from fractal_server.app.models.v2 import TaskGroupV2
|
27
26
|
from fractal_server.app.models.v2 import WorkflowTaskV2
|
28
27
|
from fractal_server.app.runner.executors.base_runner import BaseRunner
|
@@ -87,6 +86,7 @@ def execute_tasks_v2(
|
|
87
86
|
**wftask.model_dump(exclude={"task"}),
|
88
87
|
task=wftask.task.model_dump(),
|
89
88
|
)
|
89
|
+
|
90
90
|
# Exclude timestamps since they'd need to be serialized properly
|
91
91
|
task_group = db.get(TaskGroupV2, wftask.task.taskgroupv2_id)
|
92
92
|
task_group_dump = task_group.model_dump(
|
@@ -95,44 +95,18 @@ def execute_tasks_v2(
|
|
95
95
|
"timestamp_last_used",
|
96
96
|
}
|
97
97
|
)
|
98
|
-
|
99
|
-
hash(
|
100
|
-
json.dumps(
|
101
|
-
[workflowtask_dump, task_group_dump],
|
102
|
-
sort_keys=True,
|
103
|
-
indent=None,
|
104
|
-
).encode("utf-8")
|
105
|
-
)
|
106
|
-
)
|
107
|
-
images = {
|
108
|
-
image["zarr_url"]: HistoryItemImageStatus.SUBMITTED
|
109
|
-
for image in filtered_images
|
110
|
-
}
|
111
|
-
history_item = HistoryItemV2(
|
98
|
+
history_run = HistoryRun(
|
112
99
|
dataset_id=dataset.id,
|
113
100
|
workflowtask_id=wftask.id,
|
114
101
|
workflowtask_dump=workflowtask_dump,
|
115
102
|
task_group_dump=task_group_dump,
|
116
|
-
parameters_hash=parameters_hash,
|
117
103
|
num_available_images=len(type_filtered_images),
|
118
|
-
|
119
|
-
images=images,
|
104
|
+
status=XXXStatus.SUBMITTED,
|
120
105
|
)
|
121
|
-
db.add(
|
122
|
-
for image in filtered_images:
|
123
|
-
db.merge(
|
124
|
-
ImageStatus(
|
125
|
-
zarr_url=image["zarr_url"],
|
126
|
-
workflowtask_id=wftask.id,
|
127
|
-
dataset_id=dataset.id,
|
128
|
-
parameters_hash=parameters_hash,
|
129
|
-
status=HistoryItemImageStatus.SUBMITTED,
|
130
|
-
logfile="/placeholder",
|
131
|
-
)
|
132
|
-
)
|
106
|
+
db.add(history_run)
|
133
107
|
db.commit()
|
134
|
-
db.refresh(
|
135
|
-
|
108
|
+
db.refresh(history_run)
|
109
|
+
history_run_id = history_run.id
|
136
110
|
|
137
111
|
# TASK EXECUTION (V2)
|
138
112
|
if task.type == "non_parallel":
|
@@ -149,7 +123,8 @@ def execute_tasks_v2(
|
|
149
123
|
workflow_dir_remote=workflow_dir_remote,
|
150
124
|
executor=runner,
|
151
125
|
submit_setup_call=submit_setup_call,
|
152
|
-
|
126
|
+
history_run_id=history_run_id,
|
127
|
+
dataset_id=dataset.id,
|
153
128
|
)
|
154
129
|
elif task.type == "parallel":
|
155
130
|
current_task_output, num_tasks, exceptions = run_v2_task_parallel(
|
@@ -160,7 +135,8 @@ def execute_tasks_v2(
|
|
160
135
|
workflow_dir_remote=workflow_dir_remote,
|
161
136
|
executor=runner,
|
162
137
|
submit_setup_call=submit_setup_call,
|
163
|
-
|
138
|
+
history_run_id=history_run_id,
|
139
|
+
dataset_id=dataset.id,
|
164
140
|
)
|
165
141
|
elif task.type == "compound":
|
166
142
|
current_task_output, num_tasks, exceptions = run_v2_task_compound(
|
@@ -172,7 +148,8 @@ def execute_tasks_v2(
|
|
172
148
|
workflow_dir_remote=workflow_dir_remote,
|
173
149
|
executor=runner,
|
174
150
|
submit_setup_call=submit_setup_call,
|
175
|
-
|
151
|
+
history_run_id=history_run_id,
|
152
|
+
dataset_id=dataset.id,
|
176
153
|
)
|
177
154
|
else:
|
178
155
|
raise ValueError(f"Unexpected error: Invalid {task.type=}.")
|
@@ -198,7 +175,8 @@ def execute_tasks_v2(
|
|
198
175
|
# Update image list
|
199
176
|
num_new_images = 0
|
200
177
|
current_task_output.check_zarr_urls_are_unique()
|
201
|
-
# FIXME: Introduce for loop over task outputs, and processe them
|
178
|
+
# FIXME: Introduce for loop over task outputs, and processe them
|
179
|
+
# sequentially
|
202
180
|
# each failure should lead to an update of the specific image status
|
203
181
|
for image_obj in current_task_output.image_list_updates:
|
204
182
|
image = image_obj.model_dump()
|
@@ -319,22 +297,17 @@ def execute_tasks_v2(
|
|
319
297
|
type_filters_from_task_manifest = task.output_types
|
320
298
|
current_dataset_type_filters.update(type_filters_from_task_manifest)
|
321
299
|
|
322
|
-
# Write current dataset attributes (history, images, filters) into the
|
323
|
-
# database. They can be used (1) to retrieve the latest state
|
324
|
-
# when the job fails, (2) from within endpoints that need up-to-date
|
325
|
-
# information
|
326
300
|
with next(get_sync_db()) as db:
|
301
|
+
# Write current dataset attributes (history + filters) into the
|
302
|
+
# database.
|
327
303
|
db_dataset = db.get(DatasetV2, dataset.id)
|
328
304
|
db_dataset.type_filters = current_dataset_type_filters
|
329
305
|
db_dataset.images = tmp_images
|
330
|
-
for attribute_name in [
|
331
|
-
"type_filters",
|
332
|
-
"history",
|
333
|
-
"images",
|
334
|
-
]:
|
306
|
+
for attribute_name in ["type_filters", "images"]:
|
335
307
|
flag_modified(db_dataset, attribute_name)
|
336
308
|
db.merge(db_dataset)
|
337
309
|
db.commit()
|
310
|
+
db.close() # FIXME: why is this needed?
|
338
311
|
|
339
312
|
# Create accounting record
|
340
313
|
record = AccountingRecord(
|
@@ -345,15 +318,30 @@ def execute_tasks_v2(
|
|
345
318
|
db.add(record)
|
346
319
|
db.commit()
|
347
320
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
321
|
+
# Update History tables, and raise an error if task failed
|
322
|
+
if exceptions == {}:
|
323
|
+
db.execute(
|
324
|
+
update(HistoryRun)
|
325
|
+
.where(HistoryRun.id == history_run_id)
|
326
|
+
.values(status=XXXStatus.DONE)
|
327
|
+
)
|
328
|
+
db.commit()
|
329
|
+
else:
|
330
|
+
db.execute(
|
331
|
+
update(HistoryRun)
|
332
|
+
.where(HistoryRun.id == history_run_id)
|
333
|
+
.values(status=XXXStatus.FAILED)
|
334
|
+
)
|
335
|
+
db.commit()
|
336
|
+
logger.error(
|
337
|
+
f'END {wftask.order}-th task (name="{task_name}") - '
|
338
|
+
"ERROR."
|
339
|
+
)
|
340
|
+
# Raise first error
|
341
|
+
for key, value in exceptions.items():
|
342
|
+
raise JobExecutionError(
|
343
|
+
info=(f"An error occurred.\nOriginal error:\n{value}")
|
344
|
+
)
|
345
|
+
logger.debug(
|
346
|
+
f'END {wftask.order}-th task (name="{task_name}")'
|
357
347
|
)
|
358
|
-
|
359
|
-
logger.debug(f'END {wftask.order}-th task (name="{task_name}")')
|
@@ -1,13 +1,12 @@
|
|
1
1
|
import functools
|
2
2
|
import logging
|
3
|
-
import traceback
|
4
3
|
from pathlib import Path
|
5
4
|
from typing import Any
|
6
|
-
from typing import Callable
|
7
5
|
from typing import Literal
|
8
6
|
from typing import Optional
|
9
7
|
|
10
8
|
from pydantic import ValidationError
|
9
|
+
from sqlmodel import update
|
11
10
|
|
12
11
|
from ..exceptions import JobExecutionError
|
13
12
|
from .deduplicate_list import deduplicate_list
|
@@ -15,6 +14,10 @@ from .merge_outputs import merge_outputs
|
|
15
14
|
from .runner_functions_low_level import run_single_task
|
16
15
|
from .task_interface import InitTaskOutput
|
17
16
|
from .task_interface import TaskOutput
|
17
|
+
from fractal_server.app.db import get_sync_db
|
18
|
+
from fractal_server.app.history.status_enum import XXXStatus
|
19
|
+
from fractal_server.app.models.v2 import HistoryImageCache
|
20
|
+
from fractal_server.app.models.v2 import HistoryUnit
|
18
21
|
from fractal_server.app.models.v2 import TaskV2
|
19
22
|
from fractal_server.app.models.v2 import WorkflowTaskV2
|
20
23
|
from fractal_server.app.runner.components import _COMPONENT_KEY_
|
@@ -59,38 +62,18 @@ def _cast_and_validate_InitTaskOutput(
|
|
59
62
|
)
|
60
63
|
|
61
64
|
|
62
|
-
def no_op_submit_setup_call(
|
65
|
+
def no_op_submit_setup_call(
|
66
|
+
*,
|
67
|
+
wftask: WorkflowTaskV2,
|
68
|
+
root_dir_local: Path,
|
69
|
+
which_type: Literal["non_parallel", "parallel"],
|
70
|
+
) -> dict[str, Any]:
|
63
71
|
"""
|
64
72
|
Default (no-operation) interface of submit_setup_call in V2.
|
65
73
|
"""
|
66
74
|
return {}
|
67
75
|
|
68
76
|
|
69
|
-
# Backend-specific configuration
|
70
|
-
def _get_executor_options(
|
71
|
-
*,
|
72
|
-
wftask: WorkflowTaskV2,
|
73
|
-
workflow_dir_local: Path,
|
74
|
-
workflow_dir_remote: Path,
|
75
|
-
submit_setup_call: Callable,
|
76
|
-
which_type: Literal["non_parallel", "parallel"],
|
77
|
-
) -> dict:
|
78
|
-
try:
|
79
|
-
options = submit_setup_call(
|
80
|
-
wftask=wftask,
|
81
|
-
root_dir_local=workflow_dir_local,
|
82
|
-
root_dir_remote=workflow_dir_remote,
|
83
|
-
which_type=which_type,
|
84
|
-
)
|
85
|
-
except Exception as e:
|
86
|
-
tb = "".join(traceback.format_tb(e.__traceback__))
|
87
|
-
raise RuntimeError(
|
88
|
-
f"{type(e)} error in {submit_setup_call=}\n"
|
89
|
-
f"Original traceback:\n{tb}"
|
90
|
-
)
|
91
|
-
return options
|
92
|
-
|
93
|
-
|
94
77
|
def _check_parallelization_list_size(my_list):
|
95
78
|
if len(my_list) > MAX_PARALLELIZATION_LIST_SIZE:
|
96
79
|
raise JobExecutionError(
|
@@ -109,8 +92,9 @@ def run_v2_task_non_parallel(
|
|
109
92
|
workflow_dir_local: Path,
|
110
93
|
workflow_dir_remote: Optional[Path] = None,
|
111
94
|
executor: BaseRunner,
|
112
|
-
submit_setup_call:
|
113
|
-
|
95
|
+
submit_setup_call: callable = no_op_submit_setup_call,
|
96
|
+
dataset_id: int,
|
97
|
+
history_run_id: int,
|
114
98
|
) -> tuple[TaskOutput, int, dict[int, BaseException]]:
|
115
99
|
"""
|
116
100
|
This runs server-side (see `executor` argument)
|
@@ -123,11 +107,10 @@ def run_v2_task_non_parallel(
|
|
123
107
|
)
|
124
108
|
workflow_dir_remote = workflow_dir_local
|
125
109
|
|
126
|
-
executor_options =
|
110
|
+
executor_options = submit_setup_call(
|
127
111
|
wftask=wftask,
|
128
|
-
|
129
|
-
|
130
|
-
submit_setup_call=submit_setup_call,
|
112
|
+
root_dir_local=workflow_dir_local,
|
113
|
+
root_dir_remote=workflow_dir_remote,
|
131
114
|
which_type="non_parallel",
|
132
115
|
)
|
133
116
|
|
@@ -138,6 +121,29 @@ def run_v2_task_non_parallel(
|
|
138
121
|
)
|
139
122
|
function_kwargs[_COMPONENT_KEY_] = _index_to_component(0)
|
140
123
|
|
124
|
+
# Database History operations
|
125
|
+
with next(get_sync_db()) as db:
|
126
|
+
history_unit = HistoryUnit(
|
127
|
+
history_run_id=history_run_id,
|
128
|
+
status=XXXStatus.SUBMITTED,
|
129
|
+
logfile=None, # FIXME
|
130
|
+
zarr_urls=function_kwargs["zarr_urls"],
|
131
|
+
)
|
132
|
+
db.add(history_unit)
|
133
|
+
db.commit()
|
134
|
+
db.refresh(history_unit)
|
135
|
+
history_unit_id = history_unit.id
|
136
|
+
for zarr_url in function_kwargs["zarr_urls"]:
|
137
|
+
db.merge(
|
138
|
+
HistoryImageCache(
|
139
|
+
workflowtask_id=wftask.id,
|
140
|
+
dataset_id=dataset_id,
|
141
|
+
zarr_url=zarr_url,
|
142
|
+
latest_history_unit_id=history_unit_id,
|
143
|
+
)
|
144
|
+
)
|
145
|
+
db.commit()
|
146
|
+
|
141
147
|
result, exception = executor.submit(
|
142
148
|
functools.partial(
|
143
149
|
run_single_task,
|
@@ -147,18 +153,30 @@ def run_v2_task_non_parallel(
|
|
147
153
|
root_dir_remote=workflow_dir_remote,
|
148
154
|
),
|
149
155
|
parameters=function_kwargs,
|
150
|
-
history_item_id=history_item_id,
|
151
156
|
**executor_options,
|
152
157
|
)
|
153
158
|
|
154
159
|
num_tasks = 1
|
155
|
-
|
156
|
-
if
|
157
|
-
|
160
|
+
with next(get_sync_db()) as db:
|
161
|
+
if exception is None:
|
162
|
+
db.execute(
|
163
|
+
update(HistoryUnit)
|
164
|
+
.where(HistoryUnit.id == history_unit_id)
|
165
|
+
.values(status=XXXStatus.DONE)
|
166
|
+
)
|
167
|
+
db.commit()
|
168
|
+
if result is None:
|
169
|
+
return (TaskOutput(), num_tasks, {})
|
170
|
+
else:
|
171
|
+
return (_cast_and_validate_TaskOutput(result), num_tasks, {})
|
158
172
|
else:
|
159
|
-
|
160
|
-
|
161
|
-
|
173
|
+
db.execute(
|
174
|
+
update(HistoryUnit)
|
175
|
+
.where(HistoryUnit.id == history_unit_id)
|
176
|
+
.values(status=XXXStatus.FAILED)
|
177
|
+
)
|
178
|
+
db.commit()
|
179
|
+
return (TaskOutput(), num_tasks, {0: exception})
|
162
180
|
|
163
181
|
|
164
182
|
def run_v2_task_parallel(
|
@@ -169,32 +187,57 @@ def run_v2_task_parallel(
|
|
169
187
|
executor: BaseRunner,
|
170
188
|
workflow_dir_local: Path,
|
171
189
|
workflow_dir_remote: Optional[Path] = None,
|
172
|
-
submit_setup_call:
|
173
|
-
|
190
|
+
submit_setup_call: callable = no_op_submit_setup_call,
|
191
|
+
dataset_id: int,
|
192
|
+
history_run_id: int,
|
174
193
|
) -> tuple[TaskOutput, int, dict[int, BaseException]]:
|
175
194
|
|
176
195
|
if len(images) == 0:
|
196
|
+
# FIXME: Do something with history units/images?
|
177
197
|
return (TaskOutput(), 0, {})
|
178
198
|
|
179
199
|
_check_parallelization_list_size(images)
|
180
200
|
|
181
|
-
executor_options =
|
201
|
+
executor_options = submit_setup_call(
|
182
202
|
wftask=wftask,
|
183
|
-
|
184
|
-
|
185
|
-
submit_setup_call=submit_setup_call,
|
203
|
+
root_dir_local=workflow_dir_local,
|
204
|
+
root_dir_remote=workflow_dir_remote,
|
186
205
|
which_type="parallel",
|
187
206
|
)
|
188
207
|
|
189
208
|
list_function_kwargs = []
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
209
|
+
history_unit_ids = []
|
210
|
+
with next(get_sync_db()) as db:
|
211
|
+
for ind, image in enumerate(images):
|
212
|
+
list_function_kwargs.append(
|
213
|
+
dict(
|
214
|
+
zarr_url=image["zarr_url"],
|
215
|
+
**(wftask.args_parallel or {}),
|
216
|
+
),
|
217
|
+
)
|
218
|
+
list_function_kwargs[-1][_COMPONENT_KEY_] = _index_to_component(
|
219
|
+
ind
|
220
|
+
)
|
221
|
+
history_unit = HistoryUnit(
|
222
|
+
history_run_id=history_run_id,
|
223
|
+
status=XXXStatus.SUBMITTED,
|
224
|
+
logfile=None, # FIXME
|
225
|
+
zarr_urls=[image["zarr_url"]],
|
226
|
+
)
|
227
|
+
# FIXME: this should be a bulk operation
|
228
|
+
db.add(history_unit)
|
229
|
+
db.commit()
|
230
|
+
db.refresh(history_unit)
|
231
|
+
db.merge(
|
232
|
+
HistoryImageCache(
|
233
|
+
workflowtask_id=wftask.id,
|
234
|
+
dataset_id=dataset_id,
|
235
|
+
zarr_url=image["zarr_url"],
|
236
|
+
latest_history_unit_id=history_unit.id,
|
237
|
+
)
|
238
|
+
)
|
239
|
+
db.commit()
|
240
|
+
history_unit_ids.append(history_unit.id)
|
198
241
|
|
199
242
|
results, exceptions = executor.multisubmit(
|
200
243
|
functools.partial(
|
@@ -205,11 +248,12 @@ def run_v2_task_parallel(
|
|
205
248
|
root_dir_remote=workflow_dir_remote,
|
206
249
|
),
|
207
250
|
list_parameters=list_function_kwargs,
|
208
|
-
history_item_id=history_item_id,
|
209
251
|
**executor_options,
|
210
252
|
)
|
211
253
|
|
212
254
|
outputs = []
|
255
|
+
history_unit_ids_done: list[int] = []
|
256
|
+
history_unit_ids_failed: list[int] = []
|
213
257
|
for ind in range(len(list_function_kwargs)):
|
214
258
|
if ind in results.keys():
|
215
259
|
result = results[ind]
|
@@ -218,11 +262,26 @@ def run_v2_task_parallel(
|
|
218
262
|
else:
|
219
263
|
output = _cast_and_validate_TaskOutput(result)
|
220
264
|
outputs.append(output)
|
265
|
+
history_unit_ids_done.append(history_unit_ids[ind])
|
221
266
|
elif ind in exceptions.keys():
|
222
267
|
print(f"Bad: {exceptions[ind]}")
|
268
|
+
history_unit_ids_failed.append(history_unit_ids[ind])
|
223
269
|
else:
|
224
270
|
print("VERY BAD - should have not reached this point")
|
225
271
|
|
272
|
+
with next(get_sync_db()) as db:
|
273
|
+
db.execute(
|
274
|
+
update(HistoryUnit)
|
275
|
+
.where(HistoryUnit.id.in_(history_unit_ids_done))
|
276
|
+
.values(status=XXXStatus.DONE)
|
277
|
+
)
|
278
|
+
db.execute(
|
279
|
+
update(HistoryUnit)
|
280
|
+
.where(HistoryUnit.id.in_(history_unit_ids_failed))
|
281
|
+
.values(status=XXXStatus.FAILED)
|
282
|
+
)
|
283
|
+
db.commit()
|
284
|
+
|
226
285
|
num_tasks = len(images)
|
227
286
|
merged_output = merge_outputs(outputs)
|
228
287
|
return (merged_output, num_tasks, exceptions)
|
@@ -237,22 +296,21 @@ def run_v2_task_compound(
|
|
237
296
|
executor: BaseRunner,
|
238
297
|
workflow_dir_local: Path,
|
239
298
|
workflow_dir_remote: Optional[Path] = None,
|
240
|
-
submit_setup_call:
|
241
|
-
|
299
|
+
submit_setup_call: callable = no_op_submit_setup_call,
|
300
|
+
dataset_id: int,
|
301
|
+
history_run_id: int,
|
242
302
|
) -> tuple[TaskOutput, int, dict[int, BaseException]]:
|
243
303
|
|
244
|
-
executor_options_init =
|
304
|
+
executor_options_init = submit_setup_call(
|
245
305
|
wftask=wftask,
|
246
|
-
|
247
|
-
|
248
|
-
submit_setup_call=submit_setup_call,
|
306
|
+
root_dir_local=workflow_dir_local,
|
307
|
+
root_dir_remote=workflow_dir_remote,
|
249
308
|
which_type="non_parallel",
|
250
309
|
)
|
251
|
-
executor_options_compute =
|
310
|
+
executor_options_compute = submit_setup_call(
|
252
311
|
wftask=wftask,
|
253
|
-
|
254
|
-
|
255
|
-
submit_setup_call=submit_setup_call,
|
312
|
+
root_dir_local=workflow_dir_local,
|
313
|
+
root_dir_remote=workflow_dir_remote,
|
256
314
|
which_type="parallel",
|
257
315
|
)
|
258
316
|
|
@@ -263,6 +321,33 @@ def run_v2_task_compound(
|
|
263
321
|
**(wftask.args_non_parallel or {}),
|
264
322
|
)
|
265
323
|
function_kwargs[_COMPONENT_KEY_] = f"init_{_index_to_component(0)}"
|
324
|
+
|
325
|
+
# Create database History entries
|
326
|
+
input_image_zarr_urls = function_kwargs["zarr_urls"]
|
327
|
+
with next(get_sync_db()) as db:
|
328
|
+
# Create a single `HistoryUnit` for the whole compound task
|
329
|
+
history_unit = HistoryUnit(
|
330
|
+
history_run_id=history_run_id,
|
331
|
+
status=XXXStatus.SUBMITTED,
|
332
|
+
logfile=None, # FIXME
|
333
|
+
zarr_urls=input_image_zarr_urls,
|
334
|
+
)
|
335
|
+
db.add(history_unit)
|
336
|
+
db.commit()
|
337
|
+
db.refresh(history_unit)
|
338
|
+
history_unit_id = history_unit.id
|
339
|
+
# Create one `HistoryImageCache` for each input image
|
340
|
+
for zarr_url in input_image_zarr_urls:
|
341
|
+
db.merge(
|
342
|
+
HistoryImageCache(
|
343
|
+
workflowtask_id=wftask.id,
|
344
|
+
dataset_id=dataset_id,
|
345
|
+
zarr_url=zarr_url,
|
346
|
+
latest_history_unit_id=history_unit_id,
|
347
|
+
)
|
348
|
+
)
|
349
|
+
db.commit()
|
350
|
+
|
266
351
|
result, exception = executor.submit(
|
267
352
|
functools.partial(
|
268
353
|
run_single_task,
|
@@ -272,8 +357,6 @@ def run_v2_task_compound(
|
|
272
357
|
root_dir_remote=workflow_dir_remote,
|
273
358
|
),
|
274
359
|
parameters=function_kwargs,
|
275
|
-
history_item_id=history_item_id,
|
276
|
-
in_compound_task=True,
|
277
360
|
**executor_options_init,
|
278
361
|
)
|
279
362
|
|
@@ -284,6 +367,13 @@ def run_v2_task_compound(
|
|
284
367
|
else:
|
285
368
|
init_task_output = _cast_and_validate_InitTaskOutput(result)
|
286
369
|
else:
|
370
|
+
with next(get_sync_db()) as db:
|
371
|
+
db.execute(
|
372
|
+
update(HistoryUnit)
|
373
|
+
.where(HistoryUnit.id == history_unit_id)
|
374
|
+
.values(status=XXXStatus.FAILED)
|
375
|
+
)
|
376
|
+
db.commit()
|
287
377
|
return (TaskOutput(), num_tasks, {0: exception})
|
288
378
|
|
289
379
|
parallelization_list = init_task_output.parallelization_list
|
@@ -295,6 +385,13 @@ def run_v2_task_compound(
|
|
295
385
|
_check_parallelization_list_size(parallelization_list)
|
296
386
|
|
297
387
|
if len(parallelization_list) == 0:
|
388
|
+
with next(get_sync_db()) as db:
|
389
|
+
db.execute(
|
390
|
+
update(HistoryUnit)
|
391
|
+
.where(HistoryUnit.id == history_unit_id)
|
392
|
+
.values(status=XXXStatus.DONE)
|
393
|
+
)
|
394
|
+
db.commit()
|
298
395
|
return (TaskOutput(), 0, {})
|
299
396
|
|
300
397
|
list_function_kwargs = []
|
@@ -319,12 +416,12 @@ def run_v2_task_compound(
|
|
319
416
|
root_dir_remote=workflow_dir_remote,
|
320
417
|
),
|
321
418
|
list_parameters=list_function_kwargs,
|
322
|
-
history_item_id=history_item_id,
|
323
419
|
in_compound_task=True,
|
324
420
|
**executor_options_compute,
|
325
421
|
)
|
326
422
|
|
327
423
|
outputs = []
|
424
|
+
failure = False
|
328
425
|
for ind in range(len(list_function_kwargs)):
|
329
426
|
if ind in results.keys():
|
330
427
|
result = results[ind]
|
@@ -333,8 +430,27 @@ def run_v2_task_compound(
|
|
333
430
|
else:
|
334
431
|
output = _cast_and_validate_TaskOutput(result)
|
335
432
|
outputs.append(output)
|
433
|
+
|
336
434
|
elif ind in exceptions.keys():
|
337
435
|
print(f"Bad: {exceptions[ind]}")
|
436
|
+
failure = True
|
437
|
+
else:
|
438
|
+
print("VERY BAD - should have not reached this point")
|
439
|
+
|
440
|
+
with next(get_sync_db()) as db:
|
441
|
+
if failure:
|
442
|
+
db.execute(
|
443
|
+
update(HistoryUnit)
|
444
|
+
.where(HistoryUnit.id == history_unit_id)
|
445
|
+
.values(status=XXXStatus.FAILED)
|
446
|
+
)
|
447
|
+
else:
|
448
|
+
db.execute(
|
449
|
+
update(HistoryUnit)
|
450
|
+
.where(HistoryUnit.id == history_unit_id)
|
451
|
+
.values(status=XXXStatus.DONE)
|
452
|
+
)
|
453
|
+
db.commit()
|
338
454
|
|
339
455
|
merged_output = merge_outputs(outputs)
|
340
456
|
return (merged_output, num_tasks, exceptions)
|
@@ -1,43 +1,32 @@
|
|
1
1
|
import os
|
2
|
+
from typing import Annotated
|
2
3
|
from typing import Any
|
3
4
|
from typing import Optional
|
4
5
|
|
6
|
+
from pydantic.types import StringConstraints
|
5
7
|
|
6
|
-
def valstr(attribute: str, accept_none: bool = False):
|
7
|
-
"""
|
8
|
-
Check that a string attribute is not an empty string, and remove the
|
9
|
-
leading and trailing whitespace characters.
|
10
8
|
|
11
|
-
|
12
|
-
|
9
|
+
def cant_set_none(value: Any) -> Any:
|
10
|
+
if value is None:
|
11
|
+
raise ValueError("Field cannot be set to 'None'.")
|
12
|
+
return value
|
13
13
|
|
14
|
-
def val(cls, string: Optional[str]) -> Optional[str]:
|
15
|
-
if string is None:
|
16
|
-
if accept_none:
|
17
|
-
return string
|
18
|
-
else:
|
19
|
-
raise ValueError(
|
20
|
-
f"String attribute '{attribute}' cannot be None"
|
21
|
-
)
|
22
|
-
s = string.strip()
|
23
|
-
if not s:
|
24
|
-
raise ValueError(f"String attribute '{attribute}' cannot be empty")
|
25
|
-
return s
|
26
14
|
|
27
|
-
|
15
|
+
NonEmptyString = Annotated[
|
16
|
+
str, StringConstraints(min_length=1, strip_whitespace=True)
|
17
|
+
]
|
28
18
|
|
29
19
|
|
30
20
|
def valdict_keys(attribute: str):
|
31
21
|
def val(cls, d: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]:
|
32
22
|
"""
|
33
|
-
|
34
|
-
identical keys.
|
23
|
+
Strip every key of the dictionary, and fail if there are identical keys
|
35
24
|
"""
|
36
25
|
if d is not None:
|
37
26
|
old_keys = list(d.keys())
|
38
|
-
new_keys = [
|
39
|
-
|
40
|
-
|
27
|
+
new_keys = [key.strip() for key in old_keys]
|
28
|
+
if any(k == "" for k in new_keys):
|
29
|
+
raise ValueError(f"Empty string in {new_keys}.")
|
41
30
|
if len(new_keys) != len(set(new_keys)):
|
42
31
|
raise ValueError(
|
43
32
|
f"Dictionary contains multiple identical keys: '{d}'."
|