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.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +1 -1
- fractal_server/app/models/linkusergroup.py +11 -0
- fractal_server/app/models/v2/__init__.py +2 -0
- fractal_server/app/models/v2/collection_state.py +1 -0
- fractal_server/app/models/v2/task.py +67 -2
- fractal_server/app/routes/admin/v2/__init__.py +16 -0
- fractal_server/app/routes/admin/{v2.py → v2/job.py} +20 -191
- fractal_server/app/routes/admin/v2/project.py +43 -0
- fractal_server/app/routes/admin/v2/task.py +133 -0
- fractal_server/app/routes/admin/v2/task_group.py +162 -0
- fractal_server/app/routes/api/v1/task_collection.py +4 -4
- fractal_server/app/routes/api/v2/__init__.py +8 -0
- fractal_server/app/routes/api/v2/_aux_functions.py +1 -68
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +343 -0
- fractal_server/app/routes/api/v2/submit.py +16 -35
- fractal_server/app/routes/api/v2/task.py +85 -110
- fractal_server/app/routes/api/v2/task_collection.py +184 -196
- fractal_server/app/routes/api/v2/task_collection_custom.py +70 -64
- fractal_server/app/routes/api/v2/task_group.py +173 -0
- fractal_server/app/routes/api/v2/workflow.py +39 -102
- fractal_server/app/routes/api/v2/workflow_import.py +360 -0
- fractal_server/app/routes/api/v2/workflowtask.py +4 -8
- fractal_server/app/routes/auth/_aux_auth.py +86 -40
- fractal_server/app/routes/auth/current_user.py +5 -5
- fractal_server/app/routes/auth/group.py +73 -23
- fractal_server/app/routes/auth/router.py +0 -2
- fractal_server/app/routes/auth/users.py +8 -7
- fractal_server/app/runner/executors/slurm/ssh/executor.py +82 -63
- fractal_server/app/runner/v2/__init__.py +13 -7
- fractal_server/app/runner/v2/task_interface.py +4 -9
- fractal_server/app/schemas/user.py +1 -2
- fractal_server/app/schemas/v2/__init__.py +7 -0
- fractal_server/app/schemas/v2/dataset.py +2 -7
- fractal_server/app/schemas/v2/dumps.py +1 -2
- fractal_server/app/schemas/v2/job.py +1 -1
- fractal_server/app/schemas/v2/manifest.py +25 -1
- fractal_server/app/schemas/v2/project.py +1 -1
- fractal_server/app/schemas/v2/task.py +95 -36
- fractal_server/app/schemas/v2/task_collection.py +8 -6
- fractal_server/app/schemas/v2/task_group.py +85 -0
- fractal_server/app/schemas/v2/workflow.py +7 -2
- fractal_server/app/schemas/v2/workflowtask.py +9 -6
- fractal_server/app/security/__init__.py +8 -1
- fractal_server/config.py +8 -28
- fractal_server/data_migrations/2_7_0.py +323 -0
- fractal_server/images/models.py +2 -4
- fractal_server/main.py +1 -1
- fractal_server/migrations/env.py +4 -1
- fractal_server/migrations/versions/034a469ec2eb_task_groups.py +184 -0
- fractal_server/ssh/_fabric.py +186 -73
- fractal_server/string_tools.py +6 -2
- fractal_server/tasks/utils.py +19 -5
- fractal_server/tasks/v1/_TaskCollectPip.py +1 -1
- fractal_server/tasks/v1/background_operations.py +5 -5
- fractal_server/tasks/v1/get_collection_data.py +2 -2
- fractal_server/tasks/v2/_venv_pip.py +67 -70
- fractal_server/tasks/v2/background_operations.py +180 -69
- fractal_server/tasks/v2/background_operations_ssh.py +57 -70
- fractal_server/tasks/v2/database_operations.py +44 -0
- fractal_server/tasks/v2/endpoint_operations.py +104 -116
- fractal_server/tasks/v2/templates/_1_create_venv.sh +9 -5
- fractal_server/tasks/v2/templates/{_2_upgrade_pip.sh → _2_preliminary_pip_operations.sh} +1 -0
- fractal_server/tasks/v2/utils.py +5 -0
- fractal_server/utils.py +3 -2
- {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/METADATA +3 -7
- {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/RECORD +70 -61
- fractal_server/app/routes/auth/group_names.py +0 -34
- fractal_server/tasks/v2/_TaskCollectPip.py +0 -132
- {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/WHEEL +0 -0
- {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
|
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 .
|
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=
|
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[
|
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
|
-
|
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=
|
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[
|
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
|
-
|
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
|
-
|
242
|
-
logger = set_logger(None)
|
255
|
+
wf_task_list = []
|
243
256
|
for wftask in workflow.task_list:
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
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
|
-
|
332
|
-
|
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])
|