fractal-server 2.6.3__py3-none-any.whl → 2.7.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 (72) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +1 -1
  3. fractal_server/app/models/linkusergroup.py +11 -0
  4. fractal_server/app/models/v2/__init__.py +2 -0
  5. fractal_server/app/models/v2/collection_state.py +1 -0
  6. fractal_server/app/models/v2/task.py +67 -2
  7. fractal_server/app/routes/admin/v2/__init__.py +16 -0
  8. fractal_server/app/routes/admin/{v2.py → v2/job.py} +20 -191
  9. fractal_server/app/routes/admin/v2/project.py +43 -0
  10. fractal_server/app/routes/admin/v2/task.py +133 -0
  11. fractal_server/app/routes/admin/v2/task_group.py +162 -0
  12. fractal_server/app/routes/api/v1/task_collection.py +4 -4
  13. fractal_server/app/routes/api/v2/__init__.py +8 -0
  14. fractal_server/app/routes/api/v2/_aux_functions.py +1 -68
  15. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +343 -0
  16. fractal_server/app/routes/api/v2/submit.py +16 -35
  17. fractal_server/app/routes/api/v2/task.py +85 -110
  18. fractal_server/app/routes/api/v2/task_collection.py +184 -196
  19. fractal_server/app/routes/api/v2/task_collection_custom.py +70 -64
  20. fractal_server/app/routes/api/v2/task_group.py +173 -0
  21. fractal_server/app/routes/api/v2/workflow.py +39 -102
  22. fractal_server/app/routes/api/v2/workflow_import.py +360 -0
  23. fractal_server/app/routes/api/v2/workflowtask.py +4 -8
  24. fractal_server/app/routes/auth/_aux_auth.py +86 -40
  25. fractal_server/app/routes/auth/current_user.py +5 -5
  26. fractal_server/app/routes/auth/group.py +73 -23
  27. fractal_server/app/routes/auth/router.py +0 -2
  28. fractal_server/app/routes/auth/users.py +8 -7
  29. fractal_server/app/runner/executors/slurm/ssh/executor.py +82 -63
  30. fractal_server/app/runner/v2/__init__.py +13 -7
  31. fractal_server/app/runner/v2/task_interface.py +4 -9
  32. fractal_server/app/schemas/user.py +1 -2
  33. fractal_server/app/schemas/v2/__init__.py +7 -0
  34. fractal_server/app/schemas/v2/dataset.py +2 -7
  35. fractal_server/app/schemas/v2/dumps.py +1 -2
  36. fractal_server/app/schemas/v2/job.py +1 -1
  37. fractal_server/app/schemas/v2/manifest.py +25 -1
  38. fractal_server/app/schemas/v2/project.py +1 -1
  39. fractal_server/app/schemas/v2/task.py +95 -36
  40. fractal_server/app/schemas/v2/task_collection.py +8 -6
  41. fractal_server/app/schemas/v2/task_group.py +85 -0
  42. fractal_server/app/schemas/v2/workflow.py +7 -2
  43. fractal_server/app/schemas/v2/workflowtask.py +9 -6
  44. fractal_server/app/security/__init__.py +8 -1
  45. fractal_server/config.py +8 -28
  46. fractal_server/data_migrations/2_7_0.py +323 -0
  47. fractal_server/images/models.py +2 -4
  48. fractal_server/main.py +1 -1
  49. fractal_server/migrations/env.py +4 -1
  50. fractal_server/migrations/versions/034a469ec2eb_task_groups.py +184 -0
  51. fractal_server/ssh/_fabric.py +186 -73
  52. fractal_server/string_tools.py +6 -2
  53. fractal_server/tasks/utils.py +19 -5
  54. fractal_server/tasks/v1/_TaskCollectPip.py +1 -1
  55. fractal_server/tasks/v1/background_operations.py +5 -5
  56. fractal_server/tasks/v1/get_collection_data.py +2 -2
  57. fractal_server/tasks/v2/_venv_pip.py +67 -70
  58. fractal_server/tasks/v2/background_operations.py +180 -69
  59. fractal_server/tasks/v2/background_operations_ssh.py +57 -70
  60. fractal_server/tasks/v2/database_operations.py +44 -0
  61. fractal_server/tasks/v2/endpoint_operations.py +104 -116
  62. fractal_server/tasks/v2/templates/_1_create_venv.sh +9 -5
  63. fractal_server/tasks/v2/templates/{_2_upgrade_pip.sh → _2_preliminary_pip_operations.sh} +1 -0
  64. fractal_server/tasks/v2/utils.py +5 -0
  65. fractal_server/utils.py +3 -2
  66. {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/METADATA +3 -7
  67. {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/RECORD +70 -61
  68. fractal_server/app/routes/auth/group_names.py +0 -34
  69. fractal_server/tasks/v2/_TaskCollectPip.py +0 -132
  70. {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/LICENSE +0 -0
  71. {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/WHEEL +0 -0
  72. {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,173 @@
1
+ from fastapi import APIRouter
2
+ from fastapi import Depends
3
+ from fastapi import HTTPException
4
+ from fastapi import Response
5
+ from fastapi import status
6
+ from sqlmodel import or_
7
+ from sqlmodel import select
8
+
9
+ from ._aux_functions_tasks import _get_task_group_full_access
10
+ from ._aux_functions_tasks import _get_task_group_read_access
11
+ from ._aux_functions_tasks import _verify_non_duplication_group_constraint
12
+ from fractal_server.app.db import AsyncSession
13
+ from fractal_server.app.db import get_async_db
14
+ from fractal_server.app.models import LinkUserGroup
15
+ from fractal_server.app.models import UserOAuth
16
+ from fractal_server.app.models.v2 import CollectionStateV2
17
+ from fractal_server.app.models.v2 import TaskGroupV2
18
+ from fractal_server.app.models.v2 import WorkflowTaskV2
19
+ from fractal_server.app.routes.auth import current_active_user
20
+ from fractal_server.app.routes.auth._aux_auth import (
21
+ _verify_user_belongs_to_group,
22
+ )
23
+ from fractal_server.app.schemas.v2 import TaskGroupReadV2
24
+ from fractal_server.app.schemas.v2 import TaskGroupUpdateV2
25
+ from fractal_server.logger import set_logger
26
+
27
+ router = APIRouter()
28
+
29
+ logger = set_logger(__name__)
30
+
31
+
32
+ @router.get("/", response_model=list[TaskGroupReadV2])
33
+ async def get_task_group_list(
34
+ user: UserOAuth = Depends(current_active_user),
35
+ db: AsyncSession = Depends(get_async_db),
36
+ only_active: bool = False,
37
+ only_owner: bool = False,
38
+ args_schema: bool = True,
39
+ ) -> list[TaskGroupReadV2]:
40
+ """
41
+ Get all accessible TaskGroups
42
+ """
43
+
44
+ if only_owner:
45
+ condition = TaskGroupV2.user_id == user.id
46
+ else:
47
+ condition = or_(
48
+ TaskGroupV2.user_id == user.id,
49
+ TaskGroupV2.user_group_id.in_(
50
+ select(LinkUserGroup.group_id).where(
51
+ LinkUserGroup.user_id == user.id
52
+ )
53
+ ),
54
+ )
55
+ stm = select(TaskGroupV2).where(condition)
56
+ if only_active:
57
+ stm = stm.where(TaskGroupV2.active)
58
+
59
+ res = await db.execute(stm)
60
+ task_groups = res.scalars().all()
61
+
62
+ if args_schema is False:
63
+ for taskgroup in task_groups:
64
+ for task in taskgroup.task_list:
65
+ setattr(task, "args_schema_non_parallel", None)
66
+ setattr(task, "args_schema_parallel", None)
67
+
68
+ return task_groups
69
+
70
+
71
+ @router.get("/{task_group_id}/", response_model=TaskGroupReadV2)
72
+ async def get_task_group(
73
+ task_group_id: int,
74
+ user: UserOAuth = Depends(current_active_user),
75
+ db: AsyncSession = Depends(get_async_db),
76
+ ) -> TaskGroupReadV2:
77
+ """
78
+ Get single TaskGroup
79
+ """
80
+ task_group = await _get_task_group_read_access(
81
+ task_group_id=task_group_id,
82
+ user_id=user.id,
83
+ db=db,
84
+ )
85
+ return task_group
86
+
87
+
88
+ @router.delete("/{task_group_id}/", status_code=204)
89
+ async def delete_task_group(
90
+ task_group_id: int,
91
+ user: UserOAuth = Depends(current_active_user),
92
+ db: AsyncSession = Depends(get_async_db),
93
+ ):
94
+ """
95
+ Delete single TaskGroup
96
+ """
97
+
98
+ task_group = await _get_task_group_full_access(
99
+ task_group_id=task_group_id,
100
+ user_id=user.id,
101
+ db=db,
102
+ )
103
+
104
+ stm = select(WorkflowTaskV2).where(
105
+ WorkflowTaskV2.task_id.in_({task.id for task in task_group.task_list})
106
+ )
107
+ res = await db.execute(stm)
108
+ workflow_tasks = res.scalars().all()
109
+ if workflow_tasks != []:
110
+ raise HTTPException(
111
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
112
+ detail=f"TaskV2 {workflow_tasks[0].task_id} is still in use",
113
+ )
114
+
115
+ # Cascade operations: set foreign-keys to null for CollectionStateV2 which
116
+ # are in relationship with the current TaskGroupV2
117
+ logger.debug("Start of cascade operations on CollectionStateV2.")
118
+ stm = select(CollectionStateV2).where(
119
+ CollectionStateV2.taskgroupv2_id == task_group_id
120
+ )
121
+ res = await db.execute(stm)
122
+ collection_states = res.scalars().all()
123
+ for collection_state in collection_states:
124
+ logger.debug(
125
+ f"Setting CollectionStateV2[{collection_state.id}].taskgroupv2_id "
126
+ "to None."
127
+ )
128
+ collection_state.taskgroupv2_id = None
129
+ db.add(collection_state)
130
+ logger.debug("End of cascade operations on CollectionStateV2.")
131
+
132
+ await db.delete(task_group)
133
+ await db.commit()
134
+
135
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
136
+
137
+
138
+ @router.patch("/{task_group_id}/", response_model=TaskGroupReadV2)
139
+ async def patch_task_group(
140
+ task_group_id: int,
141
+ task_group_update: TaskGroupUpdateV2,
142
+ user: UserOAuth = Depends(current_active_user),
143
+ db: AsyncSession = Depends(get_async_db),
144
+ ) -> TaskGroupReadV2:
145
+ """
146
+ Patch single TaskGroup
147
+ """
148
+ task_group = await _get_task_group_full_access(
149
+ task_group_id=task_group_id,
150
+ user_id=user.id,
151
+ db=db,
152
+ )
153
+ if (
154
+ "user_group_id" in task_group_update.dict(exclude_unset=True)
155
+ and task_group_update.user_group_id != task_group.user_group_id
156
+ ):
157
+ await _verify_non_duplication_group_constraint(
158
+ db=db,
159
+ pkg_name=task_group.pkg_name,
160
+ version=task_group.version,
161
+ user_group_id=task_group_update.user_group_id,
162
+ )
163
+ for key, value in task_group_update.dict(exclude_unset=True).items():
164
+ if (key == "user_group_id") and (value is not None):
165
+ await _verify_user_belongs_to_group(
166
+ user_id=user.id, user_group_id=value, db=db
167
+ )
168
+ setattr(task_group, key, value)
169
+
170
+ db.add(task_group)
171
+ await db.commit()
172
+ await db.refresh(task_group)
173
+ return task_group
@@ -7,29 +7,25 @@ from fastapi import Response
7
7
  from fastapi import status
8
8
  from sqlmodel import select
9
9
 
10
- from .....logger import reset_logger_handlers
11
- from .....logger import set_logger
12
10
  from ....db import AsyncSession
13
11
  from ....db import get_async_db
14
12
  from ....models.v2 import JobV2
15
13
  from ....models.v2 import ProjectV2
16
- from ....models.v2 import TaskV2
17
14
  from ....models.v2 import WorkflowV2
18
15
  from ....schemas.v2 import WorkflowCreateV2
19
16
  from ....schemas.v2 import WorkflowExportV2
20
- from ....schemas.v2 import WorkflowImportV2
21
17
  from ....schemas.v2 import WorkflowReadV2
22
- from ....schemas.v2 import WorkflowTaskCreateV2
18
+ from ....schemas.v2 import WorkflowReadV2WithWarnings
23
19
  from ....schemas.v2 import WorkflowUpdateV2
24
20
  from ._aux_functions import _check_workflow_exists
25
21
  from ._aux_functions import _get_project_check_owner
26
22
  from ._aux_functions import _get_submitted_jobs_statement
27
23
  from ._aux_functions import _get_workflow_check_owner
28
- from ._aux_functions import _workflow_insert_task
24
+ from ._aux_functions_tasks import _add_warnings_to_workflow_tasks
29
25
  from fractal_server.app.models import UserOAuth
26
+ from fractal_server.app.models.v2.task import TaskGroupV2
30
27
  from fractal_server.app.routes.auth import current_active_user
31
28
 
32
-
33
29
  router = APIRouter()
34
30
 
35
31
 
@@ -89,14 +85,14 @@ async def create_workflow(
89
85
 
90
86
  @router.get(
91
87
  "/project/{project_id}/workflow/{workflow_id}/",
92
- response_model=WorkflowReadV2,
88
+ response_model=WorkflowReadV2WithWarnings,
93
89
  )
94
90
  async def read_workflow(
95
91
  project_id: int,
96
92
  workflow_id: int,
97
93
  user: UserOAuth = Depends(current_active_user),
98
94
  db: AsyncSession = Depends(get_async_db),
99
- ) -> Optional[WorkflowReadV2]:
95
+ ) -> Optional[WorkflowReadV2WithWarnings]:
100
96
  """
101
97
  Get info on an existing workflow
102
98
  """
@@ -108,12 +104,21 @@ async def read_workflow(
108
104
  db=db,
109
105
  )
110
106
 
111
- return workflow
107
+ wftask_list_with_warnings = await _add_warnings_to_workflow_tasks(
108
+ wftask_list=workflow.task_list, user_id=user.id, db=db
109
+ )
110
+ workflow_data = dict(
111
+ **workflow.model_dump(),
112
+ project=workflow.project,
113
+ task_list=wftask_list_with_warnings,
114
+ )
115
+
116
+ return workflow_data
112
117
 
113
118
 
114
119
  @router.patch(
115
120
  "/project/{project_id}/workflow/{workflow_id}/",
116
- response_model=WorkflowReadV2,
121
+ response_model=WorkflowReadV2WithWarnings,
117
122
  )
118
123
  async def update_workflow(
119
124
  project_id: int,
@@ -121,7 +126,7 @@ async def update_workflow(
121
126
  patch: WorkflowUpdateV2,
122
127
  user: UserOAuth = Depends(current_active_user),
123
128
  db: AsyncSession = Depends(get_async_db),
124
- ) -> Optional[WorkflowReadV2]:
129
+ ) -> Optional[WorkflowReadV2WithWarnings]:
125
130
  """
126
131
  Edit a workflow
127
132
  """
@@ -163,7 +168,16 @@ async def update_workflow(
163
168
  await db.refresh(workflow)
164
169
  await db.close()
165
170
 
166
- return workflow
171
+ wftask_list_with_warnings = await _add_warnings_to_workflow_tasks(
172
+ wftask_list=workflow.task_list, user_id=user.id, db=db
173
+ )
174
+ workflow_data = dict(
175
+ **workflow.model_dump(),
176
+ project=workflow.project,
177
+ task_list=wftask_list_with_warnings,
178
+ )
179
+
180
+ return workflow_data
167
181
 
168
182
 
169
183
  @router.delete(
@@ -238,98 +252,21 @@ async def export_worfklow(
238
252
  user_id=user.id,
239
253
  db=db,
240
254
  )
241
- # Emit a warning when exporting a workflow with custom tasks
242
- logger = set_logger(None)
255
+ wf_task_list = []
243
256
  for wftask in workflow.task_list:
244
- if wftask.task.owner is not None:
245
- logger.warning(
246
- f"Custom tasks (like the one with id={wftask.task_id} and "
247
- f'source="{wftask.task.source}") are not meant to be '
248
- "portable; re-importing this workflow may not work as "
249
- "expected."
250
- )
251
- reset_logger_handlers(logger)
252
-
253
- await db.close()
254
- return workflow
255
-
256
-
257
- @router.post(
258
- "/project/{project_id}/workflow/import/",
259
- response_model=WorkflowReadV2,
260
- status_code=status.HTTP_201_CREATED,
261
- )
262
- async def import_workflow(
263
- project_id: int,
264
- workflow: WorkflowImportV2,
265
- user: UserOAuth = Depends(current_active_user),
266
- db: AsyncSession = Depends(get_async_db),
267
- ) -> Optional[WorkflowReadV2]:
268
- """
269
- Import an existing workflow into a project
270
-
271
- Also create all required objects (i.e. Workflow and WorkflowTask's) along
272
- the way.
273
- """
274
-
275
- # Preliminary checks
276
- await _get_project_check_owner(
277
- project_id=project_id,
278
- user_id=user.id,
279
- db=db,
280
- )
281
-
282
- await _check_workflow_exists(
283
- name=workflow.name, project_id=project_id, db=db
284
- )
285
-
286
- # Check that all required tasks are available
287
- source_to_id = {}
288
-
289
- for wf_task in workflow.task_list:
290
-
291
- source = wf_task.task.source
292
- if source not in source_to_id.keys():
293
- stm = select(TaskV2).where(TaskV2.source == source)
294
- tasks_by_source = (await db.execute(stm)).scalars().all()
295
- if len(tasks_by_source) != 1:
296
- raise HTTPException(
297
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
298
- detail=(
299
- f"Found {len(tasks_by_source)} tasks "
300
- f"with {source=}."
301
- ),
302
- )
303
- source_to_id[source] = tasks_by_source[0].id
304
-
305
- # Create new Workflow (with empty task_list)
306
- db_workflow = WorkflowV2(
307
- project_id=project_id,
308
- **workflow.dict(exclude_none=True, exclude={"task_list"}),
309
- )
310
- db.add(db_workflow)
311
- await db.commit()
312
- await db.refresh(db_workflow)
313
-
314
- # Insert tasks
315
-
316
- for wf_task in workflow.task_list:
317
- source = wf_task.task.source
318
- task_id = source_to_id[source]
319
-
320
- new_wf_task = WorkflowTaskCreateV2(
321
- **wf_task.dict(exclude_none=True, exclude={"task"})
322
- )
323
- # Insert task
324
- await _workflow_insert_task(
325
- **new_wf_task.dict(),
326
- workflow_id=db_workflow.id,
327
- task_id=task_id,
328
- db=db,
257
+ task_group = await db.get(TaskGroupV2, wftask.task.taskgroupv2_id)
258
+ wf_task_list.append(wftask.dict())
259
+ wf_task_list[-1]["task"] = dict(
260
+ pkg_name=task_group.pkg_name,
261
+ version=task_group.version,
262
+ name=wftask.task.name,
329
263
  )
330
264
 
331
- await db.close()
332
- return db_workflow
265
+ wf = WorkflowExportV2(
266
+ **workflow.model_dump(),
267
+ task_list=wf_task_list,
268
+ )
269
+ return wf
333
270
 
334
271
 
335
272
  @router.get("/workflow/", response_model=list[WorkflowReadV2])