fractal-server 2.14.0a9__py3-none-any.whl → 2.14.0a11__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 (43) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/v2/dataset.py +0 -10
  3. fractal_server/app/models/v2/job.py +3 -0
  4. fractal_server/app/routes/api/v2/__init__.py +2 -0
  5. fractal_server/app/routes/api/v2/history.py +14 -9
  6. fractal_server/app/routes/api/v2/images.py +5 -2
  7. fractal_server/app/routes/api/v2/submit.py +16 -14
  8. fractal_server/app/routes/api/v2/verify_image_types.py +64 -0
  9. fractal_server/app/routes/api/v2/workflow.py +11 -7
  10. fractal_server/app/runner/components.py +0 -3
  11. fractal_server/app/runner/exceptions.py +4 -0
  12. fractal_server/app/runner/executors/base_runner.py +16 -17
  13. fractal_server/app/runner/executors/local/{_local_config.py → get_local_config.py} +0 -7
  14. fractal_server/app/runner/executors/local/runner.py +117 -58
  15. fractal_server/app/runner/executors/{slurm_sudo → slurm_common}/_check_jobs_status.py +4 -0
  16. fractal_server/app/runner/executors/slurm_ssh/_check_job_status_ssh.py +67 -0
  17. fractal_server/app/runner/executors/slurm_ssh/executor.py +7 -5
  18. fractal_server/app/runner/executors/slurm_ssh/runner.py +707 -0
  19. fractal_server/app/runner/executors/slurm_sudo/runner.py +265 -114
  20. fractal_server/app/runner/task_files.py +8 -0
  21. fractal_server/app/runner/v2/__init__.py +0 -365
  22. fractal_server/app/runner/v2/_local.py +4 -2
  23. fractal_server/app/runner/v2/_slurm_ssh.py +4 -2
  24. fractal_server/app/runner/v2/_slurm_sudo.py +4 -2
  25. fractal_server/app/runner/v2/db_tools.py +87 -0
  26. fractal_server/app/runner/v2/runner.py +83 -89
  27. fractal_server/app/runner/v2/runner_functions.py +279 -436
  28. fractal_server/app/runner/v2/runner_functions_low_level.py +37 -39
  29. fractal_server/app/runner/v2/submit_workflow.py +366 -0
  30. fractal_server/app/runner/v2/task_interface.py +31 -0
  31. fractal_server/app/schemas/v2/dataset.py +4 -71
  32. fractal_server/app/schemas/v2/dumps.py +6 -5
  33. fractal_server/app/schemas/v2/job.py +6 -3
  34. fractal_server/migrations/versions/47351f8c7ebc_drop_dataset_filters.py +50 -0
  35. fractal_server/migrations/versions/e81103413827_add_job_type_filters.py +36 -0
  36. {fractal_server-2.14.0a9.dist-info → fractal_server-2.14.0a11.dist-info}/METADATA +1 -1
  37. {fractal_server-2.14.0a9.dist-info → fractal_server-2.14.0a11.dist-info}/RECORD +40 -36
  38. fractal_server/app/runner/executors/local/_submit_setup.py +0 -46
  39. fractal_server/app/runner/executors/slurm_common/_submit_setup.py +0 -84
  40. fractal_server/app/runner/v2/_db_tools.py +0 -48
  41. {fractal_server-2.14.0a9.dist-info → fractal_server-2.14.0a11.dist-info}/LICENSE +0 -0
  42. {fractal_server-2.14.0a9.dist-info → fractal_server-2.14.0a11.dist-info}/WHEEL +0 -0
  43. {fractal_server-2.14.0a9.dist-info → fractal_server-2.14.0a11.dist-info}/entry_points.txt +0 -0
@@ -2,16 +2,11 @@ import json
2
2
  import logging
3
3
  import shutil
4
4
  import subprocess # nosec
5
- from pathlib import Path
6
- from shlex import split as shlex_split
5
+ from shlex import split
7
6
  from typing import Any
8
- from typing import Optional
9
7
 
10
- from ..components import _COMPONENT_KEY_
11
- from ..exceptions import JobExecutionError
12
- from ..exceptions import TaskExecutionError
13
- from fractal_server.app.models.v2 import WorkflowTaskV2
14
- from fractal_server.app.runner.task_files import TaskFiles
8
+ from fractal_server.app.runner.exceptions import JobExecutionError
9
+ from fractal_server.app.runner.exceptions import TaskExecutionError
15
10
  from fractal_server.string_tools import validate_cmd
16
11
 
17
12
 
@@ -32,9 +27,9 @@ def _call_command_wrapper(cmd: str, log_path: str) -> None:
32
27
  raise TaskExecutionError(f"Invalid command. Original error: {str(e)}")
33
28
 
34
29
  # Verify that task command is executable
35
- if shutil.which(shlex_split(cmd)[0]) is None:
30
+ if shutil.which(split(cmd)[0]) is None:
36
31
  msg = (
37
- f'Command "{shlex_split(cmd)[0]}" is not valid. '
32
+ f'Command "{split(cmd)[0]}" is not valid. '
38
33
  "Hint: make sure that it is executable."
39
34
  )
40
35
  raise TaskExecutionError(msg)
@@ -42,7 +37,7 @@ def _call_command_wrapper(cmd: str, log_path: str) -> None:
42
37
  with open(log_path, "w") as fp_log:
43
38
  try:
44
39
  result = subprocess.run( # nosec
45
- shlex_split(cmd),
40
+ split(cmd),
46
41
  stderr=fp_log,
47
42
  stdout=fp_log,
48
43
  )
@@ -60,58 +55,61 @@ def _call_command_wrapper(cmd: str, log_path: str) -> None:
60
55
 
61
56
 
62
57
  def run_single_task(
63
- parameters: dict[str, Any],
58
+ # COMMON to all parallel tasks
64
59
  command: str,
65
- wftask: WorkflowTaskV2,
66
- root_dir_local: Path,
67
- root_dir_remote: Optional[Path] = None,
68
- logger_name: Optional[str] = None,
60
+ workflow_task_order: int,
61
+ workflow_task_id: int,
62
+ task_name: str,
63
+ # SPECIAL for each parallel task
64
+ parameters: dict[str, Any],
65
+ remote_files: dict[str, str],
69
66
  ) -> dict[str, Any]:
70
67
  """
71
68
  Runs within an executor (AKA on the SLURM cluster).
72
69
  """
73
70
 
74
- logger = logging.getLogger(logger_name)
75
- logger.debug(f"Now start running {command=}")
76
-
77
- if not root_dir_remote:
78
- root_dir_remote = root_dir_local
79
-
80
- task_name = wftask.task.name
71
+ try:
72
+ args_file_remote = remote_files["args_file_remote"]
73
+ metadiff_file_remote = remote_files["metadiff_file_remote"]
74
+ log_file_remote = remote_files["log_file_remote"]
75
+ except KeyError:
76
+ raise TaskExecutionError(
77
+ f"Invalid {remote_files=}",
78
+ workflow_task_order=workflow_task_order,
79
+ workflow_task_id=workflow_task_id,
80
+ task_name=task_name,
81
+ )
81
82
 
82
- component = parameters.pop(_COMPONENT_KEY_)
83
- task_files = TaskFiles(
84
- root_dir_local=root_dir_local,
85
- root_dir_remote=root_dir_remote,
86
- task_name=task_name,
87
- task_order=wftask.order,
88
- component=component,
89
- )
83
+ logger = logging.getLogger(None)
84
+ logger.debug(f"Now start running {command=}")
90
85
 
91
86
  # Write arguments to args.json file
92
- with open(task_files.args_file_remote, "w") as f:
87
+ # FIXME: this could be done backend-side, with an additional
88
+ # file transfer if needed (e.g. on SSH)
89
+ with open(args_file_remote, "w") as f:
93
90
  json.dump(parameters, f, indent=2)
94
91
 
95
92
  # Assemble full command
93
+ # FIXME: this could be assembled backend-side
96
94
  full_command = (
97
95
  f"{command} "
98
- f"--args-json {task_files.args_file_remote} "
99
- f"--out-json {task_files.metadiff_file_remote}"
96
+ f"--args-json {args_file_remote} "
97
+ f"--out-json {metadiff_file_remote}"
100
98
  )
101
99
 
102
100
  try:
103
101
  _call_command_wrapper(
104
102
  full_command,
105
- log_path=task_files.log_file_remote,
103
+ log_path=log_file_remote,
106
104
  )
107
105
  except TaskExecutionError as e:
108
- e.workflow_task_order = wftask.order
109
- e.workflow_task_id = wftask.id
110
- e.task_name = wftask.task.name
106
+ e.workflow_task_order = workflow_task_order
107
+ e.workflow_task_id = workflow_task_id
108
+ e.task_name = task_name
111
109
  raise e
112
110
 
113
111
  try:
114
- with open(task_files.metadiff_file_remote, "r") as f:
112
+ with open(metadiff_file_remote, "r") as f:
115
113
  out_meta = json.load(f)
116
114
  except FileNotFoundError as e:
117
115
  logger.debug(
@@ -0,0 +1,366 @@
1
+ """
2
+ Runner backend subsystem root V2
3
+
4
+ This module is the single entry point to the runner backend subsystem V2.
5
+ Other subsystems should only import this module and not its submodules or
6
+ the individual backends.
7
+ """
8
+ import os
9
+ import traceback
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from sqlalchemy.orm import Session as DBSyncSession
14
+
15
+ from ....config import get_settings
16
+ from ....logger import get_logger
17
+ from ....logger import reset_logger_handlers
18
+ from ....logger import set_logger
19
+ from ....ssh._fabric import FractalSSH
20
+ from ....syringe import Inject
21
+ from ....utils import get_timestamp
22
+ from ....zip_tools import _zip_folder_to_file_and_remove
23
+ from ...db import DB
24
+ from ...models.v2 import DatasetV2
25
+ from ...models.v2 import JobV2
26
+ from ...models.v2 import WorkflowV2
27
+ from ...schemas.v2 import JobStatusTypeV2
28
+ from ..exceptions import JobExecutionError
29
+ from ..exceptions import TaskExecutionError
30
+ from ..executors.slurm_sudo._subprocess_run_as_user import _mkdir_as_user
31
+ from ..filenames import WORKFLOW_LOG_FILENAME
32
+ from ._local import process_workflow as local_process_workflow
33
+ from ._slurm_ssh import process_workflow as slurm_ssh_process_workflow
34
+ from ._slurm_sudo import process_workflow as slurm_sudo_process_workflow
35
+ from fractal_server import __VERSION__
36
+ from fractal_server.app.models import UserSettings
37
+
38
+
39
+ _backends = {}
40
+ _backends["local"] = local_process_workflow
41
+ _backends["slurm"] = slurm_sudo_process_workflow
42
+ _backends["slurm_ssh"] = slurm_ssh_process_workflow
43
+
44
+
45
+ def fail_job(
46
+ *,
47
+ db: DBSyncSession,
48
+ job: JobV2,
49
+ log_msg: str,
50
+ logger_name: str,
51
+ emit_log: bool = False,
52
+ ) -> None:
53
+ logger = get_logger(logger_name=logger_name)
54
+ if emit_log:
55
+ logger.error(log_msg)
56
+ reset_logger_handlers(logger)
57
+ job.status = JobStatusTypeV2.FAILED
58
+ job.end_timestamp = get_timestamp()
59
+ job.log = log_msg
60
+ db.merge(job)
61
+ db.commit()
62
+ db.close()
63
+ return
64
+
65
+
66
+ def submit_workflow(
67
+ *,
68
+ workflow_id: int,
69
+ dataset_id: int,
70
+ job_id: int,
71
+ user_id: int,
72
+ user_settings: UserSettings,
73
+ worker_init: Optional[str] = None,
74
+ slurm_user: Optional[str] = None,
75
+ user_cache_dir: Optional[str] = None,
76
+ fractal_ssh: Optional[FractalSSH] = None,
77
+ ) -> None:
78
+ """
79
+ Prepares a workflow and applies it to a dataset
80
+
81
+ This function wraps the process_workflow one, which is different for each
82
+ backend (e.g. local or slurm backend).
83
+
84
+ Args:
85
+ workflow_id:
86
+ ID of the workflow being applied
87
+ dataset_id:
88
+ Dataset ID
89
+ job_id:
90
+ Id of the job record which stores the state for the current
91
+ workflow application.
92
+ user_id:
93
+ User ID.
94
+ worker_init:
95
+ Custom executor parameters that get parsed before the execution of
96
+ each task.
97
+ user_cache_dir:
98
+ Cache directory (namely a path where the user can write); for the
99
+ slurm backend, this is used as a base directory for
100
+ `job.working_dir_user`.
101
+ slurm_user:
102
+ The username to impersonate for the workflow execution, for the
103
+ slurm backend.
104
+ """
105
+ # Declare runner backend and set `process_workflow` function
106
+ settings = Inject(get_settings)
107
+ FRACTAL_RUNNER_BACKEND = settings.FRACTAL_RUNNER_BACKEND
108
+ logger_name = f"WF{workflow_id}_job{job_id}"
109
+ logger = set_logger(logger_name=logger_name)
110
+
111
+ with next(DB.get_sync_db()) as db_sync:
112
+ try:
113
+ job: Optional[JobV2] = db_sync.get(JobV2, job_id)
114
+ dataset: Optional[DatasetV2] = db_sync.get(DatasetV2, dataset_id)
115
+ workflow: Optional[WorkflowV2] = db_sync.get(
116
+ WorkflowV2, workflow_id
117
+ )
118
+ except Exception as e:
119
+ logger.error(
120
+ f"Error connecting to the database. Original error: {str(e)}"
121
+ )
122
+ reset_logger_handlers(logger)
123
+ return
124
+
125
+ if job is None:
126
+ logger.error(f"JobV2 {job_id} does not exist")
127
+ reset_logger_handlers(logger)
128
+ return
129
+ if dataset is None or workflow is None:
130
+ log_msg = ""
131
+ if not dataset:
132
+ log_msg += f"Cannot fetch dataset {dataset_id} from database\n"
133
+ if not workflow:
134
+ log_msg += (
135
+ f"Cannot fetch workflow {workflow_id} from database\n"
136
+ )
137
+ fail_job(
138
+ db=db_sync, job=job, log_msg=log_msg, logger_name=logger_name
139
+ )
140
+ return
141
+
142
+ # Declare runner backend and set `process_workflow` function
143
+ settings = Inject(get_settings)
144
+ FRACTAL_RUNNER_BACKEND = settings.FRACTAL_RUNNER_BACKEND
145
+ try:
146
+ process_workflow = _backends[settings.FRACTAL_RUNNER_BACKEND]
147
+ except KeyError as e:
148
+ fail_job(
149
+ db=db_sync,
150
+ job=job,
151
+ log_msg=(
152
+ f"Invalid {FRACTAL_RUNNER_BACKEND=}.\n"
153
+ f"Original KeyError: {str(e)}"
154
+ ),
155
+ logger_name=logger_name,
156
+ emit_log=True,
157
+ )
158
+ return
159
+
160
+ # Define and create server-side working folder
161
+ WORKFLOW_DIR_LOCAL = Path(job.working_dir)
162
+ if WORKFLOW_DIR_LOCAL.exists():
163
+ fail_job(
164
+ db=db_sync,
165
+ job=job,
166
+ log_msg=f"Workflow dir {WORKFLOW_DIR_LOCAL} already exists.",
167
+ logger_name=logger_name,
168
+ emit_log=True,
169
+ )
170
+ return
171
+
172
+ try:
173
+ # Create WORKFLOW_DIR_LOCAL
174
+ if FRACTAL_RUNNER_BACKEND == "slurm":
175
+ original_umask = os.umask(0)
176
+ WORKFLOW_DIR_LOCAL.mkdir(parents=True, mode=0o755)
177
+ os.umask(original_umask)
178
+ else:
179
+ WORKFLOW_DIR_LOCAL.mkdir(parents=True)
180
+
181
+ # Define and create WORKFLOW_DIR_REMOTE
182
+ if FRACTAL_RUNNER_BACKEND == "local":
183
+ WORKFLOW_DIR_REMOTE = WORKFLOW_DIR_LOCAL
184
+ elif FRACTAL_RUNNER_BACKEND == "slurm":
185
+ WORKFLOW_DIR_REMOTE = (
186
+ Path(user_cache_dir) / WORKFLOW_DIR_LOCAL.name
187
+ )
188
+ _mkdir_as_user(
189
+ folder=str(WORKFLOW_DIR_REMOTE), user=slurm_user
190
+ )
191
+ elif FRACTAL_RUNNER_BACKEND == "slurm_ssh":
192
+ # Folder creation is deferred to _process_workflow
193
+ WORKFLOW_DIR_REMOTE = (
194
+ Path(user_settings.ssh_jobs_dir) / WORKFLOW_DIR_LOCAL.name
195
+ )
196
+ else:
197
+ logger.error(
198
+ "Invalid FRACTAL_RUNNER_BACKEND="
199
+ f"{settings.FRACTAL_RUNNER_BACKEND}."
200
+ )
201
+
202
+ except Exception as e:
203
+ error_type = type(e).__name__
204
+ fail_job(
205
+ db=db_sync,
206
+ job=job,
207
+ log_msg=(
208
+ f"{error_type} error occurred while creating job folder "
209
+ f"and subfolders.\nOriginal error: {str(e)}"
210
+ ),
211
+ logger_name=logger_name,
212
+ emit_log=True,
213
+ )
214
+ return
215
+
216
+ # After Session.commit() is called, either explicitly or when using a
217
+ # context manager, all objects associated with the Session are expired.
218
+ # https://docs.sqlalchemy.org/en/14/orm/
219
+ # session_basics.html#opening-and-closing-a-session
220
+ # https://docs.sqlalchemy.org/en/14/orm/
221
+ # session_state_management.html#refreshing-expiring
222
+
223
+ # See issue #928:
224
+ # https://github.com/fractal-analytics-platform/
225
+ # fractal-server/issues/928
226
+
227
+ db_sync.refresh(dataset)
228
+ db_sync.refresh(workflow)
229
+ for wftask in workflow.task_list:
230
+ db_sync.refresh(wftask)
231
+
232
+ # Write logs
233
+ log_file_path = WORKFLOW_DIR_LOCAL / WORKFLOW_LOG_FILENAME
234
+ logger = set_logger(
235
+ logger_name=logger_name,
236
+ log_file_path=log_file_path,
237
+ )
238
+ logger.info(
239
+ f'Start execution of workflow "{workflow.name}"; '
240
+ f"more logs at {str(log_file_path)}"
241
+ )
242
+ logger.debug(f"fractal_server.__VERSION__: {__VERSION__}")
243
+ logger.debug(f"FRACTAL_RUNNER_BACKEND: {FRACTAL_RUNNER_BACKEND}")
244
+ if FRACTAL_RUNNER_BACKEND == "slurm":
245
+ logger.debug(f"slurm_user: {slurm_user}")
246
+ logger.debug(f"slurm_account: {job.slurm_account}")
247
+ logger.debug(f"worker_init: {worker_init}")
248
+ elif FRACTAL_RUNNER_BACKEND == "slurm_ssh":
249
+ logger.debug(f"ssh_user: {user_settings.ssh_username}")
250
+ logger.debug(f"base dir: {user_settings.ssh_tasks_dir}")
251
+ logger.debug(f"worker_init: {worker_init}")
252
+ logger.debug(f"job.id: {job.id}")
253
+ logger.debug(f"job.working_dir: {job.working_dir}")
254
+ logger.debug(f"job.working_dir_user: {job.working_dir_user}")
255
+ logger.debug(f"job.first_task_index: {job.first_task_index}")
256
+ logger.debug(f"job.last_task_index: {job.last_task_index}")
257
+ logger.debug(f'START workflow "{workflow.name}"')
258
+
259
+ try:
260
+ if FRACTAL_RUNNER_BACKEND == "local":
261
+ process_workflow = local_process_workflow
262
+ backend_specific_kwargs = {}
263
+ elif FRACTAL_RUNNER_BACKEND == "slurm":
264
+ process_workflow = slurm_sudo_process_workflow
265
+ backend_specific_kwargs = dict(
266
+ slurm_user=slurm_user,
267
+ slurm_account=job.slurm_account,
268
+ user_cache_dir=user_cache_dir,
269
+ )
270
+ elif FRACTAL_RUNNER_BACKEND == "slurm_ssh":
271
+ process_workflow = slurm_ssh_process_workflow
272
+ backend_specific_kwargs = dict(fractal_ssh=fractal_ssh)
273
+ else:
274
+ raise RuntimeError(
275
+ f"Invalid runner backend {FRACTAL_RUNNER_BACKEND=}"
276
+ )
277
+
278
+ # "The Session.close() method does not prevent the Session from being
279
+ # used again. The Session itself does not actually have a distinct
280
+ # “closed” state; it merely means the Session will release all database
281
+ # connections and ORM objects."
282
+ # (https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.close).
283
+ #
284
+ # We close the session before the (possibly long) process_workflow
285
+ # call, to make sure all DB connections are released. The reason why we
286
+ # are not using a context manager within the try block is that we also
287
+ # need access to db_sync in the except branches.
288
+ db_sync = next(DB.get_sync_db())
289
+ db_sync.close()
290
+
291
+ process_workflow(
292
+ workflow=workflow,
293
+ dataset=dataset,
294
+ user_id=user_id,
295
+ workflow_dir_local=WORKFLOW_DIR_LOCAL,
296
+ workflow_dir_remote=WORKFLOW_DIR_REMOTE,
297
+ logger_name=logger_name,
298
+ worker_init=worker_init,
299
+ first_task_index=job.first_task_index,
300
+ last_task_index=job.last_task_index,
301
+ job_attribute_filters=job.attribute_filters,
302
+ job_type_filters=job.type_filters,
303
+ **backend_specific_kwargs,
304
+ )
305
+
306
+ logger.info(
307
+ f'End execution of workflow "{workflow.name}"; '
308
+ f"more logs at {str(log_file_path)}"
309
+ )
310
+ logger.debug(f'END workflow "{workflow.name}"')
311
+
312
+ # Update job DB entry
313
+ job.status = JobStatusTypeV2.DONE
314
+ job.end_timestamp = get_timestamp()
315
+ with log_file_path.open("r") as f:
316
+ logs = f.read()
317
+ job.log = logs
318
+ db_sync.merge(job)
319
+ db_sync.commit()
320
+
321
+ except TaskExecutionError as e:
322
+ logger.debug(f'FAILED workflow "{workflow.name}", TaskExecutionError.')
323
+ logger.info(f'Workflow "{workflow.name}" failed (TaskExecutionError).')
324
+
325
+ exception_args_string = "\n".join(e.args)
326
+ log_msg = (
327
+ f"TASK ERROR: "
328
+ f"Task name: {e.task_name}, "
329
+ f"position in Workflow: {e.workflow_task_order}\n"
330
+ f"TRACEBACK:\n{exception_args_string}"
331
+ )
332
+ fail_job(db=db_sync, job=job, log_msg=log_msg, logger_name=logger_name)
333
+
334
+ except JobExecutionError as e:
335
+ logger.debug(f'FAILED workflow "{workflow.name}", JobExecutionError.')
336
+ logger.info(f'Workflow "{workflow.name}" failed (JobExecutionError).')
337
+
338
+ fail_job(
339
+ db=db_sync,
340
+ job=job,
341
+ log_msg=(
342
+ f"JOB ERROR in Fractal job {job.id}:\n"
343
+ f"TRACEBACK:\n{e.assemble_error()}"
344
+ ),
345
+ logger_name=logger_name,
346
+ )
347
+
348
+ except Exception:
349
+ logger.debug(f'FAILED workflow "{workflow.name}", unknown error.')
350
+ logger.info(f'Workflow "{workflow.name}" failed (unkwnon error).')
351
+
352
+ current_traceback = traceback.format_exc()
353
+ fail_job(
354
+ db=db_sync,
355
+ job=job,
356
+ log_msg=(
357
+ f"UNKNOWN ERROR in Fractal job {job.id}\n"
358
+ f"TRACEBACK:\n{current_traceback}"
359
+ ),
360
+ logger_name=logger_name,
361
+ )
362
+
363
+ finally:
364
+ reset_logger_handlers(logger)
365
+ db_sync.close()
366
+ _zip_folder_to_file_and_remove(folder=job.working_dir)
@@ -1,11 +1,14 @@
1
1
  from typing import Any
2
+ from typing import Optional
2
3
 
3
4
  from pydantic import BaseModel
4
5
  from pydantic import ConfigDict
5
6
  from pydantic import Field
6
7
  from pydantic import field_validator
8
+ from pydantic import ValidationError
7
9
 
8
10
  from ....images import SingleImageTaskOutput
11
+ from fractal_server.app.runner.exceptions import TaskOutputValidationError
9
12
  from fractal_server.urls import normalize_url
10
13
 
11
14
 
@@ -61,3 +64,31 @@ class InitTaskOutput(BaseModel):
61
64
  model_config = ConfigDict(extra="forbid")
62
65
 
63
66
  parallelization_list: list[InitArgsModel] = Field(default_factory=list)
67
+
68
+
69
+ def _cast_and_validate_TaskOutput(
70
+ task_output: dict[str, Any]
71
+ ) -> Optional[TaskOutput]:
72
+ try:
73
+ validated_task_output = TaskOutput(**task_output)
74
+ return validated_task_output
75
+ except ValidationError as e:
76
+ raise TaskOutputValidationError(
77
+ "Validation of task output failed.\n"
78
+ f"Original error: {str(e)}\n"
79
+ f"Original data: {task_output}."
80
+ )
81
+
82
+
83
+ def _cast_and_validate_InitTaskOutput(
84
+ init_task_output: dict[str, Any],
85
+ ) -> Optional[InitTaskOutput]:
86
+ try:
87
+ validated_init_task_output = InitTaskOutput(**init_task_output)
88
+ return validated_init_task_output
89
+ except ValidationError as e:
90
+ raise TaskOutputValidationError(
91
+ "Validation of init-task output failed.\n"
92
+ f"Original error: {str(e)}\n"
93
+ f"Original data: {init_task_output}."
94
+ )
@@ -1,5 +1,4 @@
1
1
  from datetime import datetime
2
- from typing import Any
3
2
  from typing import Optional
4
3
 
5
4
  from pydantic import BaseModel
@@ -10,8 +9,6 @@ from pydantic import field_validator
10
9
  from pydantic import model_validator
11
10
  from pydantic.types import AwareDatetime
12
11
 
13
- from .._filter_validators import validate_attribute_filters
14
- from .._filter_validators import validate_type_filters
15
12
  from .._validators import cant_set_none
16
13
  from .._validators import NonEmptyString
17
14
  from .._validators import root_validate_dict_keys
@@ -28,7 +25,6 @@ class DatasetCreateV2(BaseModel):
28
25
 
29
26
  zarr_dir: Optional[str] = None
30
27
 
31
- type_filters: dict[str, bool] = Field(default_factory=dict)
32
28
  attribute_filters: AttributeFiltersType = Field(default_factory=dict)
33
29
 
34
30
  # Validators
@@ -36,12 +32,6 @@ class DatasetCreateV2(BaseModel):
36
32
  _dict_keys = model_validator(mode="before")(
37
33
  classmethod(root_validate_dict_keys)
38
34
  )
39
- _type_filters = field_validator("type_filters")(
40
- classmethod(validate_type_filters)
41
- )
42
- _attribute_filters = field_validator("attribute_filters")(
43
- classmethod(validate_attribute_filters)
44
- )
45
35
 
46
36
  @field_validator("zarr_dir")
47
37
  @classmethod
@@ -61,8 +51,6 @@ class DatasetReadV2(BaseModel):
61
51
  timestamp_created: AwareDatetime
62
52
 
63
53
  zarr_dir: str
64
- type_filters: dict[str, bool]
65
- attribute_filters: AttributeFiltersType
66
54
 
67
55
  @field_serializer("timestamp_created")
68
56
  def serialize_datetime(v: datetime) -> str:
@@ -74,20 +62,12 @@ class DatasetUpdateV2(BaseModel):
74
62
 
75
63
  name: Optional[NonEmptyString] = None
76
64
  zarr_dir: Optional[str] = None
77
- type_filters: Optional[dict[str, bool]] = None
78
- attribute_filters: Optional[dict[str, list[Any]]] = None
79
65
 
80
66
  # Validators
81
67
 
82
68
  _dict_keys = model_validator(mode="before")(
83
69
  classmethod(root_validate_dict_keys)
84
70
  )
85
- _type_filters = field_validator("type_filters")(
86
- classmethod(validate_type_filters)
87
- )
88
- _attribute_filters = field_validator("attribute_filters")(
89
- classmethod(validate_attribute_filters)
90
- )
91
71
 
92
72
  @field_validator("name")
93
73
  @classmethod
@@ -106,63 +86,20 @@ class DatasetImportV2(BaseModel):
106
86
  """
107
87
  Class for `Dataset` import.
108
88
 
89
+ We are dropping `model_config = ConfigDict(extra="forbid")` so that any
90
+ kind of legacy filters can be included in the payload, and ignored in the
91
+ API.
92
+
109
93
  Attributes:
110
94
  name:
111
95
  zarr_dir:
112
96
  images:
113
- filters:
114
- type_filters:
115
- attribute_filters:
116
97
  """
117
98
 
118
- model_config = ConfigDict(extra="forbid")
119
-
120
99
  name: str
121
100
  zarr_dir: str
122
101
  images: list[SingleImage] = Field(default_factory=list)
123
102
 
124
- filters: Optional[dict[str, Any]] = None
125
- type_filters: dict[str, bool] = Field(default_factory=dict)
126
- attribute_filters: AttributeFiltersType = Field(default_factory=dict)
127
-
128
- @model_validator(mode="before")
129
- @classmethod
130
- def update_legacy_filters(cls, values: dict):
131
- """
132
- Transform legacy filters (created with fractal-server<2.11.0)
133
- into attribute/type filters
134
- """
135
- if values.get("filters") is not None:
136
- if (
137
- "type_filters" in values.keys()
138
- or "attribute_filters" in values.keys()
139
- ):
140
- raise ValueError(
141
- "Cannot set filters both through the legacy field "
142
- "('filters') and the new ones ('type_filters' and/or "
143
- "'attribute_filters')."
144
- )
145
-
146
- else:
147
- # Convert legacy filters.types into new type_filters
148
- values["type_filters"] = values["filters"].get("types", {})
149
- values["attribute_filters"] = {
150
- key: [value]
151
- for key, value in values["filters"]
152
- .get("attributes", {})
153
- .items()
154
- }
155
- values["filters"] = None
156
-
157
- return values
158
-
159
- _type_filters = field_validator("type_filters")(
160
- classmethod(validate_type_filters)
161
- )
162
- _attribute_filters = field_validator("attribute_filters")(
163
- classmethod(validate_attribute_filters)
164
- )
165
-
166
103
  @field_validator("zarr_dir")
167
104
  @classmethod
168
105
  def normalize_zarr_dir(cls, v: str) -> str:
@@ -177,12 +114,8 @@ class DatasetExportV2(BaseModel):
177
114
  name:
178
115
  zarr_dir:
179
116
  images:
180
- type_filters:
181
- attribute_filters:
182
117
  """
183
118
 
184
119
  name: str
185
120
  zarr_dir: str
186
121
  images: list[SingleImage]
187
- type_filters: dict[str, bool]
188
- attribute_filters: AttributeFiltersType