fractal-server 2.18.0a3__py3-none-any.whl → 2.18.0a5__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 (80) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/v2/job.py +13 -2
  3. fractal_server/app/models/v2/resource.py +13 -0
  4. fractal_server/app/routes/admin/v2/__init__.py +10 -12
  5. fractal_server/app/routes/admin/v2/job.py +15 -15
  6. fractal_server/app/routes/admin/v2/task.py +7 -7
  7. fractal_server/app/routes/admin/v2/task_group.py +11 -11
  8. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +20 -20
  9. fractal_server/app/routes/api/v2/__init__.py +47 -49
  10. fractal_server/app/routes/api/v2/_aux_functions.py +22 -47
  11. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +4 -4
  12. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +2 -2
  13. fractal_server/app/routes/api/v2/dataset.py +63 -73
  14. fractal_server/app/routes/api/v2/history.py +7 -5
  15. fractal_server/app/routes/api/v2/job.py +12 -12
  16. fractal_server/app/routes/api/v2/project.py +11 -11
  17. fractal_server/app/routes/api/v2/status_legacy.py +15 -29
  18. fractal_server/app/routes/api/v2/submit.py +65 -66
  19. fractal_server/app/routes/api/v2/task.py +15 -17
  20. fractal_server/app/routes/api/v2/task_collection.py +18 -18
  21. fractal_server/app/routes/api/v2/task_collection_custom.py +11 -13
  22. fractal_server/app/routes/api/v2/task_collection_pixi.py +9 -9
  23. fractal_server/app/routes/api/v2/task_group.py +18 -18
  24. fractal_server/app/routes/api/v2/task_group_lifecycle.py +26 -26
  25. fractal_server/app/routes/api/v2/task_version_update.py +5 -5
  26. fractal_server/app/routes/api/v2/workflow.py +18 -18
  27. fractal_server/app/routes/api/v2/workflow_import.py +11 -11
  28. fractal_server/app/routes/api/v2/workflowtask.py +10 -10
  29. fractal_server/app/routes/auth/_aux_auth.py +99 -0
  30. fractal_server/app/routes/auth/users.py +9 -0
  31. fractal_server/app/schemas/user.py +1 -1
  32. fractal_server/app/schemas/v2/__init__.py +48 -48
  33. fractal_server/app/schemas/v2/dataset.py +25 -13
  34. fractal_server/app/schemas/v2/dumps.py +9 -9
  35. fractal_server/app/schemas/v2/job.py +11 -11
  36. fractal_server/app/schemas/v2/project.py +3 -3
  37. fractal_server/app/schemas/v2/resource.py +13 -4
  38. fractal_server/app/schemas/v2/status_legacy.py +3 -3
  39. fractal_server/app/schemas/v2/task.py +6 -6
  40. fractal_server/app/schemas/v2/task_collection.py +4 -4
  41. fractal_server/app/schemas/v2/task_group.py +16 -16
  42. fractal_server/app/schemas/v2/workflow.py +16 -16
  43. fractal_server/app/schemas/v2/workflowtask.py +14 -14
  44. fractal_server/app/shutdown.py +6 -6
  45. fractal_server/config/_main.py +1 -1
  46. fractal_server/data_migrations/{2_18_1.py → 2_18_0.py} +2 -1
  47. fractal_server/main.py +8 -12
  48. fractal_server/migrations/versions/88270f589c9b_add_prevent_new_submissions.py +39 -0
  49. fractal_server/migrations/versions/f0702066b007_one_submitted_job_per_dataset.py +40 -0
  50. fractal_server/runner/v2/_local.py +3 -2
  51. fractal_server/runner/v2/_slurm_ssh.py +3 -2
  52. fractal_server/runner/v2/_slurm_sudo.py +3 -2
  53. fractal_server/runner/v2/runner.py +36 -17
  54. fractal_server/runner/v2/runner_functions.py +11 -14
  55. fractal_server/runner/v2/submit_workflow.py +22 -9
  56. fractal_server/tasks/v2/local/_utils.py +2 -2
  57. fractal_server/tasks/v2/local/collect.py +5 -6
  58. fractal_server/tasks/v2/local/collect_pixi.py +5 -6
  59. fractal_server/tasks/v2/local/deactivate.py +7 -7
  60. fractal_server/tasks/v2/local/deactivate_pixi.py +3 -3
  61. fractal_server/tasks/v2/local/delete.py +5 -5
  62. fractal_server/tasks/v2/local/reactivate.py +5 -5
  63. fractal_server/tasks/v2/local/reactivate_pixi.py +5 -5
  64. fractal_server/tasks/v2/ssh/collect.py +5 -5
  65. fractal_server/tasks/v2/ssh/collect_pixi.py +5 -5
  66. fractal_server/tasks/v2/ssh/deactivate.py +7 -7
  67. fractal_server/tasks/v2/ssh/deactivate_pixi.py +2 -2
  68. fractal_server/tasks/v2/ssh/delete.py +5 -5
  69. fractal_server/tasks/v2/ssh/reactivate.py +5 -5
  70. fractal_server/tasks/v2/ssh/reactivate_pixi.py +5 -5
  71. fractal_server/tasks/v2/utils_background.py +7 -7
  72. fractal_server/tasks/v2/utils_database.py +5 -5
  73. fractal_server/types/__init__.py +13 -4
  74. fractal_server/types/validators/__init__.py +3 -1
  75. fractal_server/types/validators/_common_validators.py +23 -1
  76. {fractal_server-2.18.0a3.dist-info → fractal_server-2.18.0a5.dist-info}/METADATA +1 -1
  77. {fractal_server-2.18.0a3.dist-info → fractal_server-2.18.0a5.dist-info}/RECORD +80 -78
  78. {fractal_server-2.18.0a3.dist-info → fractal_server-2.18.0a5.dist-info}/WHEEL +0 -0
  79. {fractal_server-2.18.0a3.dist-info → fractal_server-2.18.0a5.dist-info}/entry_points.txt +0 -0
  80. {fractal_server-2.18.0a3.dist-info → fractal_server-2.18.0a5.dist-info}/licenses/LICENSE +0 -0
@@ -24,11 +24,11 @@ from fractal_server.app.routes.auth._aux_auth import (
24
24
  from fractal_server.app.routes.auth._aux_auth import (
25
25
  _verify_user_belongs_to_group,
26
26
  )
27
- from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
28
- from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
29
- from fractal_server.app.schemas.v2 import TaskGroupActivityV2Read
30
- from fractal_server.app.schemas.v2 import TaskGroupReadV2
31
- from fractal_server.app.schemas.v2 import TaskGroupUpdateV2
27
+ from fractal_server.app.schemas.v2 import TaskGroupActivityAction
28
+ from fractal_server.app.schemas.v2 import TaskGroupActivityRead
29
+ from fractal_server.app.schemas.v2 import TaskGroupActivityStatus
30
+ from fractal_server.app.schemas.v2 import TaskGroupRead
31
+ from fractal_server.app.schemas.v2 import TaskGroupUpdate
32
32
  from fractal_server.logger import set_logger
33
33
 
34
34
  from ._aux_functions import _get_user_resource_id
@@ -62,17 +62,17 @@ def _version_sort_key(
62
62
  return (1, task_group.version)
63
63
 
64
64
 
65
- @router.get("/activity/", response_model=list[TaskGroupActivityV2Read])
65
+ @router.get("/activity/", response_model=list[TaskGroupActivityRead])
66
66
  async def get_task_group_activity_list(
67
67
  task_group_activity_id: int | None = None,
68
68
  taskgroupv2_id: int | None = None,
69
69
  pkg_name: str | None = None,
70
- status: TaskGroupActivityStatusV2 | None = None,
71
- action: TaskGroupActivityActionV2 | None = None,
70
+ status: TaskGroupActivityStatus | None = None,
71
+ action: TaskGroupActivityAction | None = None,
72
72
  timestamp_started_min: AwareDatetime | None = None,
73
73
  user: UserOAuth = Depends(current_user_act_ver_prof),
74
74
  db: AsyncSession = Depends(get_async_db),
75
- ) -> list[TaskGroupActivityV2Read]:
75
+ ) -> list[TaskGroupActivityRead]:
76
76
  stm = select(TaskGroupActivityV2).where(
77
77
  TaskGroupActivityV2.user_id == user.id
78
78
  )
@@ -98,13 +98,13 @@ async def get_task_group_activity_list(
98
98
 
99
99
  @router.get(
100
100
  "/activity/{task_group_activity_id}/",
101
- response_model=TaskGroupActivityV2Read,
101
+ response_model=TaskGroupActivityRead,
102
102
  )
103
103
  async def get_task_group_activity(
104
104
  task_group_activity_id: int,
105
105
  user: UserOAuth = Depends(current_user_act_ver_prof),
106
106
  db: AsyncSession = Depends(get_async_db),
107
- ) -> TaskGroupActivityV2Read:
107
+ ) -> TaskGroupActivityRead:
108
108
  activity = await db.get(TaskGroupActivityV2, task_group_activity_id)
109
109
 
110
110
  if activity is None:
@@ -124,14 +124,14 @@ async def get_task_group_activity(
124
124
  return activity
125
125
 
126
126
 
127
- @router.get("/", response_model=list[tuple[str, list[TaskGroupReadV2]]])
127
+ @router.get("/", response_model=list[tuple[str, list[TaskGroupRead]]])
128
128
  async def get_task_group_list(
129
129
  user: UserOAuth = Depends(current_user_act_ver_prof),
130
130
  db: AsyncSession = Depends(get_async_db),
131
131
  only_active: bool = False,
132
132
  only_owner: bool = False,
133
133
  args_schema: bool = True,
134
- ) -> list[tuple[str, list[TaskGroupReadV2]]]:
134
+ ) -> list[tuple[str, list[TaskGroupRead]]]:
135
135
  """
136
136
  Get all accessible TaskGroups
137
137
  """
@@ -190,12 +190,12 @@ async def get_task_group_list(
190
190
  return grouped_result
191
191
 
192
192
 
193
- @router.get("/{task_group_id}/", response_model=TaskGroupReadV2)
193
+ @router.get("/{task_group_id}/", response_model=TaskGroupRead)
194
194
  async def get_task_group(
195
195
  task_group_id: int,
196
196
  user: UserOAuth = Depends(current_user_act_ver_prof),
197
197
  db: AsyncSession = Depends(get_async_db),
198
- ) -> TaskGroupReadV2:
198
+ ) -> TaskGroupRead:
199
199
  """
200
200
  Get single TaskGroup
201
201
  """
@@ -207,13 +207,13 @@ async def get_task_group(
207
207
  return task_group
208
208
 
209
209
 
210
- @router.patch("/{task_group_id}/", response_model=TaskGroupReadV2)
210
+ @router.patch("/{task_group_id}/", response_model=TaskGroupRead)
211
211
  async def patch_task_group(
212
212
  task_group_id: int,
213
- task_group_update: TaskGroupUpdateV2,
213
+ task_group_update: TaskGroupUpdate,
214
214
  user: UserOAuth = Depends(current_user_act_ver_prof),
215
215
  db: AsyncSession = Depends(get_async_db),
216
- ) -> TaskGroupReadV2:
216
+ ) -> TaskGroupRead:
217
217
  """
218
218
  Patch single TaskGroup
219
219
  """
@@ -14,11 +14,11 @@ from fractal_server.app.routes.aux.validate_user_profile import (
14
14
  validate_user_profile,
15
15
  )
16
16
  from fractal_server.app.schemas.v2 import ResourceType
17
- from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
18
- from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
19
- from fractal_server.app.schemas.v2 import TaskGroupActivityV2Read
20
- from fractal_server.app.schemas.v2 import TaskGroupReadV2
21
- from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
17
+ from fractal_server.app.schemas.v2 import TaskGroupActivityAction
18
+ from fractal_server.app.schemas.v2 import TaskGroupActivityRead
19
+ from fractal_server.app.schemas.v2 import TaskGroupActivityStatus
20
+ from fractal_server.app.schemas.v2 import TaskGroupOriginEnum
21
+ from fractal_server.app.schemas.v2 import TaskGroupRead
22
22
  from fractal_server.logger import set_logger
23
23
  from fractal_server.tasks.v2.local import deactivate_local
24
24
  from fractal_server.tasks.v2.local import deactivate_local_pixi
@@ -45,7 +45,7 @@ logger = set_logger(__name__)
45
45
 
46
46
  @router.post(
47
47
  "/{task_group_id}/deactivate/",
48
- response_model=TaskGroupActivityV2Read,
48
+ response_model=TaskGroupActivityRead,
49
49
  )
50
50
  async def deactivate_task_group(
51
51
  task_group_id: int,
@@ -53,7 +53,7 @@ async def deactivate_task_group(
53
53
  response: Response,
54
54
  user: UserOAuth = Depends(current_user_act_ver_prof),
55
55
  db: AsyncSession = Depends(get_async_db),
56
- ) -> TaskGroupActivityV2Read:
56
+ ) -> TaskGroupActivityRead:
57
57
  """
58
58
  Deactivate task-group venv
59
59
  """
@@ -84,13 +84,13 @@ async def deactivate_task_group(
84
84
  )
85
85
 
86
86
  # Shortcut for task-group with origin="other"
87
- if task_group.origin == TaskGroupV2OriginEnum.OTHER:
87
+ if task_group.origin == TaskGroupOriginEnum.OTHER:
88
88
  task_group.active = False
89
89
  task_group_activity = TaskGroupActivityV2(
90
90
  user_id=task_group.user_id,
91
91
  taskgroupv2_id=task_group.id,
92
- status=TaskGroupActivityStatusV2.OK,
93
- action=TaskGroupActivityActionV2.DEACTIVATE,
92
+ status=TaskGroupActivityStatus.OK,
93
+ action=TaskGroupActivityAction.DEACTIVATE,
94
94
  pkg_name=task_group.pkg_name,
95
95
  version=(task_group.version or "N/A"),
96
96
  log=(
@@ -109,8 +109,8 @@ async def deactivate_task_group(
109
109
  task_group_activity = TaskGroupActivityV2(
110
110
  user_id=task_group.user_id,
111
111
  taskgroupv2_id=task_group.id,
112
- status=TaskGroupActivityStatusV2.PENDING,
113
- action=TaskGroupActivityActionV2.DEACTIVATE,
112
+ status=TaskGroupActivityStatus.PENDING,
113
+ action=TaskGroupActivityAction.DEACTIVATE,
114
114
  pkg_name=task_group.pkg_name,
115
115
  version=task_group.version,
116
116
  timestamp_started=get_timestamp(),
@@ -122,12 +122,12 @@ async def deactivate_task_group(
122
122
 
123
123
  # Submit background task
124
124
  if resource.type == ResourceType.SLURM_SSH:
125
- if task_group.origin == TaskGroupV2OriginEnum.PIXI:
125
+ if task_group.origin == TaskGroupOriginEnum.PIXI:
126
126
  deactivate_function = deactivate_ssh_pixi
127
127
  else:
128
128
  deactivate_function = deactivate_ssh
129
129
  else:
130
- if task_group.origin == TaskGroupV2OriginEnum.PIXI:
130
+ if task_group.origin == TaskGroupOriginEnum.PIXI:
131
131
  deactivate_function = deactivate_local_pixi
132
132
  else:
133
133
  deactivate_function = deactivate_local
@@ -149,7 +149,7 @@ async def deactivate_task_group(
149
149
 
150
150
  @router.post(
151
151
  "/{task_group_id}/reactivate/",
152
- response_model=TaskGroupActivityV2Read,
152
+ response_model=TaskGroupActivityRead,
153
153
  )
154
154
  async def reactivate_task_group(
155
155
  task_group_id: int,
@@ -157,7 +157,7 @@ async def reactivate_task_group(
157
157
  response: Response,
158
158
  user: UserOAuth = Depends(current_user_act_ver_prof),
159
159
  db: AsyncSession = Depends(get_async_db),
160
- ) -> TaskGroupReadV2:
160
+ ) -> TaskGroupRead:
161
161
  """
162
162
  Deactivate task-group venv
163
163
  """
@@ -187,13 +187,13 @@ async def reactivate_task_group(
187
187
  await check_no_submitted_job(task_group_id=task_group.id, db=db)
188
188
 
189
189
  # Shortcut for task-group with origin="other"
190
- if task_group.origin == TaskGroupV2OriginEnum.OTHER:
190
+ if task_group.origin == TaskGroupOriginEnum.OTHER:
191
191
  task_group.active = True
192
192
  task_group_activity = TaskGroupActivityV2(
193
193
  user_id=task_group.user_id,
194
194
  taskgroupv2_id=task_group.id,
195
- status=TaskGroupActivityStatusV2.OK,
196
- action=TaskGroupActivityActionV2.REACTIVATE,
195
+ status=TaskGroupActivityStatus.OK,
196
+ action=TaskGroupActivityAction.REACTIVATE,
197
197
  pkg_name=task_group.pkg_name,
198
198
  version=(task_group.version or "N/A"),
199
199
  log=(
@@ -220,8 +220,8 @@ async def reactivate_task_group(
220
220
  task_group_activity = TaskGroupActivityV2(
221
221
  user_id=task_group.user_id,
222
222
  taskgroupv2_id=task_group.id,
223
- status=TaskGroupActivityStatusV2.PENDING,
224
- action=TaskGroupActivityActionV2.REACTIVATE,
223
+ status=TaskGroupActivityStatus.PENDING,
224
+ action=TaskGroupActivityAction.REACTIVATE,
225
225
  pkg_name=task_group.pkg_name,
226
226
  version=task_group.version,
227
227
  timestamp_started=get_timestamp(),
@@ -231,12 +231,12 @@ async def reactivate_task_group(
231
231
 
232
232
  # Submit background task
233
233
  if resource.type == ResourceType.SLURM_SSH:
234
- if task_group.origin == TaskGroupV2OriginEnum.PIXI:
234
+ if task_group.origin == TaskGroupOriginEnum.PIXI:
235
235
  reactivate_function = reactivate_ssh_pixi
236
236
  else:
237
237
  reactivate_function = reactivate_ssh
238
238
  else:
239
- if task_group.origin == TaskGroupV2OriginEnum.PIXI:
239
+ if task_group.origin == TaskGroupOriginEnum.PIXI:
240
240
  reactivate_function = reactivate_local_pixi
241
241
  else:
242
242
  reactivate_function = reactivate_local
@@ -265,7 +265,7 @@ async def delete_task_group(
265
265
  response: Response,
266
266
  user: UserOAuth = Depends(current_user_act_ver_prof),
267
267
  db: AsyncSession = Depends(get_async_db),
268
- ) -> TaskGroupActivityV2Read:
268
+ ) -> TaskGroupActivityRead:
269
269
  """
270
270
  Deletion of task-group from db and file system
271
271
  """
@@ -283,8 +283,8 @@ async def delete_task_group(
283
283
  task_group_activity = TaskGroupActivityV2(
284
284
  user_id=task_group.user_id,
285
285
  taskgroupv2_id=task_group.id,
286
- status=TaskGroupActivityStatusV2.PENDING,
287
- action=TaskGroupActivityActionV2.DELETE,
286
+ status=TaskGroupActivityStatus.PENDING,
287
+ action=TaskGroupActivityAction.DELETE,
288
288
  pkg_name=task_group.pkg_name,
289
289
  version=(task_group.version or "N/A"),
290
290
  timestamp_started=get_timestamp(),
@@ -19,8 +19,8 @@ from fractal_server.app.models.v2 import TaskGroupV2
19
19
  from fractal_server.app.models.v2 import TaskV2
20
20
  from fractal_server.app.routes.auth import current_user_act_ver_prof
21
21
  from fractal_server.app.schemas.v2 import TaskType
22
- from fractal_server.app.schemas.v2 import WorkflowTaskReadV2
23
- from fractal_server.app.schemas.v2 import WorkflowTaskReplaceV2
22
+ from fractal_server.app.schemas.v2 import WorkflowTaskRead
23
+ from fractal_server.app.schemas.v2 import WorkflowTaskReplace
24
24
  from fractal_server.app.schemas.v2.sharing import ProjectPermissions
25
25
 
26
26
  from ._aux_functions import _get_workflow_check_access
@@ -171,7 +171,7 @@ async def get_workflow_version_update_candidates(
171
171
 
172
172
  @router.post(
173
173
  "/project/{project_id}/workflow/{workflow_id}/wftask/replace-task/",
174
- response_model=WorkflowTaskReadV2,
174
+ response_model=WorkflowTaskRead,
175
175
  status_code=status.HTTP_201_CREATED,
176
176
  )
177
177
  async def replace_workflowtask(
@@ -179,10 +179,10 @@ async def replace_workflowtask(
179
179
  workflow_id: int,
180
180
  workflow_task_id: int,
181
181
  task_id: int,
182
- replace: WorkflowTaskReplaceV2,
182
+ replace: WorkflowTaskReplace,
183
183
  user: UserOAuth = Depends(current_user_act_ver_prof),
184
184
  db: AsyncSession = Depends(get_async_db),
185
- ) -> WorkflowTaskReadV2:
185
+ ) -> WorkflowTaskRead:
186
186
  # Get objects from database
187
187
  workflow_task, workflow = await _get_workflow_task_check_access(
188
188
  project_id=project_id,
@@ -15,11 +15,11 @@ from fractal_server.app.models.v2 import JobV2
15
15
  from fractal_server.app.models.v2 import TaskGroupV2
16
16
  from fractal_server.app.models.v2 import WorkflowV2
17
17
  from fractal_server.app.routes.auth import current_user_act_ver_prof
18
- from fractal_server.app.schemas.v2 import WorkflowCreateV2
19
- from fractal_server.app.schemas.v2 import WorkflowExportV2
20
- from fractal_server.app.schemas.v2 import WorkflowReadV2
21
- from fractal_server.app.schemas.v2 import WorkflowReadV2WithWarnings
22
- from fractal_server.app.schemas.v2 import WorkflowUpdateV2
18
+ from fractal_server.app.schemas.v2 import WorkflowCreate
19
+ from fractal_server.app.schemas.v2 import WorkflowExport
20
+ from fractal_server.app.schemas.v2 import WorkflowRead
21
+ from fractal_server.app.schemas.v2 import WorkflowReadWithWarnings
22
+ from fractal_server.app.schemas.v2 import WorkflowUpdate
23
23
  from fractal_server.app.schemas.v2.sharing import ProjectPermissions
24
24
  from fractal_server.images.tools import merge_type_filters
25
25
 
@@ -35,13 +35,13 @@ router = APIRouter()
35
35
 
36
36
  @router.get(
37
37
  "/project/{project_id}/workflow/",
38
- response_model=list[WorkflowReadV2],
38
+ response_model=list[WorkflowRead],
39
39
  )
40
40
  async def get_workflow_list(
41
41
  project_id: int,
42
42
  user: UserOAuth = Depends(current_user_act_ver_prof),
43
43
  db: AsyncSession = Depends(get_async_db),
44
- ) -> list[WorkflowReadV2] | None:
44
+ ) -> list[WorkflowRead] | None:
45
45
  """
46
46
  Get workflow list for given project
47
47
  """
@@ -63,15 +63,15 @@ async def get_workflow_list(
63
63
 
64
64
  @router.post(
65
65
  "/project/{project_id}/workflow/",
66
- response_model=WorkflowReadV2,
66
+ response_model=WorkflowRead,
67
67
  status_code=status.HTTP_201_CREATED,
68
68
  )
69
69
  async def create_workflow(
70
70
  project_id: int,
71
- workflow: WorkflowCreateV2,
71
+ workflow: WorkflowCreate,
72
72
  user: UserOAuth = Depends(current_user_act_ver_prof),
73
73
  db: AsyncSession = Depends(get_async_db),
74
- ) -> WorkflowReadV2 | None:
74
+ ) -> WorkflowRead | None:
75
75
  """
76
76
  Create a workflow, associate to a project
77
77
  """
@@ -95,14 +95,14 @@ async def create_workflow(
95
95
 
96
96
  @router.get(
97
97
  "/project/{project_id}/workflow/{workflow_id}/",
98
- response_model=WorkflowReadV2WithWarnings,
98
+ response_model=WorkflowReadWithWarnings,
99
99
  )
100
100
  async def read_workflow(
101
101
  project_id: int,
102
102
  workflow_id: int,
103
103
  user: UserOAuth = Depends(current_user_act_ver_prof),
104
104
  db: AsyncSession = Depends(get_async_db),
105
- ) -> WorkflowReadV2WithWarnings | None:
105
+ ) -> WorkflowReadWithWarnings | None:
106
106
  """
107
107
  Get info on an existing workflow
108
108
  """
@@ -129,15 +129,15 @@ async def read_workflow(
129
129
 
130
130
  @router.patch(
131
131
  "/project/{project_id}/workflow/{workflow_id}/",
132
- response_model=WorkflowReadV2WithWarnings,
132
+ response_model=WorkflowReadWithWarnings,
133
133
  )
134
134
  async def update_workflow(
135
135
  project_id: int,
136
136
  workflow_id: int,
137
- patch: WorkflowUpdateV2,
137
+ patch: WorkflowUpdate,
138
138
  user: UserOAuth = Depends(current_user_act_ver_prof),
139
139
  db: AsyncSession = Depends(get_async_db),
140
- ) -> WorkflowReadV2WithWarnings | None:
140
+ ) -> WorkflowReadWithWarnings | None:
141
141
  """
142
142
  Edit a workflow
143
143
  """
@@ -251,14 +251,14 @@ async def delete_workflow(
251
251
 
252
252
  @router.get(
253
253
  "/project/{project_id}/workflow/{workflow_id}/export/",
254
- response_model=WorkflowExportV2,
254
+ response_model=WorkflowExport,
255
255
  )
256
256
  async def export_workflow(
257
257
  project_id: int,
258
258
  workflow_id: int,
259
259
  user: UserOAuth = Depends(current_user_act_ver_prof),
260
260
  db: AsyncSession = Depends(get_async_db),
261
- ) -> WorkflowExportV2 | None:
261
+ ) -> WorkflowExport | None:
262
262
  """
263
263
  Export an existing workflow, after stripping all IDs
264
264
  """
@@ -279,7 +279,7 @@ async def export_workflow(
279
279
  name=wftask.task.name,
280
280
  )
281
281
 
282
- wf = WorkflowExportV2(
282
+ wf = WorkflowExport(
283
283
  **workflow.model_dump(),
284
284
  task_list=wf_task_list,
285
285
  )
@@ -19,11 +19,11 @@ from fractal_server.app.routes.auth import current_user_act_ver_prof
19
19
  from fractal_server.app.routes.auth._aux_auth import (
20
20
  _get_default_usergroup_id_or_none,
21
21
  )
22
- from fractal_server.app.schemas.v2 import TaskImportV2
23
- from fractal_server.app.schemas.v2 import TaskImportV2Legacy
24
- from fractal_server.app.schemas.v2 import WorkflowImportV2
25
- from fractal_server.app.schemas.v2 import WorkflowReadV2WithWarnings
26
- from fractal_server.app.schemas.v2 import WorkflowTaskCreateV2
22
+ from fractal_server.app.schemas.v2 import TaskImport
23
+ from fractal_server.app.schemas.v2 import TaskImportLegacy
24
+ from fractal_server.app.schemas.v2 import WorkflowImport
25
+ from fractal_server.app.schemas.v2 import WorkflowReadWithWarnings
26
+ from fractal_server.app.schemas.v2 import WorkflowTaskCreate
27
27
  from fractal_server.app.schemas.v2.sharing import ProjectPermissions
28
28
  from fractal_server.logger import set_logger
29
29
 
@@ -101,7 +101,7 @@ async def _get_task_by_source(
101
101
 
102
102
  async def _get_task_by_taskimport(
103
103
  *,
104
- task_import: TaskImportV2,
104
+ task_import: TaskImport,
105
105
  task_groups_list: list[TaskGroupV2],
106
106
  user_id: int,
107
107
  default_group_id: int | None,
@@ -207,15 +207,15 @@ async def _get_task_by_taskimport(
207
207
 
208
208
  @router.post(
209
209
  "/project/{project_id}/workflow/import/",
210
- response_model=WorkflowReadV2WithWarnings,
210
+ response_model=WorkflowReadWithWarnings,
211
211
  status_code=status.HTTP_201_CREATED,
212
212
  )
213
213
  async def import_workflow(
214
214
  project_id: int,
215
- workflow_import: WorkflowImportV2,
215
+ workflow_import: WorkflowImport,
216
216
  user: UserOAuth = Depends(current_user_act_ver_prof),
217
217
  db: AsyncSession = Depends(get_async_db),
218
- ) -> WorkflowReadV2WithWarnings:
218
+ ) -> WorkflowReadWithWarnings:
219
219
  """
220
220
  Import an existing workflow into a project and create required objects.
221
221
  """
@@ -246,7 +246,7 @@ async def import_workflow(
246
246
  list_task_ids = []
247
247
  for wf_task in workflow_import.task_list:
248
248
  task_import = wf_task.task
249
- if isinstance(task_import, TaskImportV2Legacy):
249
+ if isinstance(task_import, TaskImportLegacy):
250
250
  task_id = await _get_task_by_source(
251
251
  source=task_import.source,
252
252
  task_groups_list=task_group_list,
@@ -264,7 +264,7 @@ async def import_workflow(
264
264
  status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
265
265
  detail=f"Could not find a task matching with {wf_task.task}.",
266
266
  )
267
- new_wf_task = WorkflowTaskCreateV2(
267
+ new_wf_task = WorkflowTaskCreate(
268
268
  **wf_task.model_dump(exclude_none=True, exclude={"task"})
269
269
  )
270
270
  list_wf_tasks.append(new_wf_task)
@@ -11,9 +11,9 @@ from fractal_server.app.db import get_async_db
11
11
  from fractal_server.app.models import UserOAuth
12
12
  from fractal_server.app.routes.auth import current_user_act_ver_prof
13
13
  from fractal_server.app.schemas.v2 import TaskType
14
- from fractal_server.app.schemas.v2 import WorkflowTaskCreateV2
15
- from fractal_server.app.schemas.v2 import WorkflowTaskReadV2
16
- from fractal_server.app.schemas.v2 import WorkflowTaskUpdateV2
14
+ from fractal_server.app.schemas.v2 import WorkflowTaskCreate
15
+ from fractal_server.app.schemas.v2 import WorkflowTaskRead
16
+ from fractal_server.app.schemas.v2 import WorkflowTaskUpdate
17
17
  from fractal_server.app.schemas.v2.sharing import ProjectPermissions
18
18
 
19
19
  from ._aux_functions import _get_workflow_check_access
@@ -28,17 +28,17 @@ router = APIRouter()
28
28
 
29
29
  @router.post(
30
30
  "/project/{project_id}/workflow/{workflow_id}/wftask/",
31
- response_model=WorkflowTaskReadV2,
31
+ response_model=WorkflowTaskRead,
32
32
  status_code=status.HTTP_201_CREATED,
33
33
  )
34
34
  async def create_workflowtask(
35
35
  project_id: int,
36
36
  workflow_id: int,
37
37
  task_id: int,
38
- wftask: WorkflowTaskCreateV2,
38
+ wftask: WorkflowTaskCreate,
39
39
  user: UserOAuth = Depends(current_user_act_ver_prof),
40
40
  db: AsyncSession = Depends(get_async_db),
41
- ) -> WorkflowTaskReadV2 | None:
41
+ ) -> WorkflowTaskRead | None:
42
42
  """
43
43
  Add a WorkflowTask to a Workflow
44
44
  """
@@ -100,7 +100,7 @@ async def create_workflowtask(
100
100
 
101
101
  @router.get(
102
102
  "/project/{project_id}/workflow/{workflow_id}/wftask/{workflow_task_id}/",
103
- response_model=WorkflowTaskReadV2,
103
+ response_model=WorkflowTaskRead,
104
104
  )
105
105
  async def read_workflowtask(
106
106
  project_id: int,
@@ -122,16 +122,16 @@ async def read_workflowtask(
122
122
 
123
123
  @router.patch(
124
124
  "/project/{project_id}/workflow/{workflow_id}/wftask/{workflow_task_id}/",
125
- response_model=WorkflowTaskReadV2,
125
+ response_model=WorkflowTaskRead,
126
126
  )
127
127
  async def update_workflowtask(
128
128
  project_id: int,
129
129
  workflow_id: int,
130
130
  workflow_task_id: int,
131
- workflow_task_update: WorkflowTaskUpdateV2,
131
+ workflow_task_update: WorkflowTaskUpdate,
132
132
  user: UserOAuth = Depends(current_user_act_ver_prof),
133
133
  db: AsyncSession = Depends(get_async_db),
134
- ) -> WorkflowTaskReadV2 | None:
134
+ ) -> WorkflowTaskRead | None:
135
135
  """
136
136
  Edit a WorkflowTask of a Workflow
137
137
  """
@@ -1,12 +1,21 @@
1
+ from os.path import normpath
2
+ from pathlib import Path
3
+
1
4
  from fastapi import HTTPException
2
5
  from fastapi import status
3
6
  from sqlalchemy.ext.asyncio import AsyncSession
7
+ from sqlmodel import and_
4
8
  from sqlmodel import asc
9
+ from sqlmodel import not_
10
+ from sqlmodel import or_
5
11
  from sqlmodel import select
6
12
 
7
13
  from fractal_server.app.models.linkusergroup import LinkUserGroup
14
+ from fractal_server.app.models.linkuserproject import LinkUserProjectV2
8
15
  from fractal_server.app.models.security import UserGroup
9
16
  from fractal_server.app.models.security import UserOAuth
17
+ from fractal_server.app.models.v2.dataset import DatasetV2
18
+ from fractal_server.app.models.v2.project import ProjectV2
10
19
  from fractal_server.app.schemas.user import UserRead
11
20
  from fractal_server.app.schemas.user_group import UserGroupRead
12
21
  from fractal_server.config import get_settings
@@ -178,3 +187,93 @@ async def _verify_user_belongs_to_group(
178
187
  f"to UserGroup {user_group_id}"
179
188
  ),
180
189
  )
190
+
191
+
192
+ async def _check_project_dirs_update(
193
+ *,
194
+ old_project_dirs: list[str],
195
+ new_project_dirs: list[str],
196
+ user_id: int,
197
+ db: AsyncSession,
198
+ ) -> None:
199
+ """
200
+ Raises 422 if by replacing user's `project_dirs` with new ones we are
201
+ removing the access to a `zarr_dir` used by some dataset.
202
+
203
+ Note both `old_project_dirs` and `new_project_dirs` have been
204
+ normalized through `os.path.normpath`, which notably strips any trailing
205
+ `/` character. To be safe, we also re-normalize them within this function.
206
+ """
207
+ # Create a list of all the old project dirs that will lose privileges.
208
+ # E.g.:
209
+ # old_project_dirs = ["/a", "/b", "/c/d", "/e/f"]
210
+ # new_project_dirs = ["/a", "/c", "/e/f/g1", "/e/f/g2"]
211
+ # removed_project_dirs == ["/b", "/e/f"]
212
+ removed_project_dirs = [
213
+ old_project_dir
214
+ for old_project_dir in old_project_dirs
215
+ if not any(
216
+ Path(old_project_dir).is_relative_to(new_project_dir)
217
+ for new_project_dir in new_project_dirs
218
+ )
219
+ ]
220
+ if removed_project_dirs:
221
+ # Query all the `zarr_dir`s linked to the user such that `zarr_dir`
222
+ # starts with one of the project dirs in `removed_project_dirs`.
223
+ stmt = (
224
+ select(DatasetV2.zarr_dir)
225
+ .join(ProjectV2, ProjectV2.id == DatasetV2.project_id)
226
+ .join(
227
+ LinkUserProjectV2,
228
+ LinkUserProjectV2.project_id == ProjectV2.id,
229
+ )
230
+ .where(LinkUserProjectV2.user_id == user_id)
231
+ .where(
232
+ or_(
233
+ *[
234
+ DatasetV2.zarr_dir.startswith(normpath(old_project_dir))
235
+ for old_project_dir in removed_project_dirs
236
+ ]
237
+ )
238
+ )
239
+ )
240
+ if new_project_dirs:
241
+ stmt = stmt.where(
242
+ and_(
243
+ *[
244
+ not_(
245
+ DatasetV2.zarr_dir.startswith(
246
+ normpath(new_project_dir)
247
+ )
248
+ )
249
+ for new_project_dir in new_project_dirs
250
+ ]
251
+ )
252
+ )
253
+ res = await db.execute(stmt)
254
+
255
+ # Raise 422 if one of the query results is relative to a path in
256
+ # `removed_project_dirs`, but its not relative to any path in
257
+ # `new_project_dirs`.
258
+ if any(
259
+ (
260
+ any(
261
+ Path(zarr_dir).is_relative_to(old_project_dir)
262
+ for old_project_dir in removed_project_dirs
263
+ )
264
+ and not any(
265
+ Path(zarr_dir).is_relative_to(new_project_dir)
266
+ for new_project_dir in new_project_dirs
267
+ )
268
+ )
269
+ for zarr_dir in res.scalars().all()
270
+ ):
271
+ raise HTTPException(
272
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
273
+ detail=(
274
+ "You tried updating the user project_dirs, removing "
275
+ f"{removed_project_dirs}. This operation is not possible, "
276
+ "because it would make the user loose access to some of "
277
+ "their dataset zarr directories."
278
+ ),
279
+ )
@@ -28,6 +28,7 @@ from fractal_server.logger import set_logger
28
28
  from fractal_server.syringe import Inject
29
29
 
30
30
  from . import current_superuser_act
31
+ from ._aux_auth import _check_project_dirs_update
31
32
  from ._aux_auth import _get_default_usergroup_id_or_none
32
33
  from ._aux_auth import _get_single_user_with_groups
33
34
 
@@ -74,6 +75,14 @@ async def patch_user(
74
75
  detail=f"Profile {user_update.profile_id} not found.",
75
76
  )
76
77
 
78
+ if user_update.project_dirs is not None:
79
+ await _check_project_dirs_update(
80
+ old_project_dirs=user_to_patch.project_dirs,
81
+ new_project_dirs=user_update.project_dirs,
82
+ user_id=user_id,
83
+ db=db,
84
+ )
85
+
77
86
  # Modify user attributes
78
87
  try:
79
88
  user = await user_manager.update(
@@ -79,7 +79,7 @@ class UserUpdate(schemas.BaseUserUpdate):
79
79
  profile_id: int | None = None
80
80
  project_dirs: Annotated[
81
81
  ListUniqueAbsolutePathStr, AfterValidator(_validate_cmd_list)
82
- ] = None
82
+ ] = Field(default=None, min_length=1)
83
83
  slurm_accounts: ListUniqueNonEmptyString = None
84
84
 
85
85