fractal-server 1.4.6__py3-none-any.whl → 2.0.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 (139) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/db/__init__.py +0 -1
  3. fractal_server/app/models/__init__.py +6 -8
  4. fractal_server/app/models/linkuserproject.py +9 -0
  5. fractal_server/app/models/security.py +6 -0
  6. fractal_server/app/models/v1/__init__.py +12 -0
  7. fractal_server/app/models/{dataset.py → v1/dataset.py} +5 -5
  8. fractal_server/app/models/{job.py → v1/job.py} +5 -5
  9. fractal_server/app/models/{project.py → v1/project.py} +5 -5
  10. fractal_server/app/models/{state.py → v1/state.py} +2 -2
  11. fractal_server/app/models/{task.py → v1/task.py} +7 -2
  12. fractal_server/app/models/{workflow.py → v1/workflow.py} +5 -5
  13. fractal_server/app/models/v2/__init__.py +22 -0
  14. fractal_server/app/models/v2/collection_state.py +21 -0
  15. fractal_server/app/models/v2/dataset.py +54 -0
  16. fractal_server/app/models/v2/job.py +51 -0
  17. fractal_server/app/models/v2/project.py +30 -0
  18. fractal_server/app/models/v2/task.py +93 -0
  19. fractal_server/app/models/v2/workflow.py +35 -0
  20. fractal_server/app/models/v2/workflowtask.py +49 -0
  21. fractal_server/app/routes/admin/__init__.py +0 -0
  22. fractal_server/app/routes/{admin.py → admin/v1.py} +42 -42
  23. fractal_server/app/routes/admin/v2.py +309 -0
  24. fractal_server/app/routes/api/v1/__init__.py +7 -7
  25. fractal_server/app/routes/api/v1/_aux_functions.py +8 -8
  26. fractal_server/app/routes/api/v1/dataset.py +48 -41
  27. fractal_server/app/routes/api/v1/job.py +14 -14
  28. fractal_server/app/routes/api/v1/project.py +30 -27
  29. fractal_server/app/routes/api/v1/task.py +26 -16
  30. fractal_server/app/routes/api/v1/task_collection.py +28 -16
  31. fractal_server/app/routes/api/v1/workflow.py +28 -28
  32. fractal_server/app/routes/api/v1/workflowtask.py +11 -11
  33. fractal_server/app/routes/api/v2/__init__.py +34 -0
  34. fractal_server/app/routes/api/v2/_aux_functions.py +502 -0
  35. fractal_server/app/routes/api/v2/dataset.py +293 -0
  36. fractal_server/app/routes/api/v2/images.py +279 -0
  37. fractal_server/app/routes/api/v2/job.py +200 -0
  38. fractal_server/app/routes/api/v2/project.py +186 -0
  39. fractal_server/app/routes/api/v2/status.py +150 -0
  40. fractal_server/app/routes/api/v2/submit.py +210 -0
  41. fractal_server/app/routes/api/v2/task.py +222 -0
  42. fractal_server/app/routes/api/v2/task_collection.py +239 -0
  43. fractal_server/app/routes/api/v2/task_legacy.py +59 -0
  44. fractal_server/app/routes/api/v2/workflow.py +380 -0
  45. fractal_server/app/routes/api/v2/workflowtask.py +265 -0
  46. fractal_server/app/routes/aux/_job.py +2 -2
  47. fractal_server/app/runner/__init__.py +0 -379
  48. fractal_server/app/runner/async_wrap.py +27 -0
  49. fractal_server/app/runner/components.py +5 -0
  50. fractal_server/app/runner/exceptions.py +129 -0
  51. fractal_server/app/runner/executors/__init__.py +0 -0
  52. fractal_server/app/runner/executors/slurm/__init__.py +3 -0
  53. fractal_server/app/runner/{_slurm → executors/slurm}/_batching.py +1 -1
  54. fractal_server/app/runner/executors/slurm/_check_jobs_status.py +72 -0
  55. fractal_server/app/runner/{_slurm → executors/slurm}/_executor_wait_thread.py +3 -4
  56. fractal_server/app/runner/{_slurm → executors/slurm}/_slurm_config.py +3 -152
  57. fractal_server/app/runner/{_slurm → executors/slurm}/_subprocess_run_as_user.py +42 -1
  58. fractal_server/app/runner/{_slurm → executors/slurm}/executor.py +46 -27
  59. fractal_server/app/runner/filenames.py +6 -0
  60. fractal_server/app/runner/set_start_and_last_task_index.py +39 -0
  61. fractal_server/app/runner/task_files.py +103 -0
  62. fractal_server/app/runner/v1/__init__.py +366 -0
  63. fractal_server/app/runner/{_common.py → v1/_common.py} +56 -111
  64. fractal_server/app/runner/{_local → v1/_local}/__init__.py +5 -4
  65. fractal_server/app/runner/{_local → v1/_local}/_local_config.py +6 -7
  66. fractal_server/app/runner/{_local → v1/_local}/_submit_setup.py +1 -5
  67. fractal_server/app/runner/v1/_slurm/__init__.py +312 -0
  68. fractal_server/app/runner/{_slurm → v1/_slurm}/_submit_setup.py +5 -11
  69. fractal_server/app/runner/v1/_slurm/get_slurm_config.py +163 -0
  70. fractal_server/app/runner/v1/common.py +117 -0
  71. fractal_server/app/runner/{handle_failed_job.py → v1/handle_failed_job.py} +8 -8
  72. fractal_server/app/runner/v2/__init__.py +336 -0
  73. fractal_server/app/runner/v2/_local/__init__.py +162 -0
  74. fractal_server/app/runner/v2/_local/_local_config.py +118 -0
  75. fractal_server/app/runner/v2/_local/_submit_setup.py +52 -0
  76. fractal_server/app/runner/v2/_local/executor.py +100 -0
  77. fractal_server/app/runner/{_slurm → v2/_slurm}/__init__.py +38 -47
  78. fractal_server/app/runner/v2/_slurm/_submit_setup.py +82 -0
  79. fractal_server/app/runner/v2/_slurm/get_slurm_config.py +182 -0
  80. fractal_server/app/runner/v2/deduplicate_list.py +23 -0
  81. fractal_server/app/runner/v2/handle_failed_job.py +165 -0
  82. fractal_server/app/runner/v2/merge_outputs.py +38 -0
  83. fractal_server/app/runner/v2/runner.py +343 -0
  84. fractal_server/app/runner/v2/runner_functions.py +374 -0
  85. fractal_server/app/runner/v2/runner_functions_low_level.py +130 -0
  86. fractal_server/app/runner/v2/task_interface.py +62 -0
  87. fractal_server/app/runner/v2/v1_compat.py +31 -0
  88. fractal_server/app/schemas/__init__.py +1 -42
  89. fractal_server/app/schemas/_validators.py +28 -5
  90. fractal_server/app/schemas/v1/__init__.py +36 -0
  91. fractal_server/app/schemas/{applyworkflow.py → v1/applyworkflow.py} +18 -18
  92. fractal_server/app/schemas/{dataset.py → v1/dataset.py} +30 -30
  93. fractal_server/app/schemas/{dumps.py → v1/dumps.py} +8 -8
  94. fractal_server/app/schemas/{manifest.py → v1/manifest.py} +5 -5
  95. fractal_server/app/schemas/{project.py → v1/project.py} +9 -9
  96. fractal_server/app/schemas/{task.py → v1/task.py} +12 -12
  97. fractal_server/app/schemas/{task_collection.py → v1/task_collection.py} +7 -7
  98. fractal_server/app/schemas/{workflow.py → v1/workflow.py} +38 -38
  99. fractal_server/app/schemas/v2/__init__.py +37 -0
  100. fractal_server/app/schemas/v2/dataset.py +126 -0
  101. fractal_server/app/schemas/v2/dumps.py +87 -0
  102. fractal_server/app/schemas/v2/job.py +114 -0
  103. fractal_server/app/schemas/v2/manifest.py +159 -0
  104. fractal_server/app/schemas/v2/project.py +34 -0
  105. fractal_server/app/schemas/v2/status.py +16 -0
  106. fractal_server/app/schemas/v2/task.py +151 -0
  107. fractal_server/app/schemas/v2/task_collection.py +109 -0
  108. fractal_server/app/schemas/v2/workflow.py +79 -0
  109. fractal_server/app/schemas/v2/workflowtask.py +208 -0
  110. fractal_server/config.py +13 -10
  111. fractal_server/images/__init__.py +4 -0
  112. fractal_server/images/models.py +136 -0
  113. fractal_server/images/tools.py +84 -0
  114. fractal_server/main.py +11 -3
  115. fractal_server/migrations/env.py +0 -2
  116. fractal_server/migrations/versions/5bf02391cfef_v2.py +245 -0
  117. fractal_server/tasks/__init__.py +0 -5
  118. fractal_server/tasks/endpoint_operations.py +13 -19
  119. fractal_server/tasks/utils.py +35 -0
  120. fractal_server/tasks/{_TaskCollectPip.py → v1/_TaskCollectPip.py} +3 -3
  121. fractal_server/tasks/v1/__init__.py +0 -0
  122. fractal_server/tasks/{background_operations.py → v1/background_operations.py} +20 -52
  123. fractal_server/tasks/v1/get_collection_data.py +14 -0
  124. fractal_server/tasks/v2/_TaskCollectPip.py +103 -0
  125. fractal_server/tasks/v2/__init__.py +0 -0
  126. fractal_server/tasks/v2/background_operations.py +381 -0
  127. fractal_server/tasks/v2/get_collection_data.py +14 -0
  128. fractal_server/urls.py +13 -0
  129. {fractal_server-1.4.6.dist-info → fractal_server-2.0.0.dist-info}/METADATA +11 -12
  130. fractal_server-2.0.0.dist-info/RECORD +169 -0
  131. fractal_server/app/runner/_slurm/.gitignore +0 -2
  132. fractal_server/app/runner/common.py +0 -307
  133. fractal_server/app/schemas/json_schemas/manifest.json +0 -81
  134. fractal_server-1.4.6.dist-info/RECORD +0 -97
  135. /fractal_server/app/runner/{_slurm → executors/slurm}/remote.py +0 -0
  136. /fractal_server/app/runner/{_local → v1/_local}/executor.py +0 -0
  137. {fractal_server-1.4.6.dist-info → fractal_server-2.0.0.dist-info}/LICENSE +0 -0
  138. {fractal_server-1.4.6.dist-info → fractal_server-2.0.0.dist-info}/WHEEL +0 -0
  139. {fractal_server-1.4.6.dist-info → fractal_server-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,502 @@
1
+ """
2
+ Auxiliary functions to get object from the database or perform simple checks
3
+ """
4
+ from typing import Any
5
+ from typing import Literal
6
+ from typing import Optional
7
+ from typing import Union
8
+
9
+ from fastapi import HTTPException
10
+ from fastapi import status
11
+ from sqlalchemy.orm.attributes import flag_modified
12
+ from sqlmodel import select
13
+ from sqlmodel.sql.expression import SelectOfScalar
14
+
15
+ from ....db import AsyncSession
16
+ from ....models.v1 import Task
17
+ from ....models.v2 import DatasetV2
18
+ from ....models.v2 import JobV2
19
+ from ....models.v2 import LinkUserProjectV2
20
+ from ....models.v2 import ProjectV2
21
+ from ....models.v2 import TaskV2
22
+ from ....models.v2 import WorkflowTaskV2
23
+ from ....models.v2 import WorkflowV2
24
+ from ....schemas.v2 import JobStatusTypeV2
25
+ from ....security import User
26
+ from fractal_server.images import Filters
27
+
28
+
29
+ async def _get_project_check_owner(
30
+ *,
31
+ project_id: int,
32
+ user_id: int,
33
+ db: AsyncSession,
34
+ ) -> ProjectV2:
35
+ """
36
+ Check that user is a member of project and return the project.
37
+
38
+ Args:
39
+ project_id:
40
+ user_id:
41
+ db:
42
+
43
+ Returns:
44
+ The project object
45
+
46
+ Raises:
47
+ HTTPException(status_code=403_FORBIDDEN):
48
+ If the user is not a member of the project
49
+ HTTPException(status_code=404_NOT_FOUND):
50
+ If the project does not exist
51
+ """
52
+ project = await db.get(ProjectV2, project_id)
53
+
54
+ link_user_project = await db.get(LinkUserProjectV2, (project_id, user_id))
55
+ if not project:
56
+ raise HTTPException(
57
+ status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
58
+ )
59
+ if not link_user_project:
60
+ raise HTTPException(
61
+ status_code=status.HTTP_403_FORBIDDEN,
62
+ detail=f"Not allowed on project {project_id}",
63
+ )
64
+
65
+ return project
66
+
67
+
68
+ async def _get_workflow_check_owner(
69
+ *,
70
+ workflow_id: int,
71
+ project_id: int,
72
+ user_id: int,
73
+ db: AsyncSession,
74
+ ) -> WorkflowV2:
75
+ """
76
+ Get a workflow and a project, after access control on the project.
77
+
78
+ Args:
79
+ workflow_id:
80
+ project_id:
81
+ user_id:
82
+ db:
83
+
84
+ Returns:
85
+ The workflow object.
86
+
87
+ Raises:
88
+ HTTPException(status_code=404_NOT_FOUND):
89
+ If the workflow does not exist
90
+ HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
91
+ If the workflow is not associated to the project
92
+ """
93
+
94
+ # Access control for project
95
+ project = await _get_project_check_owner(
96
+ project_id=project_id, user_id=user_id, db=db
97
+ )
98
+ # Get workflow
99
+ workflow = await db.get(WorkflowV2, workflow_id)
100
+ if not workflow:
101
+ raise HTTPException(
102
+ status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found"
103
+ )
104
+ if workflow.project_id != project.id:
105
+ raise HTTPException(
106
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
107
+ detail=(f"Invalid {project_id=} for {workflow_id=}."),
108
+ )
109
+
110
+ # Refresh so that workflow.project relationship is loaded (see discussion
111
+ # in issue #1063)
112
+ await db.refresh(workflow)
113
+
114
+ return workflow
115
+
116
+
117
+ async def _get_workflow_task_check_owner(
118
+ *,
119
+ project_id: int,
120
+ workflow_id: int,
121
+ workflow_task_id: int,
122
+ user_id: int,
123
+ db: AsyncSession,
124
+ ) -> tuple[WorkflowTaskV2, WorkflowV2]:
125
+ """
126
+ Check that user has access to Workflow and WorkflowTask.
127
+
128
+ Args:
129
+ project_id:
130
+ workflow_id:
131
+ workflow_task_id:
132
+ user_id:
133
+ db:
134
+
135
+ Returns:
136
+ Tuple of WorkflowTask and Workflow objects.
137
+
138
+ Raises:
139
+ HTTPException(status_code=404_NOT_FOUND):
140
+ If the WorkflowTask does not exist
141
+ HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
142
+ If the WorkflowTask is not associated to the Workflow
143
+ """
144
+
145
+ # Access control for workflow
146
+ workflow = await _get_workflow_check_owner(
147
+ workflow_id=workflow_id,
148
+ project_id=project_id,
149
+ user_id=user_id,
150
+ db=db,
151
+ )
152
+
153
+ # If WorkflowTask is not in the db, exit
154
+ workflow_task = await db.get(WorkflowTaskV2, workflow_task_id)
155
+ if not workflow_task:
156
+ raise HTTPException(
157
+ status_code=status.HTTP_404_NOT_FOUND,
158
+ detail="WorkflowTask not found",
159
+ )
160
+
161
+ # If WorkflowTask is not part of the expected Workflow, exit
162
+ if workflow_id != workflow_task.workflow_id:
163
+ raise HTTPException(
164
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
165
+ detail=f"Invalid {workflow_id=} for {workflow_task_id=}",
166
+ )
167
+
168
+ return workflow_task, workflow
169
+
170
+
171
+ async def _check_workflow_exists(
172
+ *,
173
+ name: str,
174
+ project_id: int,
175
+ db: AsyncSession,
176
+ ) -> None:
177
+ """
178
+ Check that no other workflow of this project has the same name.
179
+
180
+ Args:
181
+ name: Workflow name
182
+ project_id: Project ID
183
+ db:
184
+
185
+ Raises:
186
+ HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
187
+ If such a workflow already exists
188
+ """
189
+ stm = (
190
+ select(WorkflowV2)
191
+ .where(WorkflowV2.name == name)
192
+ .where(WorkflowV2.project_id == project_id)
193
+ )
194
+ res = await db.execute(stm)
195
+ if res.scalars().all():
196
+ raise HTTPException(
197
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
198
+ detail=f"Workflow with {name=} and {project_id=} already exists.",
199
+ )
200
+
201
+
202
+ async def _check_project_exists(
203
+ *,
204
+ project_name: str,
205
+ user_id: int,
206
+ db: AsyncSession,
207
+ ) -> None:
208
+ """
209
+ Check that no other project with this name exists for this user.
210
+
211
+ Args:
212
+ project_name: Project name
213
+ user_id: User ID
214
+ db:
215
+
216
+ Raises:
217
+ HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
218
+ If such a project already exists
219
+ """
220
+ stm = (
221
+ select(ProjectV2)
222
+ .join(LinkUserProjectV2)
223
+ .where(ProjectV2.name == project_name)
224
+ .where(LinkUserProjectV2.user_id == user_id)
225
+ )
226
+ res = await db.execute(stm)
227
+ if res.scalars().all():
228
+ raise HTTPException(
229
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
230
+ detail=f"Project name ({project_name}) already in use",
231
+ )
232
+
233
+
234
+ async def _get_dataset_check_owner(
235
+ *,
236
+ project_id: int,
237
+ dataset_id: int,
238
+ user_id: int,
239
+ db: AsyncSession,
240
+ ) -> dict[Literal["dataset", "project"], Union[DatasetV2, ProjectV2]]:
241
+ """
242
+ Get a dataset and a project, after access control on the project
243
+
244
+ Args:
245
+ project_id:
246
+ dataset_id:
247
+ user_id:
248
+ db:
249
+
250
+ Returns:
251
+ Dictionary with the dataset and project objects (keys: `dataset`,
252
+ `project`).
253
+
254
+ Raises:
255
+ HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
256
+ If the dataset is not associated to the project
257
+ """
258
+
259
+ # Access control for project
260
+ project = await _get_project_check_owner(
261
+ project_id=project_id, user_id=user_id, db=db
262
+ )
263
+ # Get dataset
264
+ dataset = await db.get(DatasetV2, dataset_id)
265
+ if not dataset:
266
+ raise HTTPException(
267
+ status_code=status.HTTP_404_NOT_FOUND, detail="Dataset not found"
268
+ )
269
+ if dataset.project_id != project_id:
270
+ raise HTTPException(
271
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
272
+ detail=f"Invalid {project_id=} for {dataset_id=}",
273
+ )
274
+
275
+ # Refresh so that dataset.project relationship is loaded (see discussion
276
+ # in issue #1063)
277
+ await db.refresh(dataset)
278
+
279
+ return dict(dataset=dataset, project=project)
280
+
281
+
282
+ async def _get_job_check_owner(
283
+ *,
284
+ project_id: int,
285
+ job_id: int,
286
+ user_id: int,
287
+ db: AsyncSession,
288
+ ) -> dict[Literal["job", "project"], Union[JobV2, ProjectV2]]:
289
+ """
290
+ Get a job and a project, after access control on the project
291
+
292
+ Args:
293
+ project_id:
294
+ job_id:
295
+ user_id:
296
+ db:
297
+
298
+ Returns:
299
+ Dictionary with the job and project objects (keys: `job`,
300
+ `project`).
301
+
302
+ Raises:
303
+ HTTPException(status_code=422_UNPROCESSABLE_ENTITY):
304
+ If the job is not associated to the project
305
+ """
306
+ # Access control for project
307
+ project = await _get_project_check_owner(
308
+ project_id=project_id,
309
+ user_id=user_id,
310
+ db=db,
311
+ )
312
+ # Get dataset
313
+ job = await db.get(JobV2, job_id)
314
+ if not job:
315
+ raise HTTPException(
316
+ status_code=status.HTTP_404_NOT_FOUND, detail="Job not found"
317
+ )
318
+ if job.project_id != project_id:
319
+ raise HTTPException(
320
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
321
+ detail=f"Invalid {project_id=} for {job_id=}",
322
+ )
323
+ return dict(job=job, project=project)
324
+
325
+
326
+ async def _get_task_check_owner(
327
+ *,
328
+ task_id: int,
329
+ user: User,
330
+ db: AsyncSession,
331
+ ) -> TaskV2:
332
+ """
333
+ Get a task, after access control.
334
+
335
+ This check constitutes a preliminary version of access control:
336
+ if the current user is not a superuser and differs from the task owner
337
+ (including when `owner is None`), we raise an 403 HTTP Exception.
338
+
339
+ Args:
340
+ task_id:
341
+ user:
342
+ db:
343
+
344
+ Returns:
345
+ The task object.
346
+
347
+ Raises:
348
+ HTTPException(status_code=404_NOT_FOUND):
349
+ If the task does not exist
350
+ HTTPException(status_code=403_FORBIDDEN):
351
+ If the user does not have rights to edit this task.
352
+ """
353
+ task = await db.get(TaskV2, task_id)
354
+ if not task:
355
+ raise HTTPException(
356
+ status_code=status.HTTP_404_NOT_FOUND,
357
+ detail=f"TaskV2 {task_id} not found.",
358
+ )
359
+
360
+ if not user.is_superuser:
361
+ if task.owner is None:
362
+ raise HTTPException(
363
+ status_code=status.HTTP_403_FORBIDDEN,
364
+ detail=(
365
+ "Only a superuser can modify a TaskV2 with `owner=None`."
366
+ ),
367
+ )
368
+ else:
369
+ owner = user.username or user.slurm_user
370
+ if owner != task.owner:
371
+ raise HTTPException(
372
+ status_code=status.HTTP_403_FORBIDDEN,
373
+ detail=(
374
+ f"Current user ({owner}) cannot modify TaskV2 "
375
+ f"{task.id} with different owner ({task.owner})."
376
+ ),
377
+ )
378
+ return task
379
+
380
+
381
+ def _get_submitted_jobs_statement() -> SelectOfScalar:
382
+ """
383
+ Returns:
384
+ A sqlmodel statement that selects all `Job`s with
385
+ `Job.status` equal to `submitted`.
386
+ """
387
+ stm = select(JobV2).where(JobV2.status == JobStatusTypeV2.SUBMITTED)
388
+ return stm
389
+
390
+
391
+ async def _workflow_insert_task(
392
+ *,
393
+ workflow_id: int,
394
+ task_id: int,
395
+ is_legacy_task: bool = False,
396
+ order: Optional[int] = None,
397
+ meta_parallel: Optional[dict[str, Any]] = None,
398
+ meta_non_parallel: Optional[dict[str, Any]] = None,
399
+ args_non_parallel: Optional[dict[str, Any]] = None,
400
+ args_parallel: Optional[dict[str, Any]] = None,
401
+ input_filters: Optional[Filters] = None,
402
+ db: AsyncSession,
403
+ ) -> WorkflowTaskV2:
404
+ """
405
+ Insert a new WorkflowTask into Workflow.task_list
406
+
407
+ Args:
408
+ workflow_id:
409
+ task_id:
410
+ is_legacy_task:
411
+ order:
412
+ meta_parallel:
413
+ meta_non_parallel:
414
+ args_non_parallel:
415
+ args_parallel:
416
+ input_filters:
417
+ db:
418
+ """
419
+ db_workflow = await db.get(WorkflowV2, workflow_id)
420
+ if db_workflow is None:
421
+ raise ValueError(f"Workflow {workflow_id} does not exist")
422
+
423
+ if order is None:
424
+ order = len(db_workflow.task_list)
425
+
426
+ # Get task from db, and extract default arguments via a Task property
427
+ # method
428
+ if is_legacy_task is True:
429
+ db_task = await db.get(Task, task_id)
430
+ if db_task is None:
431
+ raise ValueError(f"Task {task_id} not found.")
432
+ task_type = "parallel"
433
+
434
+ final_args_parallel = db_task.default_args_from_args_schema.copy()
435
+ final_args_non_parallel = {}
436
+ final_meta_parallel = (db_task.meta or {}).copy()
437
+ final_meta_non_parallel = {}
438
+
439
+ else:
440
+ db_task = await db.get(TaskV2, task_id)
441
+ if db_task is None:
442
+ raise ValueError(f"TaskV2 {task_id} not found.")
443
+ task_type = db_task.type
444
+
445
+ final_args_non_parallel = (
446
+ db_task.default_args_non_parallel_from_args_schema.copy()
447
+ )
448
+ final_args_parallel = (
449
+ db_task.default_args_parallel_from_args_schema.copy()
450
+ )
451
+ final_meta_parallel = (db_task.meta_parallel or {}).copy()
452
+ final_meta_non_parallel = (db_task.meta_non_parallel or {}).copy()
453
+
454
+ # Combine arg_parallel
455
+ if args_parallel is not None:
456
+ for k, v in args_parallel.items():
457
+ final_args_parallel[k] = v
458
+ if final_args_parallel == {}:
459
+ final_args_parallel = None
460
+ # Combine arg_non_parallel
461
+ if args_non_parallel is not None:
462
+ for k, v in args_non_parallel.items():
463
+ final_args_non_parallel[k] = v
464
+ if final_args_non_parallel == {}:
465
+ final_args_non_parallel = None
466
+
467
+ # Combine meta_parallel (higher priority)
468
+ # and db_task.meta_parallel (lower priority)
469
+ final_meta_parallel.update(meta_parallel or {})
470
+ if final_meta_parallel == {}:
471
+ final_meta_parallel = None
472
+ # Combine meta_non_parallel (higher priority)
473
+ # and db_task.meta_non_parallel (lower priority)
474
+ final_meta_non_parallel.update(meta_non_parallel or {})
475
+ if final_meta_non_parallel == {}:
476
+ final_meta_non_parallel = None
477
+
478
+ # Prepare input_filters attribute
479
+ if input_filters is None:
480
+ input_filters_kwarg = {}
481
+ else:
482
+ input_filters_kwarg = dict(input_filters=input_filters)
483
+
484
+ # Create DB entry
485
+ wf_task = WorkflowTaskV2(
486
+ task_type=task_type,
487
+ is_legacy_task=is_legacy_task,
488
+ task_id=(task_id if not is_legacy_task else None),
489
+ task_legacy_id=(task_id if is_legacy_task else None),
490
+ args_non_parallel=final_args_non_parallel,
491
+ args_parallel=final_args_parallel,
492
+ meta_parallel=final_meta_parallel,
493
+ meta_non_parallel=final_meta_non_parallel,
494
+ **input_filters_kwarg,
495
+ )
496
+ db_workflow.task_list.insert(order, wf_task)
497
+ db_workflow.task_list.reorder() # type: ignore
498
+ flag_modified(db_workflow, "task_list")
499
+ await db.commit()
500
+ await db.refresh(wf_task)
501
+
502
+ return wf_task