fractal-server 2.13.1__py3-none-any.whl → 2.14.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 (60) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/history/__init__.py +4 -0
  3. fractal_server/app/history/image_updates.py +142 -0
  4. fractal_server/app/history/status_enum.py +16 -0
  5. fractal_server/app/models/v2/__init__.py +5 -1
  6. fractal_server/app/models/v2/history.py +53 -0
  7. fractal_server/app/routes/api/v2/__init__.py +2 -2
  8. fractal_server/app/routes/api/v2/_aux_functions.py +78 -0
  9. fractal_server/app/routes/api/v2/dataset.py +12 -9
  10. fractal_server/app/routes/api/v2/history.py +247 -0
  11. fractal_server/app/routes/api/v2/project.py +25 -0
  12. fractal_server/app/routes/api/v2/workflow.py +18 -3
  13. fractal_server/app/routes/api/v2/workflowtask.py +22 -0
  14. fractal_server/app/runner/executors/base_runner.py +114 -0
  15. fractal_server/app/runner/{v2/_local → executors/local}/_local_config.py +3 -3
  16. fractal_server/app/runner/executors/local/_submit_setup.py +54 -0
  17. fractal_server/app/runner/executors/local/runner.py +200 -0
  18. fractal_server/app/runner/executors/{slurm → slurm_common}/_batching.py +1 -1
  19. fractal_server/app/runner/executors/{slurm → slurm_common}/_slurm_config.py +3 -3
  20. fractal_server/app/runner/{v2/_slurm_ssh → executors/slurm_common}/_submit_setup.py +13 -12
  21. fractal_server/app/runner/{v2/_slurm_common → executors/slurm_common}/get_slurm_config.py +9 -15
  22. fractal_server/app/runner/executors/{slurm/ssh → slurm_ssh}/_executor_wait_thread.py +1 -1
  23. fractal_server/app/runner/executors/{slurm/ssh → slurm_ssh}/_slurm_job.py +1 -1
  24. fractal_server/app/runner/executors/{slurm/ssh → slurm_ssh}/executor.py +13 -14
  25. fractal_server/app/runner/executors/{slurm/sudo → slurm_sudo}/_check_jobs_status.py +11 -9
  26. fractal_server/app/runner/executors/{slurm/sudo → slurm_sudo}/_executor_wait_thread.py +3 -3
  27. fractal_server/app/runner/executors/{slurm/sudo → slurm_sudo}/_subprocess_run_as_user.py +2 -68
  28. fractal_server/app/runner/executors/slurm_sudo/runner.py +632 -0
  29. fractal_server/app/runner/task_files.py +70 -96
  30. fractal_server/app/runner/v2/__init__.py +5 -19
  31. fractal_server/app/runner/v2/_local.py +84 -0
  32. fractal_server/app/runner/v2/{_slurm_ssh/__init__.py → _slurm_ssh.py} +10 -13
  33. fractal_server/app/runner/v2/{_slurm_sudo/__init__.py → _slurm_sudo.py} +10 -12
  34. fractal_server/app/runner/v2/runner.py +93 -28
  35. fractal_server/app/runner/v2/runner_functions.py +85 -62
  36. fractal_server/app/runner/v2/runner_functions_low_level.py +20 -20
  37. fractal_server/app/schemas/v2/dataset.py +0 -17
  38. fractal_server/app/schemas/v2/history.py +23 -0
  39. fractal_server/config.py +2 -2
  40. fractal_server/migrations/versions/8223fcef886c_image_status.py +63 -0
  41. fractal_server/migrations/versions/87cd72a537a2_add_historyitem_table.py +68 -0
  42. {fractal_server-2.13.1.dist-info → fractal_server-2.14.0a1.dist-info}/METADATA +1 -1
  43. {fractal_server-2.13.1.dist-info → fractal_server-2.14.0a1.dist-info}/RECORD +53 -47
  44. fractal_server/app/routes/api/v2/status.py +0 -168
  45. fractal_server/app/runner/executors/slurm/sudo/executor.py +0 -1281
  46. fractal_server/app/runner/v2/_local/__init__.py +0 -132
  47. fractal_server/app/runner/v2/_local/_submit_setup.py +0 -52
  48. fractal_server/app/runner/v2/_local/executor.py +0 -100
  49. fractal_server/app/runner/v2/_slurm_sudo/_submit_setup.py +0 -83
  50. fractal_server/app/runner/v2/handle_failed_job.py +0 -59
  51. /fractal_server/app/runner/executors/{slurm → local}/__init__.py +0 -0
  52. /fractal_server/app/runner/executors/{slurm/ssh → slurm_common}/__init__.py +0 -0
  53. /fractal_server/app/runner/executors/{_job_states.py → slurm_common/_job_states.py} +0 -0
  54. /fractal_server/app/runner/executors/{slurm → slurm_common}/remote.py +0 -0
  55. /fractal_server/app/runner/executors/{slurm → slurm_common}/utils_executors.py +0 -0
  56. /fractal_server/app/runner/executors/{slurm/sudo → slurm_ssh}/__init__.py +0 -0
  57. /fractal_server/app/runner/{v2/_slurm_common → executors/slurm_sudo}/__init__.py +0 -0
  58. {fractal_server-2.13.1.dist-info → fractal_server-2.14.0a1.dist-info}/LICENSE +0 -0
  59. {fractal_server-2.13.1.dist-info → fractal_server-2.14.0a1.dist-info}/WHEEL +0 -0
  60. {fractal_server-2.13.1.dist-info → fractal_server-2.14.0a1.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,8 @@ from pathlib import Path
2
2
  from typing import Optional
3
3
  from typing import Union
4
4
 
5
+ from pydantic import BaseModel
6
+
5
7
  from fractal_server.string_tools import sanitize_string
6
8
 
7
9
 
@@ -17,108 +19,80 @@ def task_subfolder_name(order: Union[int, str], task_name: str) -> str:
17
19
  return f"{order}_{task_name_slug}"
18
20
 
19
21
 
20
- class TaskFiles:
22
+ class TaskFiles(BaseModel):
21
23
  """
22
- Group all file paths pertaining to a task
23
-
24
- Attributes:
25
- workflow_dir_local:
26
- Server-owned directory to store all task-execution-related relevant
27
- files. Note: users cannot write directly to this folder.
28
- workflow_dir_remote:
29
- User-side directory with the same scope as `workflow_dir_local`,
30
- and where a user can write.
31
- subfolder_name:
32
- Name of task-specific subfolder
33
- remote_subfolder:
34
- Path to user-side task-specific subfolder
35
- task_name:
36
- Name of the task
37
- task_order:
38
- Positional order of the task within a workflow.
39
- component:
40
- Specific component to run the task for (relevant for tasks to be
41
- executed in parallel over many components).
42
- file_prefix:
43
- Prefix for all task-related files.
44
- args:
45
- Path for input json file.
46
- metadiff:
47
- Path for output json file with metadata update.
48
- out:
49
- Path for task-execution stdout.
50
- err:
51
- Path for task-execution stderr.
24
+ Group all file paths pertaining to a task FIXME
52
25
  """
53
26
 
54
- workflow_dir_local: Path
55
- workflow_dir_remote: Path
56
- remote_subfolder: Path
57
- subfolder_name: str
27
+ # Parent directory
28
+ root_dir_local: Path
29
+ root_dir_remote: Path
30
+
31
+ # Per-wftask
58
32
  task_name: str
59
- task_order: Optional[int] = None
33
+ task_order: int
34
+
35
+ # Per-single-component
60
36
  component: Optional[str] = None
61
37
 
62
- file_prefix: str
63
- file_prefix_with_subfolder: str
64
- args: Path
65
- out: Path
66
- err: Path
67
- log: Path
68
- metadiff: Path
69
-
70
- def __init__(
71
- self,
72
- workflow_dir_local: Path,
73
- workflow_dir_remote: Path,
74
- task_name: str,
75
- task_order: Optional[int] = None,
76
- component: Optional[str] = None,
77
- ):
78
- self.workflow_dir_local = workflow_dir_local
79
- self.workflow_dir_remote = workflow_dir_remote
80
- self.task_order = task_order
81
- self.task_name = task_name
82
- self.component = component
83
-
84
- if self.component is not None:
85
- component_safe = sanitize_string(str(self.component))
86
- component_safe = f"_par_{component_safe}"
87
- else:
88
- component_safe = ""
89
-
90
- if self.task_order is not None:
91
- order = str(self.task_order)
92
- else:
93
- order = "0"
94
- self.file_prefix = f"{order}{component_safe}"
95
- self.subfolder_name = task_subfolder_name(
96
- order=order, task_name=self.task_name
97
- )
98
- self.remote_subfolder = self.workflow_dir_remote / self.subfolder_name
99
- self.args = self.remote_subfolder / f"{self.file_prefix}.args.json"
100
- self.out = self.remote_subfolder / f"{self.file_prefix}.out"
101
- self.err = self.remote_subfolder / f"{self.file_prefix}.err"
102
- self.log = self.remote_subfolder / f"{self.file_prefix}.log"
103
- self.metadiff = (
104
- self.remote_subfolder / f"{self.file_prefix}.metadiff.json"
38
+ def _check_component(self):
39
+ if self.component is None:
40
+ raise ValueError("`component` cannot be None")
41
+
42
+ @property
43
+ def subfolder_name(self) -> str:
44
+ order = str(self.task_order or 0)
45
+ return task_subfolder_name(
46
+ order=order,
47
+ task_name=self.task_name,
105
48
  )
106
49
 
50
+ @property
51
+ def wftask_subfolder_remote(self) -> Path:
52
+ return self.root_dir_remote / self.subfolder_name
107
53
 
108
- def get_task_file_paths(
109
- workflow_dir_local: Path,
110
- workflow_dir_remote: Path,
111
- task_name: str,
112
- task_order: Optional[int] = None,
113
- component: Optional[str] = None,
114
- ) -> TaskFiles:
115
- """
116
- Return the corrisponding TaskFiles object
117
- """
118
- return TaskFiles(
119
- workflow_dir_local=workflow_dir_local,
120
- workflow_dir_remote=workflow_dir_remote,
121
- task_name=task_name,
122
- task_order=task_order,
123
- component=component,
124
- )
54
+ @property
55
+ def wftask_subfolder_local(self) -> Path:
56
+ return self.root_dir_local / self.subfolder_name
57
+
58
+ @property
59
+ def log_file_local(self) -> str:
60
+ self._check_component()
61
+ return (
62
+ self.wftask_subfolder_local / f"{self.component}-log.txt"
63
+ ).as_posix()
64
+
65
+ @property
66
+ def log_file_remote(self) -> str:
67
+ self._check_component()
68
+ return (
69
+ self.wftask_subfolder_remote / f"{self.component}-log.txt"
70
+ ).as_posix()
71
+
72
+ @property
73
+ def args_file_local(self) -> str:
74
+ self._check_component()
75
+ return (
76
+ self.wftask_subfolder_local / f"{self.component}-args.json"
77
+ ).as_posix()
78
+
79
+ @property
80
+ def args_file_remote(self) -> str:
81
+ self._check_component()
82
+ return (
83
+ self.wftask_subfolder_remote / f"{self.component}-args.json"
84
+ ).as_posix()
85
+
86
+ @property
87
+ def metadiff_file_local(self) -> str:
88
+ self._check_component()
89
+ return (
90
+ self.wftask_subfolder_local / f"{self.component}-metadiff.json"
91
+ ).as_posix()
92
+
93
+ @property
94
+ def metadiff_file_remote(self) -> str:
95
+ self._check_component()
96
+ return (
97
+ self.wftask_subfolder_remote / f"{self.component}-metadiff.json"
98
+ ).as_posix()
@@ -27,13 +27,12 @@ from ...models.v2 import WorkflowV2
27
27
  from ...schemas.v2 import JobStatusTypeV2
28
28
  from ..exceptions import JobExecutionError
29
29
  from ..exceptions import TaskExecutionError
30
- from ..executors.slurm.sudo._subprocess_run_as_user import _mkdir_as_user
30
+ from ..executors.slurm_sudo._subprocess_run_as_user import _mkdir_as_user
31
31
  from ..filenames import WORKFLOW_LOG_FILENAME
32
32
  from ..task_files import task_subfolder_name
33
33
  from ._local import process_workflow as local_process_workflow
34
34
  from ._slurm_ssh import process_workflow as slurm_ssh_process_workflow
35
35
  from ._slurm_sudo import process_workflow as slurm_sudo_process_workflow
36
- from .handle_failed_job import mark_last_wftask_as_failed
37
36
  from fractal_server import __VERSION__
38
37
  from fractal_server.app.models import UserSettings
39
38
 
@@ -201,7 +200,7 @@ def submit_workflow(
201
200
  f"{settings.FRACTAL_RUNNER_BACKEND}."
202
201
  )
203
202
 
204
- # Create all tasks subfolders
203
+ # Create all tasks subfolders # FIXME: do this with Runner
205
204
  for order in range(job.first_task_index, job.last_task_index + 1):
206
205
  this_wftask = workflow.task_list[order]
207
206
  task_name = this_wftask.task.name
@@ -219,10 +218,7 @@ def submit_workflow(
219
218
  folder=str(WORKFLOW_DIR_REMOTE / subfolder_name),
220
219
  user=slurm_user,
221
220
  )
222
- else:
223
- # Create local subfolder (with standard permission set)
224
- (WORKFLOW_DIR_LOCAL / subfolder_name).mkdir()
225
- logger.info("Skip remote-subfolder creation")
221
+
226
222
  except Exception as e:
227
223
  error_type = type(e).__name__
228
224
  fail_job(
@@ -345,10 +341,6 @@ def submit_workflow(
345
341
  logger.debug(f'FAILED workflow "{workflow.name}", TaskExecutionError.')
346
342
  logger.info(f'Workflow "{workflow.name}" failed (TaskExecutionError).')
347
343
 
348
- mark_last_wftask_as_failed(
349
- dataset_id=dataset_id,
350
- logger_name=logger_name,
351
- )
352
344
  exception_args_string = "\n".join(e.args)
353
345
  log_msg = (
354
346
  f"TASK ERROR: "
@@ -361,10 +353,7 @@ def submit_workflow(
361
353
  except JobExecutionError as e:
362
354
  logger.debug(f'FAILED workflow "{workflow.name}", JobExecutionError.')
363
355
  logger.info(f'Workflow "{workflow.name}" failed (JobExecutionError).')
364
- mark_last_wftask_as_failed(
365
- dataset_id=dataset_id,
366
- logger_name=logger_name,
367
- )
356
+
368
357
  fail_job(
369
358
  db=db_sync,
370
359
  job=job,
@@ -378,10 +367,7 @@ def submit_workflow(
378
367
  except Exception:
379
368
  logger.debug(f'FAILED workflow "{workflow.name}", unknown error.')
380
369
  logger.info(f'Workflow "{workflow.name}" failed (unkwnon error).')
381
- mark_last_wftask_as_failed(
382
- dataset_id=dataset_id,
383
- logger_name=logger_name,
384
- )
370
+
385
371
  current_traceback = traceback.format_exc()
386
372
  fail_job(
387
373
  db=db_sync,
@@ -0,0 +1,84 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+
4
+ from ...models.v2 import DatasetV2
5
+ from ...models.v2 import WorkflowV2
6
+ from ..executors.local._submit_setup import _local_submit_setup
7
+ from ..executors.local.runner import LocalRunner
8
+ from ..set_start_and_last_task_index import set_start_and_last_task_index
9
+ from .runner import execute_tasks_v2
10
+ from fractal_server.images.models import AttributeFiltersType
11
+
12
+
13
+ def process_workflow(
14
+ *,
15
+ workflow: WorkflowV2,
16
+ dataset: DatasetV2,
17
+ workflow_dir_local: Path,
18
+ workflow_dir_remote: Optional[Path] = None,
19
+ first_task_index: Optional[int] = None,
20
+ last_task_index: Optional[int] = None,
21
+ logger_name: str,
22
+ job_attribute_filters: AttributeFiltersType,
23
+ user_id: int,
24
+ **kwargs,
25
+ ) -> None:
26
+ """
27
+ Run a workflow through
28
+
29
+ Args:
30
+ workflow:
31
+ The workflow to be run
32
+ dataset:
33
+ Initial dataset.
34
+ workflow_dir_local:
35
+ Working directory for this run.
36
+ workflow_dir_remote:
37
+ Working directory for this run, on the user side. This argument is
38
+ present for compatibility with the standard backend interface, but
39
+ for the `local` backend it cannot be different from
40
+ `workflow_dir_local`.
41
+ first_task_index:
42
+ Positional index of the first task to execute; if `None`, start
43
+ from `0`.
44
+ last_task_index:
45
+ Positional index of the last task to execute; if `None`, proceed
46
+ until the last task.
47
+ logger_name: Logger name
48
+ user_id:
49
+
50
+ Raises:
51
+ TaskExecutionError: wrapper for errors raised during tasks' execution
52
+ (positive exit codes).
53
+ JobExecutionError: wrapper for errors raised by the tasks' executors
54
+ (negative exit codes).
55
+ """
56
+
57
+ if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
58
+ raise NotImplementedError(
59
+ "Local backend does not support different directories "
60
+ f"{workflow_dir_local=} and {workflow_dir_remote=}"
61
+ )
62
+
63
+ # Set values of first_task_index and last_task_index
64
+ num_tasks = len(workflow.task_list)
65
+ first_task_index, last_task_index = set_start_and_last_task_index(
66
+ num_tasks,
67
+ first_task_index=first_task_index,
68
+ last_task_index=last_task_index,
69
+ )
70
+
71
+ with LocalRunner(root_dir_local=workflow_dir_local) as runner:
72
+ execute_tasks_v2(
73
+ wf_task_list=workflow.task_list[
74
+ first_task_index : (last_task_index + 1)
75
+ ],
76
+ dataset=dataset,
77
+ runner=runner,
78
+ workflow_dir_local=workflow_dir_local,
79
+ workflow_dir_remote=workflow_dir_local,
80
+ logger_name=logger_name,
81
+ submit_setup_call=_local_submit_setup,
82
+ job_attribute_filters=job_attribute_filters,
83
+ user_id=user_id,
84
+ )
@@ -19,14 +19,14 @@ Executor objects.
19
19
  from pathlib import Path
20
20
  from typing import Optional
21
21
 
22
- from .....ssh._fabric import FractalSSH
23
- from ....models.v2 import DatasetV2
24
- from ....models.v2 import WorkflowV2
25
- from ...exceptions import JobExecutionError
26
- from ...executors.slurm.ssh.executor import FractalSlurmSSHExecutor
27
- from ...set_start_and_last_task_index import set_start_and_last_task_index
28
- from ..runner import execute_tasks_v2
29
- from ._submit_setup import _slurm_submit_setup
22
+ from ....ssh._fabric import FractalSSH
23
+ from ...models.v2 import DatasetV2
24
+ from ...models.v2 import WorkflowV2
25
+ from ..exceptions import JobExecutionError
26
+ from ..executors.slurm_common._submit_setup import _slurm_submit_setup
27
+ from ..executors.slurm_ssh.executor import FractalSlurmSSHExecutor
28
+ from ..set_start_and_last_task_index import set_start_and_last_task_index
29
+ from .runner import execute_tasks_v2
30
30
  from fractal_server.images.models import AttributeFiltersType
31
31
  from fractal_server.logger import set_logger
32
32
 
@@ -46,10 +46,7 @@ def process_workflow(
46
46
  fractal_ssh: FractalSSH,
47
47
  worker_init: Optional[str] = None,
48
48
  user_id: int,
49
- # Not used
50
- user_cache_dir: Optional[str] = None,
51
- slurm_user: Optional[str] = None,
52
- slurm_account: Optional[str] = None,
49
+ **kwargs, # not used
53
50
  ) -> None:
54
51
  """
55
52
  Process workflow (SLURM backend public interface)
@@ -89,7 +86,7 @@ def process_workflow(
89
86
  first_task_index : (last_task_index + 1)
90
87
  ],
91
88
  dataset=dataset,
92
- executor=executor,
89
+ runner=executor,
93
90
  workflow_dir_local=workflow_dir_local,
94
91
  workflow_dir_remote=workflow_dir_remote,
95
92
  logger_name=logger_name,
@@ -19,12 +19,12 @@ Executor objects.
19
19
  from pathlib import Path
20
20
  from typing import Optional
21
21
 
22
- from ....models.v2 import DatasetV2
23
- from ....models.v2 import WorkflowV2
24
- from ...executors.slurm.sudo.executor import FractalSlurmSudoExecutor
25
- from ...set_start_and_last_task_index import set_start_and_last_task_index
26
- from ..runner import execute_tasks_v2
27
- from ._submit_setup import _slurm_submit_setup
22
+ from ...models.v2 import DatasetV2
23
+ from ...models.v2 import WorkflowV2
24
+ from ..executors.slurm_common._submit_setup import _slurm_submit_setup
25
+ from ..executors.slurm_sudo.runner import RunnerSlurmSudo
26
+ from ..set_start_and_last_task_index import set_start_and_last_task_index
27
+ from .runner import execute_tasks_v2
28
28
  from fractal_server.images.models import AttributeFiltersType
29
29
 
30
30
 
@@ -65,13 +65,11 @@ def process_workflow(
65
65
  if isinstance(worker_init, str):
66
66
  worker_init = worker_init.split("\n")
67
67
 
68
- with FractalSlurmSudoExecutor(
69
- debug=True,
70
- keep_logs=True,
68
+ with RunnerSlurmSudo(
71
69
  slurm_user=slurm_user,
72
70
  user_cache_dir=user_cache_dir,
73
- workflow_dir_local=workflow_dir_local,
74
- workflow_dir_remote=workflow_dir_remote,
71
+ root_dir_local=workflow_dir_local,
72
+ root_dir_remote=workflow_dir_remote,
75
73
  common_script_lines=worker_init,
76
74
  slurm_account=slurm_account,
77
75
  ) as executor:
@@ -80,7 +78,7 @@ def process_workflow(
80
78
  first_task_index : (last_task_index + 1)
81
79
  ],
82
80
  dataset=dataset,
83
- executor=executor,
81
+ runner=executor,
84
82
  workflow_dir_local=workflow_dir_local,
85
83
  workflow_dir_remote=workflow_dir_remote,
86
84
  logger_name=logger_name,
@@ -1,5 +1,5 @@
1
+ import json
1
2
  import logging
2
- from concurrent.futures import ThreadPoolExecutor
3
3
  from copy import copy
4
4
  from copy import deepcopy
5
5
  from pathlib import Path
@@ -18,11 +18,14 @@ 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 HistoryItemImageStatus
21
22
  from fractal_server.app.models.v2 import AccountingRecord
22
23
  from fractal_server.app.models.v2 import DatasetV2
24
+ from fractal_server.app.models.v2 import HistoryItemV2
25
+ from fractal_server.app.models.v2 import ImageStatus
26
+ from fractal_server.app.models.v2 import TaskGroupV2
23
27
  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
28
+ from fractal_server.app.runner.executors.base_runner import BaseRunner
26
29
  from fractal_server.images.models import AttributeFiltersType
27
30
  from fractal_server.images.tools import merge_type_filters
28
31
 
@@ -31,7 +34,7 @@ def execute_tasks_v2(
31
34
  *,
32
35
  wf_task_list: list[WorkflowTaskV2],
33
36
  dataset: DatasetV2,
34
- executor: ThreadPoolExecutor,
37
+ runner: BaseRunner,
35
38
  user_id: int,
36
39
  workflow_dir_local: Path,
37
40
  workflow_dir_remote: Optional[Path] = None,
@@ -43,8 +46,8 @@ def execute_tasks_v2(
43
46
 
44
47
  if not workflow_dir_local.exists():
45
48
  logger.warning(
46
- f"Now creating {workflow_dir_local}, "
47
- "but it should have already happened."
49
+ f"Now creating {workflow_dir_local}, but it "
50
+ "should have already happened."
48
51
  )
49
52
  workflow_dir_local.mkdir()
50
53
 
@@ -60,66 +63,116 @@ def execute_tasks_v2(
60
63
 
61
64
  # PRE TASK EXECUTION
62
65
 
63
- # Get filtered images
66
+ # Filter images by types and attributes (in two steps)
64
67
  type_filters = copy(current_dataset_type_filters)
65
68
  type_filters_patch = merge_type_filters(
66
69
  task_input_types=task.input_types,
67
70
  wftask_type_filters=wftask.type_filters,
68
71
  )
69
72
  type_filters.update(type_filters_patch)
70
- filtered_images = filter_image_list(
73
+ type_filtered_images = filter_image_list(
71
74
  images=tmp_images,
72
75
  type_filters=type_filters,
76
+ attribute_filters=None,
77
+ )
78
+ filtered_images = filter_image_list(
79
+ images=type_filtered_images,
80
+ type_filters=None,
73
81
  attribute_filters=job_attribute_filters,
74
82
  )
75
83
 
76
- # First, set status SUBMITTED in dataset.history for each wftask
84
+ # Create history item
77
85
  with next(get_sync_db()) as db:
78
- db_dataset = db.get(DatasetV2, dataset.id)
79
- new_history_item = _DatasetHistoryItemV2(
80
- workflowtask=dict(
81
- **wftask.model_dump(exclude={"task"}),
82
- task=wftask.task.model_dump(),
83
- ),
84
- status=WorkflowTaskStatusTypeV2.SUBMITTED,
85
- parallelization=dict(), # FIXME: re-include parallelization
86
- ).model_dump()
87
- db_dataset.history.append(new_history_item)
88
- flag_modified(db_dataset, "history")
89
- db.merge(db_dataset)
86
+ workflowtask_dump = dict(
87
+ **wftask.model_dump(exclude={"task"}),
88
+ task=wftask.task.model_dump(),
89
+ )
90
+ # Exclude timestamps since they'd need to be serialized properly
91
+ task_group = db.get(TaskGroupV2, wftask.task.taskgroupv2_id)
92
+ task_group_dump = task_group.model_dump(
93
+ exclude={
94
+ "timestamp_created",
95
+ "timestamp_last_used",
96
+ }
97
+ )
98
+ parameters_hash = str(
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(
112
+ dataset_id=dataset.id,
113
+ workflowtask_id=wftask.id,
114
+ workflowtask_dump=workflowtask_dump,
115
+ task_group_dump=task_group_dump,
116
+ parameters_hash=parameters_hash,
117
+ num_available_images=len(type_filtered_images),
118
+ num_current_images=len(filtered_images),
119
+ images=images,
120
+ )
121
+ db.add(history_item)
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
+ )
90
133
  db.commit()
134
+ db.refresh(history_item)
135
+ history_item_id = history_item.id
136
+
91
137
  # TASK EXECUTION (V2)
92
138
  if task.type == "non_parallel":
93
- current_task_output, num_tasks = run_v2_task_non_parallel(
139
+ (
140
+ current_task_output,
141
+ num_tasks,
142
+ exceptions,
143
+ ) = run_v2_task_non_parallel(
94
144
  images=filtered_images,
95
145
  zarr_dir=zarr_dir,
96
146
  wftask=wftask,
97
147
  task=task,
98
148
  workflow_dir_local=workflow_dir_local,
99
149
  workflow_dir_remote=workflow_dir_remote,
100
- executor=executor,
150
+ executor=runner,
101
151
  submit_setup_call=submit_setup_call,
152
+ history_item_id=history_item_id,
102
153
  )
103
154
  elif task.type == "parallel":
104
- current_task_output, num_tasks = run_v2_task_parallel(
155
+ current_task_output, num_tasks, exceptions = run_v2_task_parallel(
105
156
  images=filtered_images,
106
157
  wftask=wftask,
107
158
  task=task,
108
159
  workflow_dir_local=workflow_dir_local,
109
160
  workflow_dir_remote=workflow_dir_remote,
110
- executor=executor,
161
+ executor=runner,
111
162
  submit_setup_call=submit_setup_call,
163
+ history_item_id=history_item_id,
112
164
  )
113
165
  elif task.type == "compound":
114
- current_task_output, num_tasks = run_v2_task_compound(
166
+ current_task_output, num_tasks, exceptions = run_v2_task_compound(
115
167
  images=filtered_images,
116
168
  zarr_dir=zarr_dir,
117
169
  wftask=wftask,
118
170
  task=task,
119
171
  workflow_dir_local=workflow_dir_local,
120
172
  workflow_dir_remote=workflow_dir_remote,
121
- executor=executor,
173
+ executor=runner,
122
174
  submit_setup_call=submit_setup_call,
175
+ history_item_id=history_item_id,
123
176
  )
124
177
  else:
125
178
  raise ValueError(f"Unexpected error: Invalid {task.type=}.")
@@ -145,6 +198,8 @@ def execute_tasks_v2(
145
198
  # Update image list
146
199
  num_new_images = 0
147
200
  current_task_output.check_zarr_urls_are_unique()
201
+ # FIXME: Introduce for loop over task outputs, and processe them sequentially
202
+ # each failure should lead to an update of the specific image status
148
203
  for image_obj in current_task_output.image_list_updates:
149
204
  image = image_obj.model_dump()
150
205
  # Edit existing image
@@ -270,7 +325,6 @@ def execute_tasks_v2(
270
325
  # information
271
326
  with next(get_sync_db()) as db:
272
327
  db_dataset = db.get(DatasetV2, dataset.id)
273
- db_dataset.history[-1]["status"] = WorkflowTaskStatusTypeV2.DONE
274
328
  db_dataset.type_filters = current_dataset_type_filters
275
329
  db_dataset.images = tmp_images
276
330
  for attribute_name in [
@@ -291,4 +345,15 @@ def execute_tasks_v2(
291
345
  db.add(record)
292
346
  db.commit()
293
347
 
348
+ if exceptions != {}:
349
+ logger.error(
350
+ f'END {wftask.order}-th task (name="{task_name}") '
351
+ "- ERROR."
352
+ )
353
+ # Raise first error
354
+ for key, value in exceptions.items():
355
+ raise JobExecutionError(
356
+ info=(f"An error occurred.\nOriginal error:\n{value}")
357
+ )
358
+
294
359
  logger.debug(f'END {wftask.order}-th task (name="{task_name}")')