fractal-server 2.10.6__py3-none-any.whl → 2.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/v2/dataset.py +9 -6
  3. fractal_server/app/models/v2/job.py +5 -0
  4. fractal_server/app/models/v2/workflowtask.py +5 -8
  5. fractal_server/app/routes/api/v1/dataset.py +2 -2
  6. fractal_server/app/routes/api/v2/_aux_functions.py +3 -10
  7. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +21 -0
  8. fractal_server/app/routes/api/v2/images.py +30 -7
  9. fractal_server/app/routes/api/v2/job.py +14 -1
  10. fractal_server/app/routes/api/v2/status.py +20 -20
  11. fractal_server/app/routes/api/v2/submit.py +11 -4
  12. fractal_server/app/routes/api/v2/workflow.py +95 -0
  13. fractal_server/app/routes/api/v2/workflow_import.py +8 -0
  14. fractal_server/app/routes/api/v2/workflowtask.py +45 -26
  15. fractal_server/app/runner/{async_wrap.py → async_wrap_v1.py} +1 -1
  16. fractal_server/app/runner/executors/slurm/_slurm_config.py +1 -1
  17. fractal_server/app/runner/executors/slurm/ssh/executor.py +2 -2
  18. fractal_server/app/runner/filenames.py +2 -4
  19. fractal_server/app/runner/v1/_common.py +4 -4
  20. fractal_server/app/runner/v1/_local/__init__.py +2 -2
  21. fractal_server/app/runner/v1/_slurm/__init__.py +2 -2
  22. fractal_server/app/runner/v1/handle_failed_job.py +4 -4
  23. fractal_server/app/runner/v2/__init__.py +12 -66
  24. fractal_server/app/runner/v2/_local/__init__.py +17 -47
  25. fractal_server/app/runner/v2/_local_experimental/__init__.py +27 -61
  26. fractal_server/app/runner/v2/_slurm_ssh/__init__.py +26 -65
  27. fractal_server/app/runner/v2/_slurm_sudo/__init__.py +24 -66
  28. fractal_server/app/runner/v2/handle_failed_job.py +31 -130
  29. fractal_server/app/runner/v2/merge_outputs.py +6 -17
  30. fractal_server/app/runner/v2/runner.py +51 -89
  31. fractal_server/app/runner/v2/task_interface.py +0 -2
  32. fractal_server/app/schemas/_filter_validators.py +43 -0
  33. fractal_server/app/schemas/_validators.py +13 -2
  34. fractal_server/app/schemas/v2/dataset.py +85 -12
  35. fractal_server/app/schemas/v2/dumps.py +6 -8
  36. fractal_server/app/schemas/v2/job.py +14 -0
  37. fractal_server/app/schemas/v2/task.py +9 -9
  38. fractal_server/app/schemas/v2/task_group.py +2 -2
  39. fractal_server/app/schemas/v2/workflowtask.py +69 -20
  40. fractal_server/data_migrations/2_11_0.py +168 -0
  41. fractal_server/images/__init__.py +0 -1
  42. fractal_server/images/models.py +12 -35
  43. fractal_server/images/tools.py +53 -14
  44. fractal_server/migrations/versions/db09233ad13a_split_filters_and_keep_old_columns.py +96 -0
  45. fractal_server/utils.py +9 -7
  46. {fractal_server-2.10.6.dist-info → fractal_server-2.11.0.dist-info}/METADATA +1 -1
  47. {fractal_server-2.10.6.dist-info → fractal_server-2.11.0.dist-info}/RECORD +50 -47
  48. {fractal_server-2.10.6.dist-info → fractal_server-2.11.0.dist-info}/LICENSE +0 -0
  49. {fractal_server-2.10.6.dist-info → fractal_server-2.11.0.dist-info}/WHEEL +0 -0
  50. {fractal_server-2.10.6.dist-info → fractal_server-2.11.0.dist-info}/entry_points.txt +0 -0
@@ -4,60 +4,16 @@ from typing import Optional
4
4
 
5
5
  from ....models.v2 import DatasetV2
6
6
  from ....models.v2 import WorkflowV2
7
- from ...async_wrap import async_wrap
8
7
  from ...exceptions import JobExecutionError
9
8
  from ...filenames import SHUTDOWN_FILENAME
10
9
  from ...set_start_and_last_task_index import set_start_and_last_task_index
11
10
  from ..runner import execute_tasks_v2
12
11
  from ._submit_setup import _local_submit_setup
13
12
  from .executor import FractalProcessPoolExecutor
13
+ from fractal_server.images.models import AttributeFiltersType
14
14
 
15
15
 
16
- def _process_workflow(
17
- *,
18
- workflow: WorkflowV2,
19
- dataset: DatasetV2,
20
- logger_name: str,
21
- workflow_dir_local: Path,
22
- first_task_index: int,
23
- last_task_index: int,
24
- ) -> dict:
25
- """
26
- Internal processing routine
27
-
28
- Schedules the workflow using a `FractalProcessPoolExecutor`.
29
-
30
- Cf.
31
- [process_workflow][fractal_server.app.runner.v2._local_experimental.process_workflow]
32
- for the call signature.
33
- """
34
- with FractalProcessPoolExecutor(
35
- shutdown_file=workflow_dir_local / SHUTDOWN_FILENAME
36
- ) as executor:
37
- try:
38
- new_dataset_attributes = execute_tasks_v2(
39
- wf_task_list=workflow.task_list[
40
- first_task_index : (last_task_index + 1) # noqa
41
- ],
42
- dataset=dataset,
43
- executor=executor,
44
- workflow_dir_local=workflow_dir_local,
45
- workflow_dir_remote=workflow_dir_local,
46
- logger_name=logger_name,
47
- submit_setup_call=_local_submit_setup,
48
- )
49
- except BrokenProcessPool as e:
50
- raise JobExecutionError(
51
- info=(
52
- "Job failed with BrokenProcessPool error, likely due to "
53
- f"an executor shutdown.\nOriginal error:\n{e.args[0]}"
54
- )
55
- )
56
-
57
- return new_dataset_attributes
58
-
59
-
60
- async def process_workflow(
16
+ def process_workflow(
61
17
  *,
62
18
  workflow: WorkflowV2,
63
19
  dataset: DatasetV2,
@@ -66,12 +22,13 @@ async def process_workflow(
66
22
  first_task_index: Optional[int] = None,
67
23
  last_task_index: Optional[int] = None,
68
24
  logger_name: str,
25
+ job_attribute_filters: AttributeFiltersType,
69
26
  # Slurm-specific
70
27
  user_cache_dir: Optional[str] = None,
71
28
  slurm_user: Optional[str] = None,
72
29
  slurm_account: Optional[str] = None,
73
30
  worker_init: Optional[str] = None,
74
- ) -> dict:
31
+ ) -> None:
75
32
  """
76
33
  Run a workflow
77
34
 
@@ -123,11 +80,6 @@ async def process_workflow(
123
80
  (positive exit codes).
124
81
  JobExecutionError: wrapper for errors raised by the tasks' executors
125
82
  (negative exit codes).
126
-
127
- Returns:
128
- output_dataset_metadata:
129
- The updated metadata for the dataset, as returned by the last task
130
- of the workflow
131
83
  """
132
84
 
133
85
  if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
@@ -144,12 +96,26 @@ async def process_workflow(
144
96
  last_task_index=last_task_index,
145
97
  )
146
98
 
147
- new_dataset_attributes = await async_wrap(_process_workflow)(
148
- workflow=workflow,
149
- dataset=dataset,
150
- logger_name=logger_name,
151
- workflow_dir_local=workflow_dir_local,
152
- first_task_index=first_task_index,
153
- last_task_index=last_task_index,
154
- )
155
- return new_dataset_attributes
99
+ with FractalProcessPoolExecutor(
100
+ shutdown_file=workflow_dir_local / SHUTDOWN_FILENAME
101
+ ) as executor:
102
+ try:
103
+ execute_tasks_v2(
104
+ wf_task_list=workflow.task_list[
105
+ first_task_index : (last_task_index + 1)
106
+ ],
107
+ dataset=dataset,
108
+ executor=executor,
109
+ workflow_dir_local=workflow_dir_local,
110
+ workflow_dir_remote=workflow_dir_local,
111
+ logger_name=logger_name,
112
+ submit_setup_call=_local_submit_setup,
113
+ job_attribute_filters=job_attribute_filters,
114
+ )
115
+ except BrokenProcessPool as e:
116
+ raise JobExecutionError(
117
+ info=(
118
+ "Job failed with BrokenProcessPool error, likely due to "
119
+ f"an executor shutdown.\nOriginal error:\n{e.args[0]}"
120
+ )
121
+ )
@@ -17,48 +17,51 @@ This backend runs fractal workflows in a SLURM cluster using Clusterfutures
17
17
  Executor objects.
18
18
  """
19
19
  from pathlib import Path
20
- from typing import Any
21
20
  from typing import Optional
22
- from typing import Union
23
21
 
24
22
  from .....ssh._fabric import FractalSSH
25
23
  from ....models.v2 import DatasetV2
26
24
  from ....models.v2 import WorkflowV2
27
- from ...async_wrap import async_wrap
28
25
  from ...exceptions import JobExecutionError
29
26
  from ...executors.slurm.ssh.executor import FractalSlurmSSHExecutor
30
27
  from ...set_start_and_last_task_index import set_start_and_last_task_index
31
28
  from ..runner import execute_tasks_v2
32
29
  from ._submit_setup import _slurm_submit_setup
30
+ from fractal_server.images.models import AttributeFiltersType
33
31
  from fractal_server.logger import set_logger
34
32
 
35
-
36
33
  logger = set_logger(__name__)
37
34
 
38
35
 
39
- def _process_workflow(
36
+ def process_workflow(
40
37
  *,
41
38
  workflow: WorkflowV2,
42
39
  dataset: DatasetV2,
43
- logger_name: str,
44
40
  workflow_dir_local: Path,
45
- workflow_dir_remote: Path,
46
- first_task_index: int,
47
- last_task_index: int,
41
+ workflow_dir_remote: Optional[Path] = None,
42
+ first_task_index: Optional[int] = None,
43
+ last_task_index: Optional[int] = None,
44
+ logger_name: str,
45
+ job_attribute_filters: AttributeFiltersType,
48
46
  fractal_ssh: FractalSSH,
49
- worker_init: Optional[Union[str, list[str]]] = None,
50
- ) -> dict[str, Any]:
47
+ worker_init: Optional[str] = None,
48
+ # Not used
49
+ user_cache_dir: Optional[str] = None,
50
+ slurm_user: Optional[str] = None,
51
+ slurm_account: Optional[str] = None,
52
+ ) -> None:
51
53
  """
52
- Internal processing routine for the SLURM backend
53
-
54
- This function initialises the a FractalSlurmExecutor, setting logging,
55
- workflow working dir and user to impersonate. It then schedules the
56
- workflow tasks and returns the new dataset attributes
57
-
58
- Returns:
59
- new_dataset_attributes:
54
+ Process workflow (SLURM backend public interface)
60
55
  """
61
56
 
57
+ # Set values of first_task_index and last_task_index
58
+ num_tasks = len(workflow.task_list)
59
+ first_task_index, last_task_index = set_start_and_last_task_index(
60
+ num_tasks,
61
+ first_task_index=first_task_index,
62
+ last_task_index=last_task_index,
63
+ )
64
+
62
65
  if isinstance(worker_init, str):
63
66
  worker_init = worker_init.split("\n")
64
67
 
@@ -80,57 +83,15 @@ def _process_workflow(
80
83
  workflow_dir_remote=workflow_dir_remote,
81
84
  common_script_lines=worker_init,
82
85
  ) as executor:
83
- new_dataset_attributes = execute_tasks_v2(
86
+ execute_tasks_v2(
84
87
  wf_task_list=workflow.task_list[
85
- first_task_index : (last_task_index + 1) # noqa
86
- ], # noqa
88
+ first_task_index : (last_task_index + 1)
89
+ ],
87
90
  dataset=dataset,
88
91
  executor=executor,
89
92
  workflow_dir_local=workflow_dir_local,
90
93
  workflow_dir_remote=workflow_dir_remote,
91
94
  logger_name=logger_name,
92
95
  submit_setup_call=_slurm_submit_setup,
96
+ job_attribute_filters=job_attribute_filters,
93
97
  )
94
- return new_dataset_attributes
95
-
96
-
97
- async def process_workflow(
98
- *,
99
- workflow: WorkflowV2,
100
- dataset: DatasetV2,
101
- workflow_dir_local: Path,
102
- workflow_dir_remote: Optional[Path] = None,
103
- first_task_index: Optional[int] = None,
104
- last_task_index: Optional[int] = None,
105
- logger_name: str,
106
- # Not used
107
- fractal_ssh: FractalSSH,
108
- user_cache_dir: Optional[str] = None,
109
- slurm_user: Optional[str] = None,
110
- slurm_account: Optional[str] = None,
111
- worker_init: Optional[str] = None,
112
- ) -> dict:
113
- """
114
- Process workflow (SLURM backend public interface)
115
- """
116
-
117
- # Set values of first_task_index and last_task_index
118
- num_tasks = len(workflow.task_list)
119
- first_task_index, last_task_index = set_start_and_last_task_index(
120
- num_tasks,
121
- first_task_index=first_task_index,
122
- last_task_index=last_task_index,
123
- )
124
-
125
- new_dataset_attributes = await async_wrap(_process_workflow)(
126
- workflow=workflow,
127
- dataset=dataset,
128
- logger_name=logger_name,
129
- workflow_dir_local=workflow_dir_local,
130
- workflow_dir_remote=workflow_dir_remote,
131
- first_task_index=first_task_index,
132
- last_task_index=last_task_index,
133
- worker_init=worker_init,
134
- fractal_ssh=fractal_ssh,
135
- )
136
- return new_dataset_attributes
@@ -17,44 +17,45 @@ This backend runs fractal workflows in a SLURM cluster using Clusterfutures
17
17
  Executor objects.
18
18
  """
19
19
  from pathlib import Path
20
- from typing import Any
21
20
  from typing import Optional
22
- from typing import Union
23
21
 
24
22
  from ....models.v2 import DatasetV2
25
23
  from ....models.v2 import WorkflowV2
26
- from ...async_wrap import async_wrap
27
24
  from ...executors.slurm.sudo.executor import FractalSlurmExecutor
28
25
  from ...set_start_and_last_task_index import set_start_and_last_task_index
29
26
  from ..runner import execute_tasks_v2
30
27
  from ._submit_setup import _slurm_submit_setup
28
+ from fractal_server.images.models import AttributeFiltersType
31
29
 
32
30
 
33
- def _process_workflow(
31
+ def process_workflow(
34
32
  *,
35
33
  workflow: WorkflowV2,
36
34
  dataset: DatasetV2,
37
- logger_name: str,
38
35
  workflow_dir_local: Path,
39
- workflow_dir_remote: Path,
40
- first_task_index: int,
41
- last_task_index: int,
36
+ workflow_dir_remote: Optional[Path] = None,
37
+ first_task_index: Optional[int] = None,
38
+ last_task_index: Optional[int] = None,
39
+ logger_name: str,
40
+ job_attribute_filters: AttributeFiltersType,
41
+ # Slurm-specific
42
+ user_cache_dir: Optional[str] = None,
42
43
  slurm_user: Optional[str] = None,
43
44
  slurm_account: Optional[str] = None,
44
- user_cache_dir: str,
45
- worker_init: Optional[Union[str, list[str]]] = None,
46
- ) -> dict[str, Any]:
45
+ worker_init: Optional[str] = None,
46
+ ) -> None:
47
47
  """
48
- Internal processing routine for the SLURM backend
49
-
50
- This function initialises the a FractalSlurmExecutor, setting logging,
51
- workflow working dir and user to impersonate. It then schedules the
52
- workflow tasks and returns the new dataset attributes
53
-
54
- Returns:
55
- new_dataset_attributes:
48
+ Process workflow (SLURM backend public interface).
56
49
  """
57
50
 
51
+ # Set values of first_task_index and last_task_index
52
+ num_tasks = len(workflow.task_list)
53
+ first_task_index, last_task_index = set_start_and_last_task_index(
54
+ num_tasks,
55
+ first_task_index=first_task_index,
56
+ last_task_index=last_task_index,
57
+ )
58
+
58
59
  if not slurm_user:
59
60
  raise RuntimeError(
60
61
  "slurm_user argument is required, for slurm backend"
@@ -73,58 +74,15 @@ def _process_workflow(
73
74
  common_script_lines=worker_init,
74
75
  slurm_account=slurm_account,
75
76
  ) as executor:
76
- new_dataset_attributes = execute_tasks_v2(
77
+ execute_tasks_v2(
77
78
  wf_task_list=workflow.task_list[
78
- first_task_index : (last_task_index + 1) # noqa
79
- ], # noqa
79
+ first_task_index : (last_task_index + 1)
80
+ ],
80
81
  dataset=dataset,
81
82
  executor=executor,
82
83
  workflow_dir_local=workflow_dir_local,
83
84
  workflow_dir_remote=workflow_dir_remote,
84
85
  logger_name=logger_name,
85
86
  submit_setup_call=_slurm_submit_setup,
87
+ job_attribute_filters=job_attribute_filters,
86
88
  )
87
- return new_dataset_attributes
88
-
89
-
90
- async def process_workflow(
91
- *,
92
- workflow: WorkflowV2,
93
- dataset: DatasetV2,
94
- workflow_dir_local: Path,
95
- workflow_dir_remote: Optional[Path] = None,
96
- first_task_index: Optional[int] = None,
97
- last_task_index: Optional[int] = None,
98
- logger_name: str,
99
- # Slurm-specific
100
- user_cache_dir: Optional[str] = None,
101
- slurm_user: Optional[str] = None,
102
- slurm_account: Optional[str] = None,
103
- worker_init: Optional[str] = None,
104
- ) -> dict:
105
- """
106
- Process workflow (SLURM backend public interface).
107
- """
108
-
109
- # Set values of first_task_index and last_task_index
110
- num_tasks = len(workflow.task_list)
111
- first_task_index, last_task_index = set_start_and_last_task_index(
112
- num_tasks,
113
- first_task_index=first_task_index,
114
- last_task_index=last_task_index,
115
- )
116
-
117
- new_dataset_attributes = await async_wrap(_process_workflow)(
118
- workflow=workflow,
119
- dataset=dataset,
120
- logger_name=logger_name,
121
- workflow_dir_local=workflow_dir_local,
122
- workflow_dir_remote=workflow_dir_remote,
123
- first_task_index=first_task_index,
124
- last_task_index=last_task_index,
125
- user_cache_dir=user_cache_dir,
126
- slurm_user=slurm_user,
127
- slurm_account=slurm_account,
128
- worker_init=worker_init,
129
- )
130
- return new_dataset_attributes
@@ -12,147 +12,48 @@
12
12
  """
13
13
  Helper functions to handle Dataset history.
14
14
  """
15
- import json
16
15
  import logging
17
- from pathlib import Path
18
- from typing import Any
19
- from typing import Optional
16
+
17
+ from sqlalchemy.orm.attributes import flag_modified
20
18
 
21
19
  from ...models.v2 import DatasetV2
22
- from ...models.v2 import JobV2
23
- from ...models.v2 import WorkflowTaskV2
24
- from ...models.v2 import WorkflowV2
25
20
  from ...schemas.v2 import WorkflowTaskStatusTypeV2
26
- from ..filenames import FILTERS_FILENAME
27
- from ..filenames import HISTORY_FILENAME
28
- from ..filenames import IMAGES_FILENAME
21
+ from fractal_server.app.db import get_sync_db
29
22
 
30
23
 
31
- def assemble_history_failed_job(
32
- job: JobV2,
33
- dataset: DatasetV2,
34
- workflow: WorkflowV2,
35
- logger_name: Optional[str] = None,
36
- failed_wftask: Optional[WorkflowTaskV2] = None,
37
- ) -> list[dict[str, Any]]:
24
+ def mark_last_wftask_as_failed(
25
+ dataset_id: int,
26
+ logger_name: str,
27
+ ) -> None:
38
28
  """
39
- Assemble `history` after a workflow-execution job fails.
29
+ Edit dataset history, by marking last item as failed.
40
30
 
41
31
  Args:
42
- job:
43
- The failed `JobV2` object.
44
- dataset:
45
- The `DatasetV2` object associated to `job`.
46
- workflow:
47
- The `WorkflowV2` object associated to `job`.
32
+ dataset: The `DatasetV2` object
48
33
  logger_name: A logger name.
49
- failed_wftask:
50
- If set, append it to `history` during step 3; if `None`, infer
51
- it by comparing the job task list and the one in
52
- `HISTORY_FILENAME`.
53
-
54
- Returns:
55
- The new value of `history`, to be merged into
56
- `dataset.meta`.
57
34
  """
58
35
 
59
36
  logger = logging.getLogger(logger_name)
60
-
61
- # The final value of the history attribute should include up to three
62
- # parts, coming from: the database, the temporary file, the failed-task
63
- # information.
64
-
65
- # Part 1: Read exising history from DB
66
- new_history = dataset.history
67
-
68
- # Part 2: Extend history based on temporary-file contents
69
- tmp_history_file = Path(job.working_dir) / HISTORY_FILENAME
70
- try:
71
- with tmp_history_file.open("r") as f:
72
- tmp_file_history = json.load(f)
73
- new_history.extend(tmp_file_history)
74
- except FileNotFoundError:
75
- tmp_file_history = []
76
-
77
- # Part 3/A: Identify failed task, if needed
78
- if failed_wftask is None:
79
- job_wftasks = workflow.task_list[
80
- job.first_task_index : (job.last_task_index + 1) # noqa
81
- ]
82
- tmp_file_wftasks = [
83
- history_item["workflowtask"] for history_item in tmp_file_history
84
- ]
85
- if len(job_wftasks) <= len(tmp_file_wftasks):
86
- n_tasks_job = len(job_wftasks)
87
- n_tasks_tmp = len(tmp_file_wftasks)
88
- logger.error(
89
- "Cannot identify the failed task based on job task list "
90
- f"(length {n_tasks_job}) and temporary-file task list "
91
- f"(length {n_tasks_tmp})."
37
+ with next(get_sync_db()) as db:
38
+ db_dataset = db.get(DatasetV2, dataset_id)
39
+ if len(db_dataset.history) == 0:
40
+ logger.warning(
41
+ f"History for {dataset_id=} is empty. Likely reason: the job "
42
+ "failed before its first task was marked as SUBMITTED. "
43
+ "Continue."
92
44
  )
93
- logger.error("Failed task not appended to history.")
94
- else:
95
- failed_wftask = job_wftasks[len(tmp_file_wftasks)]
96
-
97
- # Part 3/B: Append failed task to history
98
- if failed_wftask is not None:
99
- failed_wftask_dump = failed_wftask.model_dump(exclude={"task"})
100
- failed_wftask_dump["task"] = failed_wftask.task.model_dump()
101
- new_history_item = dict(
102
- workflowtask=failed_wftask_dump,
103
- status=WorkflowTaskStatusTypeV2.FAILED,
104
- parallelization=dict(), # FIXME: re-include parallelization
105
- )
106
- new_history.append(new_history_item)
107
-
108
- return new_history
109
-
110
-
111
- def assemble_images_failed_job(job: JobV2) -> Optional[dict[str, Any]]:
112
- """
113
- Assemble `DatasetV2.images` for a failed workflow-execution.
114
-
115
- Assemble new value of `images` based on the last successful task, i.e.
116
- based on the content of the temporary `IMAGES_FILENAME` file. If the file
117
- is missing, return `None`.
118
-
119
- Argumentss:
120
- job:
121
- The failed `JobV2` object.
122
-
123
- Returns:
124
- The new value of `dataset.images`, or `None` if `IMAGES_FILENAME`
125
- is missing.
126
- """
127
- tmp_file = Path(job.working_dir) / IMAGES_FILENAME
128
- try:
129
- with tmp_file.open("r") as f:
130
- new_images = json.load(f)
131
- return new_images
132
- except FileNotFoundError:
133
- return None
134
-
135
-
136
- def assemble_filters_failed_job(job: JobV2) -> Optional[dict[str, Any]]:
137
- """
138
- Assemble `DatasetV2.filters` for a failed workflow-execution.
139
-
140
- Assemble new value of `filters` based on the last successful task, i.e.
141
- based on the content of the temporary `FILTERS_FILENAME` file. If the file
142
- is missing, return `None`.
143
-
144
- Argumentss:
145
- job:
146
- The failed `JobV2` object.
147
-
148
- Returns:
149
- The new value of `dataset.filters`, or `None` if `FILTERS_FILENAME`
150
- is missing.
151
- """
152
- tmp_file = Path(job.working_dir) / FILTERS_FILENAME
153
- try:
154
- with tmp_file.open("r") as f:
155
- new_filters = json.load(f)
156
- return new_filters
157
- except FileNotFoundError:
158
- return None
45
+ return
46
+ workflowtask_id = db_dataset.history[-1]["workflowtask"]["id"]
47
+ last_item_status = db_dataset.history[-1]["status"]
48
+ if last_item_status != WorkflowTaskStatusTypeV2.SUBMITTED:
49
+ logger.warning(
50
+ "Unexpected branch: "
51
+ f"Last history item, for {workflowtask_id=}, "
52
+ f"has status {last_item_status}. Skip."
53
+ )
54
+ return
55
+ logger.info(f"Setting history item for {workflowtask_id=} to failed.")
56
+ db_dataset.history[-1]["status"] = WorkflowTaskStatusTypeV2.FAILED
57
+ flag_modified(db_dataset, "history")
58
+ db.merge(db_dataset)
59
+ db.commit()
@@ -1,38 +1,27 @@
1
- from copy import copy
2
-
3
1
  from fractal_server.app.runner.v2.deduplicate_list import deduplicate_list
4
2
  from fractal_server.app.runner.v2.task_interface import TaskOutput
5
3
 
6
4
 
7
5
  def merge_outputs(task_outputs: list[TaskOutput]) -> TaskOutput:
8
6
 
7
+ if len(task_outputs) == 0:
8
+ return TaskOutput()
9
+
9
10
  final_image_list_updates = []
10
11
  final_image_list_removals = []
11
- last_new_filters = None
12
12
 
13
- for ind, task_output in enumerate(task_outputs):
13
+ for task_output in task_outputs:
14
14
 
15
15
  final_image_list_updates.extend(task_output.image_list_updates)
16
16
  final_image_list_removals.extend(task_output.image_list_removals)
17
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
-
18
+ # Note: the ordering of `image_list_removals` is not guaranteed
26
19
  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
20
+ final_image_list_removals = list(set(final_image_list_removals))
31
21
 
32
22
  final_output = TaskOutput(
33
23
  image_list_updates=final_image_list_updates,
34
24
  image_list_removals=final_image_list_removals,
35
- **additional_args,
36
25
  )
37
26
 
38
27
  return final_output