fractal-server 2.17.2__py3-none-any.whl → 2.18.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 (108) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +2 -1
  3. fractal_server/app/models/linkuserproject.py +40 -0
  4. fractal_server/app/models/security.py +7 -5
  5. fractal_server/app/models/v2/job.py +13 -2
  6. fractal_server/app/models/v2/resource.py +13 -0
  7. fractal_server/app/routes/admin/v2/__init__.py +11 -11
  8. fractal_server/app/routes/admin/v2/accounting.py +2 -2
  9. fractal_server/app/routes/admin/v2/job.py +34 -23
  10. fractal_server/app/routes/admin/v2/sharing.py +103 -0
  11. fractal_server/app/routes/admin/v2/task.py +9 -8
  12. fractal_server/app/routes/admin/v2/task_group.py +94 -16
  13. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +20 -20
  14. fractal_server/app/routes/api/__init__.py +0 -9
  15. fractal_server/app/routes/api/v2/__init__.py +47 -47
  16. fractal_server/app/routes/api/v2/_aux_functions.py +65 -64
  17. fractal_server/app/routes/api/v2/_aux_functions_history.py +8 -3
  18. fractal_server/app/routes/api/v2/_aux_functions_sharing.py +97 -0
  19. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +4 -4
  20. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +2 -2
  21. fractal_server/app/routes/api/v2/dataset.py +89 -77
  22. fractal_server/app/routes/api/v2/history.py +28 -16
  23. fractal_server/app/routes/api/v2/images.py +22 -8
  24. fractal_server/app/routes/api/v2/job.py +40 -24
  25. fractal_server/app/routes/api/v2/pre_submission_checks.py +13 -6
  26. fractal_server/app/routes/api/v2/project.py +48 -25
  27. fractal_server/app/routes/api/v2/sharing.py +311 -0
  28. fractal_server/app/routes/api/v2/status_legacy.py +22 -33
  29. fractal_server/app/routes/api/v2/submit.py +76 -71
  30. fractal_server/app/routes/api/v2/task.py +15 -17
  31. fractal_server/app/routes/api/v2/task_collection.py +18 -18
  32. fractal_server/app/routes/api/v2/task_collection_custom.py +11 -13
  33. fractal_server/app/routes/api/v2/task_collection_pixi.py +9 -9
  34. fractal_server/app/routes/api/v2/task_group.py +18 -18
  35. fractal_server/app/routes/api/v2/task_group_lifecycle.py +26 -26
  36. fractal_server/app/routes/api/v2/task_version_update.py +12 -9
  37. fractal_server/app/routes/api/v2/workflow.py +41 -29
  38. fractal_server/app/routes/api/v2/workflow_import.py +25 -23
  39. fractal_server/app/routes/api/v2/workflowtask.py +25 -17
  40. fractal_server/app/routes/auth/_aux_auth.py +100 -0
  41. fractal_server/app/routes/auth/current_user.py +0 -63
  42. fractal_server/app/routes/auth/group.py +1 -30
  43. fractal_server/app/routes/auth/router.py +2 -0
  44. fractal_server/app/routes/auth/users.py +9 -0
  45. fractal_server/app/routes/auth/viewer_paths.py +43 -0
  46. fractal_server/app/schemas/user.py +29 -12
  47. fractal_server/app/schemas/user_group.py +0 -15
  48. fractal_server/app/schemas/v2/__init__.py +55 -48
  49. fractal_server/app/schemas/v2/dataset.py +35 -13
  50. fractal_server/app/schemas/v2/dumps.py +9 -9
  51. fractal_server/app/schemas/v2/job.py +11 -11
  52. fractal_server/app/schemas/v2/project.py +3 -3
  53. fractal_server/app/schemas/v2/resource.py +13 -4
  54. fractal_server/app/schemas/v2/sharing.py +99 -0
  55. fractal_server/app/schemas/v2/status_legacy.py +3 -3
  56. fractal_server/app/schemas/v2/task.py +6 -6
  57. fractal_server/app/schemas/v2/task_collection.py +4 -4
  58. fractal_server/app/schemas/v2/task_group.py +16 -16
  59. fractal_server/app/schemas/v2/workflow.py +16 -16
  60. fractal_server/app/schemas/v2/workflowtask.py +14 -14
  61. fractal_server/app/security/__init__.py +1 -1
  62. fractal_server/app/shutdown.py +6 -6
  63. fractal_server/config/__init__.py +0 -6
  64. fractal_server/config/_data.py +0 -79
  65. fractal_server/config/_main.py +6 -1
  66. fractal_server/data_migrations/2_18_0.py +30 -0
  67. fractal_server/images/models.py +1 -2
  68. fractal_server/main.py +72 -11
  69. fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py +60 -0
  70. fractal_server/migrations/versions/88270f589c9b_add_prevent_new_submissions.py +39 -0
  71. fractal_server/migrations/versions/bc0e8b3327a7_project_sharing.py +72 -0
  72. fractal_server/migrations/versions/f0702066b007_one_submitted_job_per_dataset.py +40 -0
  73. fractal_server/runner/config/_slurm.py +2 -0
  74. fractal_server/runner/executors/slurm_common/_batching.py +4 -10
  75. fractal_server/runner/executors/slurm_common/slurm_config.py +1 -0
  76. fractal_server/runner/executors/slurm_ssh/runner.py +1 -1
  77. fractal_server/runner/executors/slurm_sudo/runner.py +1 -1
  78. fractal_server/runner/v2/_local.py +4 -3
  79. fractal_server/runner/v2/_slurm_ssh.py +4 -3
  80. fractal_server/runner/v2/_slurm_sudo.py +4 -3
  81. fractal_server/runner/v2/runner.py +36 -17
  82. fractal_server/runner/v2/runner_functions.py +11 -14
  83. fractal_server/runner/v2/submit_workflow.py +22 -9
  84. fractal_server/tasks/v2/local/_utils.py +2 -2
  85. fractal_server/tasks/v2/local/collect.py +5 -6
  86. fractal_server/tasks/v2/local/collect_pixi.py +5 -6
  87. fractal_server/tasks/v2/local/deactivate.py +7 -7
  88. fractal_server/tasks/v2/local/deactivate_pixi.py +3 -3
  89. fractal_server/tasks/v2/local/delete.py +5 -5
  90. fractal_server/tasks/v2/local/reactivate.py +5 -5
  91. fractal_server/tasks/v2/local/reactivate_pixi.py +5 -5
  92. fractal_server/tasks/v2/ssh/collect.py +5 -5
  93. fractal_server/tasks/v2/ssh/collect_pixi.py +5 -5
  94. fractal_server/tasks/v2/ssh/deactivate.py +7 -7
  95. fractal_server/tasks/v2/ssh/deactivate_pixi.py +2 -2
  96. fractal_server/tasks/v2/ssh/delete.py +5 -5
  97. fractal_server/tasks/v2/ssh/reactivate.py +5 -5
  98. fractal_server/tasks/v2/ssh/reactivate_pixi.py +5 -5
  99. fractal_server/tasks/v2/utils_background.py +7 -7
  100. fractal_server/tasks/v2/utils_database.py +5 -5
  101. fractal_server/types/__init__.py +22 -0
  102. fractal_server/types/validators/__init__.py +3 -0
  103. fractal_server/types/validators/_common_validators.py +32 -0
  104. {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/METADATA +3 -2
  105. {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/RECORD +108 -98
  106. {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/WHEEL +0 -0
  107. {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/entry_points.txt +0 -0
  108. {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,11 +3,10 @@ Auxiliary functions to get object from the database or perform simple checks
3
3
  """
4
4
 
5
5
  from typing import Any
6
- from typing import Literal
6
+ from typing import TypedDict
7
7
 
8
8
  from fastapi import HTTPException
9
9
  from fastapi import status
10
- from sqlalchemy.exc import MultipleResultsFound
11
10
  from sqlalchemy.orm.attributes import flag_modified
12
11
  from sqlmodel import select
13
12
  from sqlmodel.sql.expression import SelectOfScalar
@@ -23,16 +22,18 @@ from fractal_server.app.models.v2 import ProjectV2
23
22
  from fractal_server.app.models.v2 import TaskV2
24
23
  from fractal_server.app.models.v2 import WorkflowTaskV2
25
24
  from fractal_server.app.models.v2 import WorkflowV2
26
- from fractal_server.app.schemas.v2 import JobStatusTypeV2
25
+ from fractal_server.app.schemas.v2 import JobStatusType
26
+ from fractal_server.app.schemas.v2 import ProjectPermissions
27
27
  from fractal_server.logger import set_logger
28
28
 
29
29
  logger = set_logger(__name__)
30
30
 
31
31
 
32
- async def _get_project_check_owner(
32
+ async def _get_project_check_access(
33
33
  *,
34
34
  project_id: int,
35
35
  user_id: int,
36
+ required_permissions: ProjectPermissions,
36
37
  db: AsyncSession,
37
38
  ) -> ProjectV2:
38
39
  """
@@ -41,6 +42,7 @@ async def _get_project_check_owner(
41
42
  Args:
42
43
  project_id:
43
44
  user_id:
45
+ required_permissions:
44
46
  db:
45
47
 
46
48
  Returns:
@@ -48,31 +50,42 @@ async def _get_project_check_owner(
48
50
 
49
51
  Raises:
50
52
  HTTPException(status_code=403_FORBIDDEN):
51
- If the user is not a member of the project
53
+ - If the user is not a member of the project;
54
+ - If the user has not accepted the invitation yet;
55
+ - If the user has not the target permissions.
52
56
  HTTPException(status_code=404_NOT_FOUND):
53
57
  If the project does not exist
54
58
  """
55
59
  project = await db.get(ProjectV2, project_id)
56
-
57
- link_user_project = await db.get(LinkUserProjectV2, (project_id, user_id))
58
- if not project:
60
+ if project is None:
59
61
  raise HTTPException(
60
62
  status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
61
63
  )
62
- if not link_user_project:
64
+
65
+ link_user_project = await db.get(LinkUserProjectV2, (project_id, user_id))
66
+ if (
67
+ link_user_project is None
68
+ or not link_user_project.is_verified
69
+ or required_permissions not in link_user_project.permissions
70
+ ):
63
71
  raise HTTPException(
64
72
  status_code=status.HTTP_403_FORBIDDEN,
65
- detail=f"Not allowed on project {project_id}",
73
+ detail=(
74
+ "You are not authorized to perform this action. "
75
+ "If you think this is by mistake, "
76
+ "please contact the project owner."
77
+ ),
66
78
  )
67
79
 
68
80
  return project
69
81
 
70
82
 
71
- async def _get_workflow_check_owner(
83
+ async def _get_workflow_check_access(
72
84
  *,
73
85
  workflow_id: int,
74
86
  project_id: int,
75
87
  user_id: int,
88
+ required_permissions: ProjectPermissions,
76
89
  db: AsyncSession,
77
90
  ) -> WorkflowV2:
78
91
  """
@@ -96,8 +109,11 @@ async def _get_workflow_check_owner(
96
109
  """
97
110
 
98
111
  # Access control for project
99
- await _get_project_check_owner(
100
- project_id=project_id, user_id=user_id, db=db
112
+ await _get_project_check_access(
113
+ project_id=project_id,
114
+ user_id=user_id,
115
+ required_permissions=required_permissions,
116
+ db=db,
101
117
  )
102
118
 
103
119
  res = await db.execute(
@@ -116,12 +132,13 @@ async def _get_workflow_check_owner(
116
132
  return workflow
117
133
 
118
134
 
119
- async def _get_workflow_task_check_owner(
135
+ async def _get_workflow_task_check_access(
120
136
  *,
121
137
  project_id: int,
122
138
  workflow_id: int,
123
139
  workflow_task_id: int,
124
140
  user_id: int,
141
+ required_permissions: ProjectPermissions,
125
142
  db: AsyncSession,
126
143
  ) -> tuple[WorkflowTaskV2, WorkflowV2]:
127
144
  """
@@ -146,10 +163,11 @@ async def _get_workflow_task_check_owner(
146
163
  """
147
164
 
148
165
  # Access control for workflow
149
- workflow = await _get_workflow_check_owner(
166
+ workflow = await _get_workflow_check_access(
150
167
  workflow_id=workflow_id,
151
168
  project_id=project_id,
152
169
  user_id=user_id,
170
+ required_permissions=required_permissions,
153
171
  db=db,
154
172
  )
155
173
 
@@ -223,6 +241,7 @@ async def _check_project_exists(
223
241
  .join(LinkUserProjectV2, LinkUserProjectV2.project_id == ProjectV2.id)
224
242
  .where(ProjectV2.name == project_name)
225
243
  .where(LinkUserProjectV2.user_id == user_id)
244
+ .where(LinkUserProjectV2.is_owner.is_(True))
226
245
  )
227
246
  res = await db.execute(stm)
228
247
  if res.scalars().all():
@@ -232,13 +251,19 @@ async def _check_project_exists(
232
251
  )
233
252
 
234
253
 
235
- async def _get_dataset_check_owner(
254
+ class DatasetOrProject(TypedDict):
255
+ dataset: DatasetV2
256
+ project: ProjectV2
257
+
258
+
259
+ async def _get_dataset_check_access(
236
260
  *,
237
261
  project_id: int,
238
262
  dataset_id: int,
239
263
  user_id: int,
264
+ required_permissions: ProjectPermissions,
240
265
  db: AsyncSession,
241
- ) -> dict[Literal["dataset", "project"], DatasetV2 | ProjectV2]:
266
+ ) -> DatasetOrProject:
242
267
  """
243
268
  Get a dataset and a project, after access control on the project
244
269
 
@@ -260,8 +285,11 @@ async def _get_dataset_check_owner(
260
285
  If the user is not a member of the project
261
286
  """
262
287
  # Access control for project
263
- project = await _get_project_check_owner(
264
- project_id=project_id, user_id=user_id, db=db
288
+ project = await _get_project_check_access(
289
+ project_id=project_id,
290
+ user_id=user_id,
291
+ required_permissions=required_permissions,
292
+ db=db,
265
293
  )
266
294
 
267
295
  res = await db.execute(
@@ -280,13 +308,19 @@ async def _get_dataset_check_owner(
280
308
  return dict(dataset=dataset, project=project)
281
309
 
282
310
 
283
- async def _get_job_check_owner(
311
+ class JobAndProject(TypedDict):
312
+ job: JobV2
313
+ project: ProjectV2
314
+
315
+
316
+ async def _get_job_check_access(
284
317
  *,
285
318
  project_id: int,
286
319
  job_id: int,
287
320
  user_id: int,
321
+ required_permissions: ProjectPermissions,
288
322
  db: AsyncSession,
289
- ) -> dict[Literal["job", "project"], JobV2 | ProjectV2]:
323
+ ) -> JobAndProject:
290
324
  """
291
325
  Get a job and a project, after access control on the project
292
326
 
@@ -308,9 +342,10 @@ async def _get_job_check_owner(
308
342
  If the user is not a member of the project
309
343
  """
310
344
  # Access control for project
311
- project = await _get_project_check_owner(
345
+ project = await _get_project_check_access(
312
346
  project_id=project_id,
313
347
  user_id=user_id,
348
+ required_permissions=required_permissions,
314
349
  db=db,
315
350
  )
316
351
 
@@ -335,7 +370,7 @@ def _get_submitted_jobs_statement() -> SelectOfScalar:
335
370
  A sqlmodel statement that selects all `Job`s with
336
371
  `Job.status` equal to `submitted`.
337
372
  """
338
- stm = select(JobV2).where(JobV2.status == JobStatusTypeV2.SUBMITTED)
373
+ stm = select(JobV2).where(JobV2.status == JobStatusType.SUBMITTED)
339
374
  return stm
340
375
 
341
376
 
@@ -345,7 +380,7 @@ async def _workflow_has_submitted_job(
345
380
  ) -> bool:
346
381
  res = await db.execute(
347
382
  select(JobV2.id)
348
- .where(JobV2.status == JobStatusTypeV2.SUBMITTED)
383
+ .where(JobV2.status == JobStatusType.SUBMITTED)
349
384
  .where(JobV2.workflow_id == workflow_id)
350
385
  .limit(1)
351
386
  )
@@ -427,8 +462,9 @@ async def _workflow_insert_task(
427
462
  return wf_task
428
463
 
429
464
 
430
- async def clean_app_job_list_v2(
431
- db: AsyncSession, jobs_list: list[int]
465
+ async def clean_app_job_list(
466
+ db: AsyncSession,
467
+ jobs_list: list[int],
432
468
  ) -> list[int]:
433
469
  """
434
470
  Remove from a job list all jobs with status different from submitted.
@@ -440,14 +476,14 @@ async def clean_app_job_list_v2(
440
476
  Return:
441
477
  List of IDs for submitted jobs.
442
478
  """
479
+ logger.info(f"[clean_app_job_list] START - {jobs_list=}.")
443
480
  stmt = select(JobV2).where(JobV2.id.in_(jobs_list))
444
481
  result = await db.execute(stmt)
445
482
  db_jobs_list = result.scalars().all()
446
483
  submitted_job_ids = [
447
- job.id
448
- for job in db_jobs_list
449
- if job.status == JobStatusTypeV2.SUBMITTED
484
+ job.id for job in db_jobs_list if job.status == JobStatusType.SUBMITTED
450
485
  ]
486
+ logger.info(f"[clean_app_job_list] END - {submitted_job_ids=}.")
451
487
  return submitted_job_ids
452
488
 
453
489
 
@@ -517,41 +553,6 @@ async def _get_workflowtask_or_404(
517
553
  return wftask
518
554
 
519
555
 
520
- async def _get_submitted_job_or_none(
521
- *,
522
- dataset_id: int,
523
- workflow_id: int,
524
- db: AsyncSession,
525
- ) -> JobV2 | None:
526
- """
527
- Get the submitted job for given dataset/workflow, if any.
528
-
529
- This function also handles the invalid branch where more than one job
530
- is found.
531
-
532
- Args:
533
- dataset_id:
534
- workflow_id:
535
- db:
536
- """
537
- res = await db.execute(
538
- _get_submitted_jobs_statement()
539
- .where(JobV2.dataset_id == dataset_id)
540
- .where(JobV2.workflow_id == workflow_id)
541
- )
542
- try:
543
- return res.scalars().one_or_none()
544
- except MultipleResultsFound as e:
545
- error_msg = (
546
- f"Multiple running jobs found for {dataset_id=} and {workflow_id=}."
547
- )
548
- logger.error(f"{error_msg} Original error: {str(e)}.")
549
- raise HTTPException(
550
- status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
551
- detail=error_msg,
552
- )
553
-
554
-
555
556
  async def _get_user_resource_id(user_id: int, db: AsyncSession) -> int | None:
556
557
  res = await db.execute(
557
558
  select(Resource.id)
@@ -13,12 +13,13 @@ from fractal_server.app.models.v2 import HistoryUnit
13
13
  from fractal_server.app.models.v2 import WorkflowV2
14
14
  from fractal_server.app.routes.api.v2._aux_functions import _get_dataset_or_404
15
15
  from fractal_server.app.routes.api.v2._aux_functions import (
16
- _get_project_check_owner,
16
+ _get_project_check_access,
17
17
  )
18
18
  from fractal_server.app.routes.api.v2._aux_functions import _get_workflow_or_404
19
19
  from fractal_server.app.routes.api.v2._aux_functions import (
20
20
  _get_workflowtask_or_404,
21
21
  )
22
+ from fractal_server.app.schemas.v2.sharing import ProjectPermissions
22
23
  from fractal_server.logger import set_logger
23
24
  from fractal_server.zip_tools import _read_single_file_from_zip
24
25
 
@@ -119,6 +120,7 @@ async def _verify_workflow_and_dataset_access(
119
120
  workflow_id: int,
120
121
  dataset_id: int,
121
122
  user_id: int,
123
+ required_permissions: ProjectPermissions,
122
124
  db: AsyncSession,
123
125
  ) -> dict[Literal["dataset", "workflow"], DatasetV2 | WorkflowV2]:
124
126
  """
@@ -131,9 +133,10 @@ async def _verify_workflow_and_dataset_access(
131
133
  user_id:
132
134
  db:
133
135
  """
134
- await _get_project_check_owner(
136
+ await _get_project_check_access(
135
137
  project_id=project_id,
136
138
  user_id=user_id,
139
+ required_permissions=required_permissions,
137
140
  db=db,
138
141
  )
139
142
  workflow = await _get_workflow_or_404(
@@ -158,12 +161,13 @@ async def _verify_workflow_and_dataset_access(
158
161
  return dict(dataset=dataset, workflow=workflow)
159
162
 
160
163
 
161
- async def get_wftask_check_owner(
164
+ async def get_wftask_check_access(
162
165
  *,
163
166
  project_id: int,
164
167
  dataset_id: int,
165
168
  workflowtask_id: int,
166
169
  user_id: int,
170
+ required_permissions: ProjectPermissions,
167
171
  db: AsyncSession,
168
172
  ) -> WorkflowTaskV2:
169
173
  """
@@ -184,6 +188,7 @@ async def get_wftask_check_owner(
184
188
  project_id=project_id,
185
189
  dataset_id=dataset_id,
186
190
  workflow_id=wftask.workflow_id,
191
+ required_permissions=required_permissions,
187
192
  user_id=user_id,
188
193
  db=db,
189
194
  )
@@ -0,0 +1,97 @@
1
+ from fastapi import HTTPException
2
+ from fastapi import status
3
+ from sqlmodel import select
4
+
5
+ from fractal_server.app.db import AsyncSession
6
+ from fractal_server.app.models import UserOAuth
7
+ from fractal_server.app.models.v2 import LinkUserProjectV2
8
+
9
+
10
+ async def raise_403_if_not_owner(
11
+ *,
12
+ user_id: int,
13
+ project_id: int,
14
+ db: AsyncSession,
15
+ ) -> None:
16
+ """
17
+ Raises 403 if User[`user_id`] is not owner of Project[`project_id`],
18
+ regardless of whether the User or Project exists.
19
+ """
20
+ res = await db.execute(
21
+ select(LinkUserProjectV2)
22
+ .where(LinkUserProjectV2.project_id == project_id)
23
+ .where(LinkUserProjectV2.user_id == user_id)
24
+ .where(LinkUserProjectV2.is_owner.is_(True))
25
+ )
26
+ link = res.scalars().one_or_none()
27
+ if link is None:
28
+ raise HTTPException(
29
+ status_code=status.HTTP_403_FORBIDDEN,
30
+ detail="Current user is not the project owner.",
31
+ )
32
+ return link
33
+
34
+
35
+ async def get_link_or_404(
36
+ *, user_id: int, project_id: int, db: AsyncSession
37
+ ) -> LinkUserProjectV2:
38
+ """
39
+ Raises 404 if User[`user_id`] is not linked to Project[`project_id`],
40
+ regardless of whether the User or Project exists.
41
+ """
42
+ link = await db.get(LinkUserProjectV2, (project_id, user_id))
43
+ if link is None:
44
+ raise HTTPException(
45
+ status_code=status.HTTP_404_NOT_FOUND,
46
+ detail="User is not linked to project.",
47
+ )
48
+ return link
49
+
50
+
51
+ async def get_pending_invitation_or_404(
52
+ *, user_id: int, project_id: int, db: AsyncSession
53
+ ) -> LinkUserProjectV2:
54
+ """
55
+ Raises 404 if User[`user_id`] has not a pending invitation to
56
+ Project[`project_id`], regardless of whether the User or Project exists.
57
+ """
58
+ link = await get_link_or_404(user_id=user_id, project_id=project_id, db=db)
59
+ if link.is_verified:
60
+ raise HTTPException(
61
+ status_code=status.HTTP_404_NOT_FOUND,
62
+ detail="No pending invitation for user on this project.",
63
+ )
64
+ return link
65
+
66
+
67
+ async def raise_422_if_link_exists(
68
+ *, user_id: int, project_id: int, db: AsyncSession
69
+ ) -> None:
70
+ """
71
+ Raises 422 if User[`user_id`] is linked Project[`project_id`], regardless
72
+ of whether the User or Project exists.
73
+ """
74
+ link = await db.get(LinkUserProjectV2, (project_id, user_id))
75
+ if link is not None:
76
+ raise HTTPException(
77
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
78
+ detail="User is already associated to project.",
79
+ )
80
+ return
81
+
82
+
83
+ async def get_user_id_from_email_or_404(
84
+ *, user_email: str, db: AsyncSession
85
+ ) -> int:
86
+ """
87
+ Raises 404 if there is no User with email `user_email`.
88
+ """
89
+ res = await db.execute(
90
+ select(UserOAuth.id).where(UserOAuth.email == user_email)
91
+ )
92
+ user_id = res.scalar_one_or_none()
93
+ if user_id is None:
94
+ raise HTTPException(
95
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found."
96
+ )
97
+ return user_id
@@ -14,8 +14,8 @@ from fractal_server.app.models.v2 import TaskGroupV2
14
14
  from fractal_server.app.models.v2 import TaskV2
15
15
  from fractal_server.app.models.v2 import WorkflowTaskV2
16
16
  from fractal_server.app.models.v2 import WorkflowV2
17
- from fractal_server.app.schemas.v2 import JobStatusTypeV2
18
- from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
17
+ from fractal_server.app.schemas.v2 import JobStatusType
18
+ from fractal_server.app.schemas.v2 import TaskGroupActivityStatus
19
19
  from fractal_server.logger import set_logger
20
20
  from fractal_server.tasks.v2.utils_package_names import normalize_package_name
21
21
 
@@ -171,7 +171,7 @@ async def check_no_ongoing_activity(
171
171
  stm = (
172
172
  select(TaskGroupActivityV2)
173
173
  .where(TaskGroupActivityV2.taskgroupv2_id == task_group_id)
174
- .where(TaskGroupActivityV2.status == TaskGroupActivityStatusV2.ONGOING)
174
+ .where(TaskGroupActivityV2.status == TaskGroupActivityStatus.ONGOING)
175
175
  )
176
176
  res = await db.execute(stm)
177
177
  ongoing_activities = res.scalars().all()
@@ -213,7 +213,7 @@ async def check_no_submitted_job(
213
213
  .join(TaskV2, WorkflowTaskV2.task_id == TaskV2.id)
214
214
  .where(WorkflowTaskV2.order >= JobV2.first_task_index)
215
215
  .where(WorkflowTaskV2.order <= JobV2.last_task_index)
216
- .where(JobV2.status == JobStatusTypeV2.SUBMITTED)
216
+ .where(JobV2.status == JobStatusType.SUBMITTED)
217
217
  .where(TaskV2.taskgroupv2_id == task_group_id)
218
218
  )
219
219
  res = await db.execute(stm)
@@ -27,7 +27,7 @@ from fractal_server.app.routes.auth._aux_auth import (
27
27
  from fractal_server.app.routes.auth._aux_auth import (
28
28
  _verify_user_belongs_to_group,
29
29
  )
30
- from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
30
+ from fractal_server.app.schemas.v2 import TaskGroupActivityAction
31
31
  from fractal_server.images.tools import merge_type_filters
32
32
  from fractal_server.logger import set_logger
33
33
 
@@ -252,7 +252,7 @@ async def _get_collection_task_group_activity_status_message(
252
252
  res = await db.execute(
253
253
  select(TaskGroupActivityV2)
254
254
  .where(TaskGroupActivityV2.taskgroupv2_id == task_group_id)
255
- .where(TaskGroupActivityV2.action == TaskGroupActivityActionV2.COLLECT)
255
+ .where(TaskGroupActivityV2.action == TaskGroupActivityAction.COLLECT)
256
256
  )
257
257
  task_group_activity_list = res.scalars().all()
258
258
  if len(task_group_activity_list) > 1: