fractal-server 2.10.5__py3-none-any.whl → 2.11.0a2__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 (40) 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/images.py +29 -6
  8. fractal_server/app/routes/api/v2/status.py +20 -20
  9. fractal_server/app/routes/api/v2/submit.py +5 -1
  10. fractal_server/app/routes/api/v2/workflowtask.py +3 -3
  11. fractal_server/app/runner/filenames.py +2 -4
  12. fractal_server/app/runner/v1/_common.py +4 -4
  13. fractal_server/app/runner/v1/handle_failed_job.py +4 -4
  14. fractal_server/app/runner/v2/__init__.py +11 -65
  15. fractal_server/app/runner/v2/_local/__init__.py +12 -17
  16. fractal_server/app/runner/v2/_local_experimental/__init__.py +11 -20
  17. fractal_server/app/runner/v2/_slurm_ssh/__init__.py +14 -16
  18. fractal_server/app/runner/v2/_slurm_sudo/__init__.py +12 -14
  19. fractal_server/app/runner/v2/handle_failed_job.py +31 -130
  20. fractal_server/app/runner/v2/merge_outputs.py +13 -16
  21. fractal_server/app/runner/v2/runner.py +63 -72
  22. fractal_server/app/runner/v2/task_interface.py +41 -2
  23. fractal_server/app/schemas/_filter_validators.py +47 -0
  24. fractal_server/app/schemas/_validators.py +13 -2
  25. fractal_server/app/schemas/v2/dataset.py +58 -12
  26. fractal_server/app/schemas/v2/dumps.py +6 -8
  27. fractal_server/app/schemas/v2/job.py +14 -0
  28. fractal_server/app/schemas/v2/task.py +9 -9
  29. fractal_server/app/schemas/v2/task_group.py +2 -2
  30. fractal_server/app/schemas/v2/workflowtask.py +42 -19
  31. fractal_server/data_migrations/2_11_0.py +67 -0
  32. fractal_server/images/__init__.py +0 -1
  33. fractal_server/images/models.py +12 -35
  34. fractal_server/images/tools.py +29 -13
  35. fractal_server/migrations/versions/db09233ad13a_split_filters_and_keep_old_columns.py +96 -0
  36. {fractal_server-2.10.5.dist-info → fractal_server-2.11.0a2.dist-info}/METADATA +1 -1
  37. {fractal_server-2.10.5.dist-info → fractal_server-2.11.0a2.dist-info}/RECORD +40 -37
  38. {fractal_server-2.10.5.dist-info → fractal_server-2.11.0a2.dist-info}/LICENSE +0 -0
  39. {fractal_server-2.10.5.dist-info → fractal_server-2.11.0a2.dist-info}/WHEEL +0 -0
  40. {fractal_server-2.10.5.dist-info → fractal_server-2.11.0a2.dist-info}/entry_points.txt +0 -0
@@ -1 +1 @@
1
- __VERSION__ = "2.10.5"
1
+ __VERSION__ = "2.11.0a2"
@@ -11,6 +11,7 @@ from sqlmodel import Relationship
11
11
  from sqlmodel import SQLModel
12
12
 
13
13
  from ....utils import get_timestamp
14
+ from fractal_server.images.models import AttributeFiltersType
14
15
 
15
16
 
16
17
  class DatasetV2(SQLModel, table=True):
@@ -41,12 +42,14 @@ class DatasetV2(SQLModel, table=True):
41
42
  sa_column=Column(JSON, server_default="[]", nullable=False)
42
43
  )
43
44
 
44
- filters: dict[Literal["attributes", "types"], dict[str, Any]] = Field(
45
- sa_column=Column(
46
- JSON,
47
- nullable=False,
48
- server_default='{"attributes": {}, "types": {}}',
49
- )
45
+ filters: Optional[
46
+ dict[Literal["attributes", "types"], dict[str, Any]]
47
+ ] = Field(sa_column=Column(JSON, nullable=True, server_default="null"))
48
+ type_filters: dict[str, bool] = Field(
49
+ sa_column=Column(JSON, nullable=False, server_default="{}")
50
+ )
51
+ attribute_filters: AttributeFiltersType = Field(
52
+ sa_column=Column(JSON, nullable=False, server_default="{}")
50
53
  )
51
54
 
52
55
  @property
@@ -10,6 +10,7 @@ from sqlmodel import SQLModel
10
10
 
11
11
  from ....utils import get_timestamp
12
12
  from ...schemas.v2 import JobStatusTypeV2
13
+ from fractal_server.images.models import AttributeFiltersType
13
14
 
14
15
 
15
16
  class JobV2(SQLModel, table=True):
@@ -49,3 +50,7 @@ class JobV2(SQLModel, table=True):
49
50
  )
50
51
  status: str = JobStatusTypeV2.SUBMITTED
51
52
  log: Optional[str] = None
53
+
54
+ attribute_filters: AttributeFiltersType = Field(
55
+ sa_column=Column(JSON, nullable=False, server_default="{}")
56
+ )
@@ -25,14 +25,11 @@ class WorkflowTaskV2(SQLModel, table=True):
25
25
  args_parallel: Optional[dict[str, Any]] = Field(sa_column=Column(JSON))
26
26
  args_non_parallel: Optional[dict[str, Any]] = Field(sa_column=Column(JSON))
27
27
 
28
- input_filters: dict[
29
- Literal["attributes", "types"], dict[str, Any]
30
- ] = Field(
31
- sa_column=Column(
32
- JSON,
33
- nullable=False,
34
- server_default='{"attributes": {}, "types": {}}',
35
- )
28
+ input_filters: Optional[
29
+ dict[Literal["attributes", "types"], dict[str, Any]]
30
+ ] = Field(sa_column=Column(JSON, nullable=True, server_default="null"))
31
+ type_filters: dict[str, bool] = Field(
32
+ sa_column=Column(JSON, nullable=False, server_default="{}")
36
33
  )
37
34
 
38
35
  # Task
@@ -17,7 +17,7 @@ from ....models.v1 import ApplyWorkflow
17
17
  from ....models.v1 import Dataset
18
18
  from ....models.v1 import Project
19
19
  from ....models.v1 import Resource
20
- from ....runner.filenames import HISTORY_FILENAME
20
+ from ....runner.filenames import HISTORY_FILENAME_V1
21
21
  from ....schemas.v1 import DatasetCreateV1
22
22
  from ....schemas.v1 import DatasetReadV1
23
23
  from ....schemas.v1 import DatasetStatusReadV1
@@ -511,7 +511,7 @@ async def get_workflowtask_status(
511
511
  # Highest priority: Read status updates coming from the running-job
512
512
  # temporary file. Note: this file only contains information on
513
513
  # WorkflowTask's that ran through successfully
514
- tmp_file = Path(running_job.working_dir) / HISTORY_FILENAME
514
+ tmp_file = Path(running_job.working_dir) / HISTORY_FILENAME_V1
515
515
  try:
516
516
  with tmp_file.open("r") as f:
517
517
  history = json.load(f)
@@ -21,7 +21,6 @@ from ....models.v2 import TaskV2
21
21
  from ....models.v2 import WorkflowTaskV2
22
22
  from ....models.v2 import WorkflowV2
23
23
  from ....schemas.v2 import JobStatusTypeV2
24
- from fractal_server.images import Filters
25
24
 
26
25
 
27
26
  async def _get_project_check_owner(
@@ -336,7 +335,7 @@ async def _workflow_insert_task(
336
335
  meta_non_parallel: Optional[dict[str, Any]] = None,
337
336
  args_non_parallel: Optional[dict[str, Any]] = None,
338
337
  args_parallel: Optional[dict[str, Any]] = None,
339
- input_filters: Optional[Filters] = None,
338
+ type_filters: Optional[dict[str, bool]] = None,
340
339
  db: AsyncSession,
341
340
  ) -> WorkflowTaskV2:
342
341
  """
@@ -350,7 +349,7 @@ async def _workflow_insert_task(
350
349
  meta_non_parallel:
351
350
  args_non_parallel:
352
351
  args_parallel:
353
- input_filters:
352
+ type_filters:
354
353
  db:
355
354
  """
356
355
  db_workflow = await db.get(WorkflowV2, workflow_id)
@@ -376,12 +375,6 @@ async def _workflow_insert_task(
376
375
  if final_meta_non_parallel == {}:
377
376
  final_meta_non_parallel = None
378
377
 
379
- # Prepare input_filters attribute
380
- if input_filters is None:
381
- input_filters_kwarg = {}
382
- else:
383
- input_filters_kwarg = dict(input_filters=input_filters)
384
-
385
378
  # Create DB entry
386
379
  wf_task = WorkflowTaskV2(
387
380
  task_type=task_type,
@@ -390,7 +383,7 @@ async def _workflow_insert_task(
390
383
  args_parallel=args_parallel,
391
384
  meta_parallel=final_meta_parallel,
392
385
  meta_non_parallel=final_meta_non_parallel,
393
- **input_filters_kwarg,
386
+ type_filters=(type_filters or dict()),
394
387
  )
395
388
  db_workflow.task_list.append(wf_task)
396
389
  flag_modified(db_workflow, "task_list")
@@ -8,6 +8,8 @@ from fastapi import Response
8
8
  from fastapi import status
9
9
  from pydantic import BaseModel
10
10
  from pydantic import Field
11
+ from pydantic import root_validator
12
+ from pydantic import validator
11
13
  from sqlalchemy.orm.attributes import flag_modified
12
14
 
13
15
  from ._aux_functions import _get_dataset_check_owner
@@ -15,9 +17,14 @@ from fractal_server.app.db import AsyncSession
15
17
  from fractal_server.app.db import get_async_db
16
18
  from fractal_server.app.models import UserOAuth
17
19
  from fractal_server.app.routes.auth import current_active_user
18
- from fractal_server.images import Filters
20
+ from fractal_server.app.schemas._filter_validators import (
21
+ validate_attribute_filters,
22
+ )
23
+ from fractal_server.app.schemas._filter_validators import validate_type_filters
24
+ from fractal_server.app.schemas._validators import root_validate_dict_keys
19
25
  from fractal_server.images import SingleImage
20
26
  from fractal_server.images import SingleImageUpdate
27
+ from fractal_server.images.models import AttributeFiltersType
21
28
  from fractal_server.images.tools import find_image_by_zarr_url
22
29
  from fractal_server.images.tools import match_filter
23
30
 
@@ -38,7 +45,18 @@ class ImagePage(BaseModel):
38
45
 
39
46
  class ImageQuery(BaseModel):
40
47
  zarr_url: Optional[str]
41
- filters: Filters = Field(default_factory=Filters)
48
+ type_filters: dict[str, bool] = Field(default_factory=dict)
49
+ attribute_filters: AttributeFiltersType = Field(default_factory=dict)
50
+
51
+ _dict_keys = root_validator(pre=True, allow_reuse=True)(
52
+ root_validate_dict_keys
53
+ )
54
+ _type_filters = validator("type_filters", allow_reuse=True)(
55
+ validate_type_filters
56
+ )
57
+ _attribute_filters = validator("attribute_filters", allow_reuse=True)(
58
+ validate_attribute_filters
59
+ )
42
60
 
43
61
 
44
62
  @router.post(
@@ -124,7 +142,11 @@ async def query_dataset_images(
124
142
  images = [
125
143
  image
126
144
  for image in images
127
- if match_filter(image, Filters(**dataset.filters))
145
+ if match_filter(
146
+ image=image,
147
+ type_filters=dataset.type_filters,
148
+ attribute_filters=dataset.attribute_filters,
149
+ )
128
150
  ]
129
151
 
130
152
  attributes = {}
@@ -154,13 +176,14 @@ async def query_dataset_images(
154
176
  else:
155
177
  images = [image]
156
178
 
157
- if query.filters.attributes or query.filters.types:
179
+ if query.attribute_filters or query.type_filters:
158
180
  images = [
159
181
  image
160
182
  for image in images
161
183
  if match_filter(
162
- image,
163
- Filters(**query.filters.dict()),
184
+ image=image,
185
+ type_filters=query.type_filters,
186
+ attribute_filters=query.attribute_filters,
164
187
  )
165
188
  ]
166
189
 
@@ -1,5 +1,3 @@
1
- import json
2
- from pathlib import Path
3
1
  from typing import Optional
4
2
 
5
3
  from fastapi import APIRouter
@@ -18,7 +16,6 @@ from ._aux_functions import _get_submitted_jobs_statement
18
16
  from ._aux_functions import _get_workflow_check_owner
19
17
  from fractal_server.app.models import UserOAuth
20
18
  from fractal_server.app.routes.auth import current_active_user
21
- from fractal_server.app.runner.filenames import HISTORY_FILENAME
22
19
 
23
20
  router = APIRouter()
24
21
 
@@ -98,8 +95,8 @@ async def get_workflowtask_status(
98
95
  if running_job is None:
99
96
  # If no job is running, the chronological-last history item is also the
100
97
  # positional-last workflow task to be included in the response.
101
- if len(dataset.history) > 0:
102
- last_valid_wftask_id = dataset.history[-1]["workflowtask"]["id"]
98
+ if len(history) > 0:
99
+ last_valid_wftask_id = history[-1]["workflowtask"]["id"]
103
100
  else:
104
101
  last_valid_wftask_id = None
105
102
  else:
@@ -109,7 +106,24 @@ async def get_workflowtask_status(
109
106
  # as "submitted"
110
107
  start = running_job.first_task_index
111
108
  end = running_job.last_task_index + 1
112
- for wftask in workflow.task_list[start:end]:
109
+
110
+ running_job_wftasks = workflow.task_list[start:end]
111
+ running_job_statuses = [
112
+ workflow_tasks_status_dict.get(wft.id, None)
113
+ for wft in running_job_wftasks
114
+ ]
115
+ try:
116
+ first_submitted_index = running_job_statuses.index(
117
+ WorkflowTaskStatusTypeV2.SUBMITTED
118
+ )
119
+ except ValueError:
120
+ logger.warning(
121
+ f"Job {running_job.id} is submitted but its task list does "
122
+ f"not contain a {WorkflowTaskStatusTypeV2.SUBMITTED} task."
123
+ )
124
+ first_submitted_index = 0
125
+
126
+ for wftask in running_job_wftasks[first_submitted_index:]:
113
127
  workflow_tasks_status_dict[
114
128
  wftask.id
115
129
  ] = WorkflowTaskStatusTypeV2.SUBMITTED
@@ -133,20 +147,6 @@ async def get_workflowtask_status(
133
147
  last_valid_wftask_id = None
134
148
  logger.warning(f"Now setting {last_valid_wftask_id=}.")
135
149
 
136
- # Highest priority: Read status updates coming from the running-job
137
- # temporary file. Note: this file only contains information on
138
- # WorkflowTask's that ran through successfully.
139
- tmp_file = Path(running_job.working_dir) / HISTORY_FILENAME
140
- try:
141
- with tmp_file.open("r") as f:
142
- history = json.load(f)
143
- except FileNotFoundError:
144
- history = []
145
- for history_item in history:
146
- wftask_id = history_item["workflowtask"]["id"]
147
- wftask_status = history_item["status"]
148
- workflow_tasks_status_dict[wftask_id] = wftask_status
149
-
150
150
  # Based on previously-gathered information, clean up the response body
151
151
  clean_workflow_tasks_status_dict = {}
152
152
  for wf_task in workflow.task_list:
@@ -159,7 +159,11 @@ async def apply_workflow(
159
159
  dataset_id=dataset_id,
160
160
  workflow_id=workflow_id,
161
161
  user_email=user.email,
162
- dataset_dump=json.loads(dataset.json(exclude={"images", "history"})),
162
+ # The 'filters' field is not supported any more but still exists as a
163
+ # database column, therefore we manually exclude it from dumps.
164
+ dataset_dump=json.loads(
165
+ dataset.json(exclude={"images", "history", "filters"})
166
+ ),
163
167
  workflow_dump=json.loads(workflow.json(exclude={"task_list"})),
164
168
  project_dump=json.loads(project.json(exclude={"user_list"})),
165
169
  **job_create.dict(),
@@ -109,7 +109,7 @@ async def replace_workflowtask(
109
109
  task_type=task.type,
110
110
  task=task,
111
111
  # old-task values
112
- input_filters=old_workflow_task.input_filters,
112
+ type_filters=old_workflow_task.type_filters,
113
113
  # possibly new values
114
114
  args_non_parallel=_args_non_parallel,
115
115
  args_parallel=_args_parallel,
@@ -183,7 +183,7 @@ async def create_workflowtask(
183
183
  meta_parallel=new_task.meta_parallel,
184
184
  args_non_parallel=new_task.args_non_parallel,
185
185
  args_parallel=new_task.args_parallel,
186
- input_filters=new_task.input_filters,
186
+ type_filters=new_task.type_filters,
187
187
  db=db,
188
188
  )
189
189
 
@@ -274,7 +274,7 @@ async def update_workflowtask(
274
274
  if not actual_args:
275
275
  actual_args = None
276
276
  setattr(db_wf_task, key, actual_args)
277
- elif key in ["meta_parallel", "meta_non_parallel", "input_filters"]:
277
+ elif key in ["meta_parallel", "meta_non_parallel", "type_filters"]:
278
278
  setattr(db_wf_task, key, value)
279
279
  else:
280
280
  raise HTTPException(
@@ -1,6 +1,4 @@
1
- HISTORY_FILENAME = "history.json"
2
- FILTERS_FILENAME = "filters.json"
3
- IMAGES_FILENAME = "images.json"
4
- METADATA_FILENAME = "metadata.json"
1
+ HISTORY_FILENAME_V1 = "history.json"
2
+ METADATA_FILENAME_V1 = "metadata.json"
5
3
  SHUTDOWN_FILENAME = "shutdown"
6
4
  WORKFLOW_LOG_FILENAME = "workflow.log"
@@ -28,8 +28,8 @@ from ..exceptions import JobExecutionError
28
28
  from ..exceptions import TaskExecutionError
29
29
  from .common import TaskParameters
30
30
  from .common import write_args_file
31
- from fractal_server.app.runner.filenames import HISTORY_FILENAME
32
- from fractal_server.app.runner.filenames import METADATA_FILENAME
31
+ from fractal_server.app.runner.filenames import HISTORY_FILENAME_V1
32
+ from fractal_server.app.runner.filenames import METADATA_FILENAME_V1
33
33
  from fractal_server.app.runner.task_files import get_task_file_paths
34
34
  from fractal_server.string_tools import validate_cmd
35
35
 
@@ -610,11 +610,11 @@ def execute_tasks(
610
610
  )
611
611
 
612
612
  # Write most recent metadata to METADATA_FILENAME
613
- with open(workflow_dir_local / METADATA_FILENAME, "w") as f:
613
+ with open(workflow_dir_local / METADATA_FILENAME_V1, "w") as f:
614
614
  json.dump(current_task_pars.metadata, f, indent=2)
615
615
 
616
616
  # Write most recent metadata to HISTORY_FILENAME
617
- with open(workflow_dir_local / HISTORY_FILENAME, "w") as f:
617
+ with open(workflow_dir_local / HISTORY_FILENAME_V1, "w") as f:
618
618
  json.dump(current_task_pars.history, f, indent=2)
619
619
 
620
620
  return current_task_pars
@@ -24,8 +24,8 @@ from ...models.v1 import Dataset
24
24
  from ...models.v1 import Workflow
25
25
  from ...models.v1 import WorkflowTask
26
26
  from ...schemas.v1 import WorkflowTaskStatusTypeV1
27
- from ..filenames import HISTORY_FILENAME
28
- from ..filenames import METADATA_FILENAME
27
+ from ..filenames import HISTORY_FILENAME_V1
28
+ from ..filenames import METADATA_FILENAME_V1
29
29
 
30
30
 
31
31
  def assemble_history_failed_job(
@@ -64,7 +64,7 @@ def assemble_history_failed_job(
64
64
  new_history = output_dataset.history
65
65
 
66
66
  # Part 2: Extend history based on tmp_metadata_file
67
- tmp_history_file = Path(job.working_dir) / HISTORY_FILENAME
67
+ tmp_history_file = Path(job.working_dir) / HISTORY_FILENAME_V1
68
68
  try:
69
69
  with tmp_history_file.open("r") as f:
70
70
  tmp_file_history = json.load(f)
@@ -129,7 +129,7 @@ def assemble_meta_failed_job(
129
129
  """
130
130
 
131
131
  new_meta = deepcopy(output_dataset.meta)
132
- metadata_file = Path(job.working_dir) / METADATA_FILENAME
132
+ metadata_file = Path(job.working_dir) / METADATA_FILENAME_V1
133
133
  try:
134
134
  with metadata_file.open("r") as f:
135
135
  metadata_update = json.load(f)
@@ -11,7 +11,6 @@ from pathlib import Path
11
11
  from typing import Optional
12
12
 
13
13
  from sqlalchemy.orm import Session as DBSyncSession
14
- from sqlalchemy.orm.attributes import flag_modified
15
14
 
16
15
  from ....config import get_settings
17
16
  from ....logger import get_logger
@@ -24,7 +23,6 @@ from ....zip_tools import _zip_folder_to_file_and_remove
24
23
  from ...db import DB
25
24
  from ...models.v2 import DatasetV2
26
25
  from ...models.v2 import JobV2
27
- from ...models.v2 import WorkflowTaskV2
28
26
  from ...models.v2 import WorkflowV2
29
27
  from ...schemas.v2 import JobStatusTypeV2
30
28
  from ..exceptions import JobExecutionError
@@ -38,12 +36,11 @@ from ._local_experimental import (
38
36
  )
39
37
  from ._slurm_ssh import process_workflow as slurm_ssh_process_workflow
40
38
  from ._slurm_sudo import process_workflow as slurm_sudo_process_workflow
41
- from .handle_failed_job import assemble_filters_failed_job
42
- from .handle_failed_job import assemble_history_failed_job
43
- from .handle_failed_job import assemble_images_failed_job
39
+ from .handle_failed_job import mark_last_wftask_as_failed
44
40
  from fractal_server import __VERSION__
45
41
  from fractal_server.app.models import UserSettings
46
42
 
43
+
47
44
  _backends = {}
48
45
  _backends["local"] = local_process_workflow
49
46
  _backends["slurm"] = slurm_sudo_process_workflow
@@ -115,7 +112,6 @@ async def submit_workflow(
115
112
  logger = set_logger(logger_name=logger_name)
116
113
 
117
114
  with next(DB.get_sync_db()) as db_sync:
118
-
119
115
  try:
120
116
  job: Optional[JobV2] = db_sync.get(JobV2, job_id)
121
117
  dataset: Optional[DatasetV2] = db_sync.get(DatasetV2, dataset_id)
@@ -322,7 +318,7 @@ async def submit_workflow(
322
318
  db_sync = next(DB.get_sync_db())
323
319
  db_sync.close()
324
320
 
325
- new_dataset_attributes = await process_workflow(
321
+ await process_workflow(
326
322
  workflow=workflow,
327
323
  dataset=dataset,
328
324
  workflow_dir_local=WORKFLOW_DIR_LOCAL,
@@ -331,6 +327,7 @@ async def submit_workflow(
331
327
  worker_init=worker_init,
332
328
  first_task_index=job.first_task_index,
333
329
  last_task_index=job.last_task_index,
330
+ job_attribute_filters=job.attribute_filters,
334
331
  **backend_specific_kwargs,
335
332
  )
336
333
 
@@ -340,14 +337,6 @@ async def submit_workflow(
340
337
  )
341
338
  logger.debug(f'END workflow "{workflow.name}"')
342
339
 
343
- # Update dataset attributes, in case of successful execution
344
- dataset.history.extend(new_dataset_attributes["history"])
345
- dataset.filters = new_dataset_attributes["filters"]
346
- dataset.images = new_dataset_attributes["images"]
347
- for attribute_name in ["filters", "history", "images"]:
348
- flag_modified(dataset, attribute_name)
349
- db_sync.merge(dataset)
350
-
351
340
  # Update job DB entry
352
341
  job.status = JobStatusTypeV2.DONE
353
342
  job.end_timestamp = get_timestamp()
@@ -358,28 +347,13 @@ async def submit_workflow(
358
347
  db_sync.commit()
359
348
 
360
349
  except TaskExecutionError as e:
361
-
362
350
  logger.debug(f'FAILED workflow "{workflow.name}", TaskExecutionError.')
363
351
  logger.info(f'Workflow "{workflow.name}" failed (TaskExecutionError).')
364
352
 
365
- # Read dataset attributes produced by the last successful task, and
366
- # update the DB dataset accordingly
367
- failed_wftask = db_sync.get(WorkflowTaskV2, e.workflow_task_id)
368
- dataset.history = assemble_history_failed_job(
369
- job,
370
- dataset,
371
- workflow,
353
+ mark_last_wftask_as_failed(
354
+ dataset_id=dataset_id,
372
355
  logger_name=logger_name,
373
- failed_wftask=failed_wftask,
374
356
  )
375
- latest_filters = assemble_filters_failed_job(job)
376
- if latest_filters is not None:
377
- dataset.filters = latest_filters
378
- latest_images = assemble_images_failed_job(job)
379
- if latest_images is not None:
380
- dataset.images = latest_images
381
- db_sync.merge(dataset)
382
-
383
357
  exception_args_string = "\n".join(e.args)
384
358
  log_msg = (
385
359
  f"TASK ERROR: "
@@ -390,26 +364,12 @@ async def submit_workflow(
390
364
  fail_job(db=db_sync, job=job, log_msg=log_msg, logger_name=logger_name)
391
365
 
392
366
  except JobExecutionError as e:
393
-
394
367
  logger.debug(f'FAILED workflow "{workflow.name}", JobExecutionError.')
395
368
  logger.info(f'Workflow "{workflow.name}" failed (JobExecutionError).')
396
-
397
- # Read dataset attributes produced by the last successful task, and
398
- # update the DB dataset accordingly
399
- dataset.history = assemble_history_failed_job(
400
- job,
401
- dataset,
402
- workflow,
369
+ mark_last_wftask_as_failed(
370
+ dataset_id=dataset_id,
403
371
  logger_name=logger_name,
404
372
  )
405
- latest_filters = assemble_filters_failed_job(job)
406
- if latest_filters is not None:
407
- dataset.filters = latest_filters
408
- latest_images = assemble_images_failed_job(job)
409
- if latest_images is not None:
410
- dataset.images = latest_images
411
- db_sync.merge(dataset)
412
-
413
373
  fail_job(
414
374
  db=db_sync,
415
375
  job=job,
@@ -421,27 +381,13 @@ async def submit_workflow(
421
381
  )
422
382
 
423
383
  except Exception:
424
-
425
384
  logger.debug(f'FAILED workflow "{workflow.name}", unknown error.')
426
385
  logger.info(f'Workflow "{workflow.name}" failed (unkwnon error).')
427
-
428
- current_traceback = traceback.format_exc()
429
-
430
- # Read dataset attributes produced by the last successful task, and
431
- # update the DB dataset accordingly
432
- dataset.history = assemble_history_failed_job(
433
- job,
434
- dataset,
435
- workflow,
386
+ mark_last_wftask_as_failed(
387
+ dataset_id=dataset_id,
436
388
  logger_name=logger_name,
437
389
  )
438
- latest_filters = assemble_filters_failed_job(job)
439
- if latest_filters is not None:
440
- dataset.filters = latest_filters
441
- latest_images = assemble_images_failed_job(job)
442
- if latest_images is not None:
443
- dataset.images = latest_images
444
- db_sync.merge(dataset)
390
+ current_traceback = traceback.format_exc()
445
391
  fail_job(
446
392
  db=db_sync,
447
393
  job=job,
@@ -29,6 +29,7 @@ from ...set_start_and_last_task_index import set_start_and_last_task_index
29
29
  from ..runner import execute_tasks_v2
30
30
  from ._submit_setup import _local_submit_setup
31
31
  from .executor import FractalThreadPoolExecutor
32
+ from fractal_server.images.models import AttributeFiltersType
32
33
 
33
34
 
34
35
  def _process_workflow(
@@ -39,26 +40,24 @@ def _process_workflow(
39
40
  workflow_dir_local: Path,
40
41
  first_task_index: int,
41
42
  last_task_index: int,
42
- ) -> dict:
43
+ job_attribute_filters: AttributeFiltersType,
44
+ ) -> None:
43
45
  """
44
- Internal processing routine
45
-
46
- Schedules the workflow using a `FractalThreadPoolExecutor`.
46
+ Run the workflow using a `FractalThreadPoolExecutor`.
47
47
  """
48
-
49
48
  with FractalThreadPoolExecutor() as executor:
50
- new_dataset_attributes = execute_tasks_v2(
49
+ execute_tasks_v2(
51
50
  wf_task_list=workflow.task_list[
52
- first_task_index : (last_task_index + 1) # noqa
53
- ], # noqa
51
+ first_task_index : (last_task_index + 1)
52
+ ],
54
53
  dataset=dataset,
55
54
  executor=executor,
56
55
  workflow_dir_local=workflow_dir_local,
57
56
  workflow_dir_remote=workflow_dir_local,
58
57
  logger_name=logger_name,
59
58
  submit_setup_call=_local_submit_setup,
59
+ job_attribute_filters=job_attribute_filters,
60
60
  )
61
- return new_dataset_attributes
62
61
 
63
62
 
64
63
  async def process_workflow(
@@ -70,12 +69,13 @@ async def process_workflow(
70
69
  first_task_index: Optional[int] = None,
71
70
  last_task_index: Optional[int] = None,
72
71
  logger_name: str,
72
+ job_attribute_filters: AttributeFiltersType,
73
73
  # Slurm-specific
74
74
  user_cache_dir: Optional[str] = None,
75
75
  slurm_user: Optional[str] = None,
76
76
  slurm_account: Optional[str] = None,
77
77
  worker_init: Optional[str] = None,
78
- ) -> dict:
78
+ ) -> None:
79
79
  """
80
80
  Run a workflow
81
81
 
@@ -127,11 +127,6 @@ async def process_workflow(
127
127
  (positive exit codes).
128
128
  JobExecutionError: wrapper for errors raised by the tasks' executors
129
129
  (negative exit codes).
130
-
131
- Returns:
132
- output_dataset_metadata:
133
- The updated metadata for the dataset, as returned by the last task
134
- of the workflow
135
130
  """
136
131
 
137
132
  if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
@@ -148,12 +143,12 @@ async def process_workflow(
148
143
  last_task_index=last_task_index,
149
144
  )
150
145
 
151
- new_dataset_attributes = await async_wrap(_process_workflow)(
146
+ await async_wrap(_process_workflow)(
152
147
  workflow=workflow,
153
148
  dataset=dataset,
154
149
  logger_name=logger_name,
155
150
  workflow_dir_local=workflow_dir_local,
156
151
  first_task_index=first_task_index,
157
152
  last_task_index=last_task_index,
153
+ job_attribute_filters=job_attribute_filters,
158
154
  )
159
- return new_dataset_attributes