fractal-server 2.17.1a1__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 (225) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +21 -19
  3. fractal_server/app/db/__init__.py +3 -3
  4. fractal_server/app/models/__init__.py +1 -0
  5. fractal_server/app/models/linkuserproject.py +43 -1
  6. fractal_server/app/models/security.py +28 -8
  7. fractal_server/app/models/v2/__init__.py +3 -1
  8. fractal_server/app/models/v2/accounting.py +9 -1
  9. fractal_server/app/models/v2/dataset.py +5 -1
  10. fractal_server/app/models/v2/history.py +15 -1
  11. fractal_server/app/models/v2/job.py +17 -2
  12. fractal_server/app/models/v2/profile.py +29 -0
  13. fractal_server/app/models/v2/project.py +4 -10
  14. fractal_server/app/models/v2/resource.py +17 -0
  15. fractal_server/app/models/v2/task_group.py +4 -3
  16. fractal_server/app/models/v2/workflow.py +2 -1
  17. fractal_server/app/routes/admin/v2/__init__.py +12 -13
  18. fractal_server/app/routes/admin/v2/accounting.py +3 -3
  19. fractal_server/app/routes/admin/v2/job.py +35 -24
  20. fractal_server/app/routes/admin/v2/profile.py +3 -2
  21. fractal_server/app/routes/admin/v2/resource.py +5 -5
  22. fractal_server/app/routes/admin/v2/sharing.py +103 -0
  23. fractal_server/app/routes/admin/v2/task.py +37 -26
  24. fractal_server/app/routes/admin/v2/task_group.py +94 -17
  25. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +21 -22
  26. fractal_server/app/routes/api/__init__.py +1 -9
  27. fractal_server/app/routes/api/v2/__init__.py +49 -50
  28. fractal_server/app/routes/api/v2/_aux_functions.py +132 -124
  29. fractal_server/app/routes/api/v2/_aux_functions_history.py +51 -23
  30. fractal_server/app/routes/api/v2/_aux_functions_sharing.py +97 -0
  31. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +6 -8
  32. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +7 -9
  33. fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +1 -2
  34. fractal_server/app/routes/api/v2/dataset.py +95 -102
  35. fractal_server/app/routes/api/v2/history.py +59 -33
  36. fractal_server/app/routes/api/v2/images.py +24 -9
  37. fractal_server/app/routes/api/v2/job.py +52 -33
  38. fractal_server/app/routes/api/v2/pre_submission_checks.py +16 -8
  39. fractal_server/app/routes/api/v2/project.py +65 -37
  40. fractal_server/app/routes/api/v2/sharing.py +311 -0
  41. fractal_server/app/routes/api/v2/status_legacy.py +31 -41
  42. fractal_server/app/routes/api/v2/submit.py +82 -78
  43. fractal_server/app/routes/api/v2/task.py +19 -20
  44. fractal_server/app/routes/api/v2/task_collection.py +41 -43
  45. fractal_server/app/routes/api/v2/task_collection_custom.py +19 -20
  46. fractal_server/app/routes/api/v2/task_collection_pixi.py +10 -11
  47. fractal_server/app/routes/api/v2/task_group.py +25 -24
  48. fractal_server/app/routes/api/v2/task_group_lifecycle.py +32 -32
  49. fractal_server/app/routes/api/v2/task_version_update.py +23 -19
  50. fractal_server/app/routes/api/v2/workflow.py +50 -55
  51. fractal_server/app/routes/api/v2/workflow_import.py +37 -37
  52. fractal_server/app/routes/api/v2/workflowtask.py +32 -26
  53. fractal_server/app/routes/auth/__init__.py +1 -3
  54. fractal_server/app/routes/auth/_aux_auth.py +101 -2
  55. fractal_server/app/routes/auth/current_user.py +2 -66
  56. fractal_server/app/routes/auth/group.py +8 -35
  57. fractal_server/app/routes/auth/login.py +1 -0
  58. fractal_server/app/routes/auth/oauth.py +4 -3
  59. fractal_server/app/routes/auth/register.py +4 -2
  60. fractal_server/app/routes/auth/router.py +2 -0
  61. fractal_server/app/routes/auth/users.py +19 -10
  62. fractal_server/app/routes/auth/viewer_paths.py +43 -0
  63. fractal_server/app/routes/aux/_job.py +1 -1
  64. fractal_server/app/routes/aux/_runner.py +2 -2
  65. fractal_server/app/routes/pagination.py +1 -1
  66. fractal_server/app/schemas/user.py +29 -12
  67. fractal_server/app/schemas/user_group.py +0 -15
  68. fractal_server/app/schemas/v2/__init__.py +55 -48
  69. fractal_server/app/schemas/v2/accounting.py +11 -0
  70. fractal_server/app/schemas/v2/dataset.py +57 -11
  71. fractal_server/app/schemas/v2/dumps.py +10 -9
  72. fractal_server/app/schemas/v2/job.py +11 -11
  73. fractal_server/app/schemas/v2/manifest.py +4 -3
  74. fractal_server/app/schemas/v2/profile.py +53 -2
  75. fractal_server/app/schemas/v2/project.py +3 -3
  76. fractal_server/app/schemas/v2/resource.py +121 -16
  77. fractal_server/app/schemas/v2/sharing.py +99 -0
  78. fractal_server/app/schemas/v2/status_legacy.py +3 -3
  79. fractal_server/app/schemas/v2/task.py +6 -7
  80. fractal_server/app/schemas/v2/task_collection.py +5 -5
  81. fractal_server/app/schemas/v2/task_group.py +16 -16
  82. fractal_server/app/schemas/v2/workflow.py +16 -16
  83. fractal_server/app/schemas/v2/workflowtask.py +16 -15
  84. fractal_server/app/security/__init__.py +5 -8
  85. fractal_server/app/security/signup_email.py +4 -5
  86. fractal_server/app/shutdown.py +6 -6
  87. fractal_server/config/__init__.py +0 -6
  88. fractal_server/config/_data.py +0 -68
  89. fractal_server/config/_database.py +19 -20
  90. fractal_server/config/_email.py +30 -38
  91. fractal_server/config/_main.py +38 -52
  92. fractal_server/config/_oauth.py +17 -21
  93. fractal_server/data_migrations/2_18_0.py +30 -0
  94. fractal_server/exceptions.py +4 -0
  95. fractal_server/images/models.py +4 -5
  96. fractal_server/images/status_tools.py +4 -2
  97. fractal_server/logger.py +1 -1
  98. fractal_server/main.py +75 -13
  99. fractal_server/migrations/versions/034a469ec2eb_task_groups.py +4 -8
  100. fractal_server/migrations/versions/091b01f51f88_add_usergroup_and_linkusergroup_table.py +1 -1
  101. fractal_server/migrations/versions/0f5f85bb2ae7_add_pre_pinned_packages.py +1 -0
  102. fractal_server/migrations/versions/19eca0dd47a9_user_settings_project_dir.py +1 -1
  103. fractal_server/migrations/versions/1a83a5260664_rename.py +1 -1
  104. fractal_server/migrations/versions/1eac13a26c83_drop_v1_tables.py +1 -0
  105. fractal_server/migrations/versions/316140ff7ee1_remove_usersettings_cache_dir.py +1 -1
  106. fractal_server/migrations/versions/40d6d6511b20_add_index_to_history_models.py +47 -0
  107. fractal_server/migrations/versions/45fbb391d7af_make_resource_id_fk_non_nullable.py +1 -1
  108. fractal_server/migrations/versions/47351f8c7ebc_drop_dataset_filters.py +1 -0
  109. fractal_server/migrations/versions/49d0856e9569_drop_table.py +2 -3
  110. fractal_server/migrations/versions/4c308bcaea2b_add_task_args_schema_and_task_args_.py +1 -1
  111. fractal_server/migrations/versions/4cedeb448a53_workflowtask_foreign_keys_not_nullables.py +1 -1
  112. fractal_server/migrations/versions/501961cfcd85_remove_link_between_v1_and_v2_tasks_.py +2 -1
  113. fractal_server/migrations/versions/50a13d6138fd_initial_schema.py +7 -19
  114. fractal_server/migrations/versions/5bf02391cfef_v2.py +4 -10
  115. fractal_server/migrations/versions/70e77f1c38b0_add_applyworkflow_first_task_index_and_.py +1 -0
  116. fractal_server/migrations/versions/71eefd1dd202_add_slurm_accounts.py +1 -1
  117. fractal_server/migrations/versions/7673fe18c05d_remove_project_dir_server_default.py +1 -1
  118. fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py +60 -0
  119. fractal_server/migrations/versions/791ce783d3d8_add_indices.py +1 -1
  120. fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py +1 -0
  121. fractal_server/migrations/versions/84bf0fffde30_add_dumps_to_applyworkflow.py +1 -0
  122. fractal_server/migrations/versions/88270f589c9b_add_prevent_new_submissions.py +39 -0
  123. fractal_server/migrations/versions/8e8f227a3e36_update_taskv2_post_2_7_0.py +2 -4
  124. fractal_server/migrations/versions/8f79bd162e35_add_docs_info_and_docs_link_to_task_.py +1 -1
  125. fractal_server/migrations/versions/94a47ea2d3ff_remove_cache_dir_slurm_user_and_slurm_.py +1 -0
  126. fractal_server/migrations/versions/969d84257cac_add_historyrun_task_id.py +1 -1
  127. fractal_server/migrations/versions/97f444d47249_add_applyworkflow_project_dump.py +1 -1
  128. fractal_server/migrations/versions/981d588fe248_add_executor_error_log.py +1 -1
  129. fractal_server/migrations/versions/99ea79d9e5d2_add_dataset_history.py +2 -4
  130. fractal_server/migrations/versions/9c5ae74c9b98_add_user_settings_table.py +1 -1
  131. fractal_server/migrations/versions/9db60297b8b2_set_ondelete.py +1 -1
  132. fractal_server/migrations/versions/9fd26a2b0de4_add_workflow_timestamp_created.py +1 -1
  133. fractal_server/migrations/versions/a7f4d6137b53_add_workflow_dump_to_applyworkflow.py +1 -1
  134. fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py +1 -0
  135. fractal_server/migrations/versions/af8673379a5c_drop_old_filter_columns.py +1 -0
  136. fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py +1 -1
  137. fractal_server/migrations/versions/b3ffb095f973_json_to_jsonb.py +1 -0
  138. fractal_server/migrations/versions/bc0e8b3327a7_project_sharing.py +72 -0
  139. fractal_server/migrations/versions/c90a7c76e996_job_id_in_history_run.py +1 -1
  140. fractal_server/migrations/versions/caba9fb1ea5e_drop_useroauth_user_settings_id.py +1 -1
  141. fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py +4 -9
  142. fractal_server/migrations/versions/d4fe3708d309_make_applyworkflow_workflow_dump_non_.py +1 -0
  143. fractal_server/migrations/versions/da2cb2ac4255_user_group_viewer_paths.py +1 -1
  144. fractal_server/migrations/versions/db09233ad13a_split_filters_and_keep_old_columns.py +1 -0
  145. fractal_server/migrations/versions/e0e717ae2f26_delete_linkuserproject_ondelete_project.py +50 -0
  146. fractal_server/migrations/versions/e75cac726012_make_applyworkflow_start_timestamp_not_.py +1 -0
  147. fractal_server/migrations/versions/e81103413827_add_job_type_filters.py +1 -1
  148. fractal_server/migrations/versions/efa89c30e0a4_add_project_timestamp_created.py +1 -0
  149. fractal_server/migrations/versions/f0702066b007_one_submitted_job_per_dataset.py +40 -0
  150. fractal_server/migrations/versions/f37aceb45062_make_historyunit_logfile_required.py +1 -1
  151. fractal_server/migrations/versions/f384e1c0cf5d_drop_task_default_args_columns.py +1 -0
  152. fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py +4 -9
  153. fractal_server/runner/config/_local.py +8 -5
  154. fractal_server/runner/config/_slurm.py +39 -33
  155. fractal_server/runner/config/slurm_mem_to_MB.py +0 -1
  156. fractal_server/runner/executors/base_runner.py +29 -4
  157. fractal_server/runner/executors/local/get_local_config.py +1 -0
  158. fractal_server/runner/executors/local/runner.py +14 -13
  159. fractal_server/runner/executors/slurm_common/_batching.py +9 -20
  160. fractal_server/runner/executors/slurm_common/base_slurm_runner.py +53 -27
  161. fractal_server/runner/executors/slurm_common/get_slurm_config.py +14 -7
  162. fractal_server/runner/executors/slurm_common/remote.py +3 -1
  163. fractal_server/runner/executors/slurm_common/slurm_config.py +2 -0
  164. fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -3
  165. fractal_server/runner/executors/slurm_ssh/runner.py +16 -11
  166. fractal_server/runner/executors/slurm_ssh/tar_commands.py +1 -0
  167. fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +1 -0
  168. fractal_server/runner/executors/slurm_sudo/runner.py +16 -11
  169. fractal_server/runner/task_files.py +9 -3
  170. fractal_server/runner/v2/_local.py +12 -6
  171. fractal_server/runner/v2/_slurm_ssh.py +14 -7
  172. fractal_server/runner/v2/_slurm_sudo.py +14 -7
  173. fractal_server/runner/v2/db_tools.py +0 -1
  174. fractal_server/runner/v2/deduplicate_list.py +2 -1
  175. fractal_server/runner/v2/runner.py +44 -28
  176. fractal_server/runner/v2/runner_functions.py +22 -28
  177. fractal_server/runner/v2/submit_workflow.py +29 -15
  178. fractal_server/ssh/_fabric.py +6 -13
  179. fractal_server/string_tools.py +0 -1
  180. fractal_server/syringe.py +1 -1
  181. fractal_server/tasks/config/_pixi.py +1 -1
  182. fractal_server/tasks/config/_python.py +16 -9
  183. fractal_server/tasks/utils.py +0 -1
  184. fractal_server/tasks/v2/local/_utils.py +3 -3
  185. fractal_server/tasks/v2/local/collect.py +15 -18
  186. fractal_server/tasks/v2/local/collect_pixi.py +14 -16
  187. fractal_server/tasks/v2/local/deactivate.py +14 -15
  188. fractal_server/tasks/v2/local/deactivate_pixi.py +7 -7
  189. fractal_server/tasks/v2/local/delete.py +6 -8
  190. fractal_server/tasks/v2/local/reactivate.py +12 -12
  191. fractal_server/tasks/v2/local/reactivate_pixi.py +12 -12
  192. fractal_server/tasks/v2/ssh/_utils.py +3 -3
  193. fractal_server/tasks/v2/ssh/collect.py +19 -24
  194. fractal_server/tasks/v2/ssh/collect_pixi.py +22 -24
  195. fractal_server/tasks/v2/ssh/deactivate.py +17 -15
  196. fractal_server/tasks/v2/ssh/deactivate_pixi.py +8 -7
  197. fractal_server/tasks/v2/ssh/delete.py +12 -10
  198. fractal_server/tasks/v2/ssh/reactivate.py +16 -16
  199. fractal_server/tasks/v2/ssh/reactivate_pixi.py +13 -14
  200. fractal_server/tasks/v2/templates/1_create_venv.sh +2 -0
  201. fractal_server/tasks/v2/templates/2_pip_install.sh +2 -0
  202. fractal_server/tasks/v2/templates/3_pip_freeze.sh +2 -0
  203. fractal_server/tasks/v2/templates/4_pip_show.sh +2 -0
  204. fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +3 -1
  205. fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +2 -0
  206. fractal_server/tasks/v2/templates/pixi_1_extract.sh +2 -0
  207. fractal_server/tasks/v2/templates/pixi_2_install.sh +2 -0
  208. fractal_server/tasks/v2/templates/pixi_3_post_install.sh +2 -0
  209. fractal_server/tasks/v2/utils_background.py +10 -10
  210. fractal_server/tasks/v2/utils_database.py +5 -5
  211. fractal_server/tasks/v2/utils_package_names.py +1 -2
  212. fractal_server/tasks/v2/utils_pixi.py +1 -3
  213. fractal_server/types/__init__.py +98 -1
  214. fractal_server/types/validators/__init__.py +3 -0
  215. fractal_server/types/validators/_common_validators.py +33 -3
  216. fractal_server/types/validators/_workflow_task_arguments_validators.py +1 -2
  217. fractal_server/utils.py +1 -0
  218. fractal_server/zip_tools.py +34 -0
  219. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/METADATA +3 -2
  220. fractal_server-2.18.0.dist-info/RECORD +275 -0
  221. fractal_server/app/routes/admin/v2/project.py +0 -41
  222. fractal_server-2.17.1a1.dist-info/RECORD +0 -264
  223. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/WHEEL +0 -0
  224. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/entry_points.txt +0 -0
  225. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,28 +5,33 @@ from fastapi import Response
5
5
  from fastapi import status
6
6
  from sqlmodel import select
7
7
 
8
- from .....logger import reset_logger_handlers
9
- from .....logger import set_logger
10
- from ....db import AsyncSession
11
- from ....db import get_async_db
12
- from ....models.v2 import JobV2
13
- from ....models.v2 import LinkUserProjectV2
14
- from ....models.v2 import ProjectV2
15
- from ....schemas.v2 import ProjectCreateV2
16
- from ....schemas.v2 import ProjectReadV2
17
- from ....schemas.v2 import ProjectUpdateV2
18
- from ...aux.validate_user_profile import validate_user_profile
19
- from ._aux_functions import _check_project_exists
20
- from ._aux_functions import _get_project_check_owner
21
- from ._aux_functions import _get_submitted_jobs_statement
8
+ from fractal_server.app.db import AsyncSession
9
+ from fractal_server.app.db import get_async_db
22
10
  from fractal_server.app.models import UserOAuth
11
+ from fractal_server.app.models.v2 import JobV2
12
+ from fractal_server.app.models.v2 import LinkUserProjectV2
13
+ from fractal_server.app.models.v2 import ProjectV2
23
14
  from fractal_server.app.routes.auth import current_user_act_ver_prof
15
+ from fractal_server.app.routes.aux.validate_user_profile import (
16
+ validate_user_profile,
17
+ )
18
+ from fractal_server.app.schemas.v2 import ProjectCreate
19
+ from fractal_server.app.schemas.v2 import ProjectPermissions
20
+ from fractal_server.app.schemas.v2 import ProjectRead
21
+ from fractal_server.app.schemas.v2 import ProjectUpdate
22
+ from fractal_server.logger import set_logger
24
23
 
24
+ from ._aux_functions import _check_project_exists
25
+ from ._aux_functions import _get_project_check_access
26
+ from ._aux_functions import _get_submitted_jobs_statement
27
+
28
+ logger = set_logger(__name__)
25
29
  router = APIRouter()
26
30
 
27
31
 
28
- @router.get("/project/", response_model=list[ProjectReadV2])
32
+ @router.get("/project/", response_model=list[ProjectRead])
29
33
  async def get_list_project(
34
+ is_owner: bool = True,
30
35
  user: UserOAuth = Depends(current_user_act_ver_prof),
31
36
  db: AsyncSession = Depends(get_async_db),
32
37
  ) -> list[ProjectV2]:
@@ -35,8 +40,10 @@ async def get_list_project(
35
40
  """
36
41
  stm = (
37
42
  select(ProjectV2)
38
- .join(LinkUserProjectV2)
43
+ .join(LinkUserProjectV2, LinkUserProjectV2.project_id == ProjectV2.id)
39
44
  .where(LinkUserProjectV2.user_id == user.id)
45
+ .where(LinkUserProjectV2.is_owner == is_owner)
46
+ .where(LinkUserProjectV2.is_verified.is_(True))
40
47
  )
41
48
  res = await db.execute(stm)
42
49
  project_list = res.scalars().all()
@@ -44,12 +51,12 @@ async def get_list_project(
44
51
  return project_list
45
52
 
46
53
 
47
- @router.post("/project/", response_model=ProjectReadV2, status_code=201)
54
+ @router.post("/project/", response_model=ProjectRead, status_code=201)
48
55
  async def create_project(
49
- project: ProjectCreateV2,
56
+ project: ProjectCreate,
50
57
  user: UserOAuth = Depends(current_user_act_ver_prof),
51
58
  db: AsyncSession = Depends(get_async_db),
52
- ) -> ProjectReadV2 | None:
59
+ ) -> ProjectRead | None:
53
60
  """
54
61
  Create new project
55
62
  """
@@ -67,41 +74,55 @@ async def create_project(
67
74
  )
68
75
 
69
76
  db_project = ProjectV2(**project.model_dump(), resource_id=resource_id)
70
- db_project.user_list.append(user)
71
-
72
77
  db.add(db_project)
78
+ await db.flush()
79
+
80
+ link = LinkUserProjectV2(
81
+ project_id=db_project.id,
82
+ user_id=user.id,
83
+ is_owner=True,
84
+ is_verified=True,
85
+ permissions=ProjectPermissions.EXECUTE,
86
+ )
87
+ db.add(link)
88
+
73
89
  await db.commit()
74
90
  await db.refresh(db_project)
75
- await db.close()
76
91
 
77
92
  return db_project
78
93
 
79
94
 
80
- @router.get("/project/{project_id}/", response_model=ProjectReadV2)
95
+ @router.get("/project/{project_id}/", response_model=ProjectRead)
81
96
  async def read_project(
82
97
  project_id: int,
83
98
  user: UserOAuth = Depends(current_user_act_ver_prof),
84
99
  db: AsyncSession = Depends(get_async_db),
85
- ) -> ProjectReadV2 | None:
100
+ ) -> ProjectRead | None:
86
101
  """
87
102
  Return info on an existing project
88
103
  """
89
- project = await _get_project_check_owner(
90
- project_id=project_id, user_id=user.id, db=db
104
+ project = await _get_project_check_access(
105
+ project_id=project_id,
106
+ user_id=user.id,
107
+ required_permissions=ProjectPermissions.READ,
108
+ db=db,
91
109
  )
92
110
  await db.close()
93
111
  return project
94
112
 
95
113
 
96
- @router.patch("/project/{project_id}/", response_model=ProjectReadV2)
114
+ @router.patch("/project/{project_id}/", response_model=ProjectRead)
97
115
  async def update_project(
98
116
  project_id: int,
99
- project_update: ProjectUpdateV2,
117
+ project_update: ProjectUpdate,
100
118
  user: UserOAuth = Depends(current_user_act_ver_prof),
101
119
  db: AsyncSession = Depends(get_async_db),
102
120
  ):
103
- project = await _get_project_check_owner(
104
- project_id=project_id, user_id=user.id, db=db
121
+ project = await _get_project_check_access(
122
+ project_id=project_id,
123
+ user_id=user.id,
124
+ required_permissions=ProjectPermissions.WRITE,
125
+ db=db,
105
126
  )
106
127
 
107
128
  # Check that there is no project with the same user and name
@@ -129,10 +150,18 @@ async def delete_project(
129
150
  Delete project
130
151
  """
131
152
 
132
- project = await _get_project_check_owner(
133
- project_id=project_id, user_id=user.id, db=db
153
+ project = await _get_project_check_access(
154
+ project_id=project_id,
155
+ user_id=user.id,
156
+ required_permissions=ProjectPermissions.EXECUTE,
157
+ db=db,
134
158
  )
135
- logger = set_logger(__name__)
159
+ link_user_project = await db.get(LinkUserProjectV2, (project_id, user.id))
160
+ if not link_user_project.is_owner:
161
+ raise HTTPException(
162
+ status_code=status.HTTP_403_FORBIDDEN,
163
+ detail="Only the owner can delete a Project.",
164
+ )
136
165
 
137
166
  # Fail if there exist jobs that are submitted and in relation with the
138
167
  # current project.
@@ -149,13 +178,12 @@ async def delete_project(
149
178
  ),
150
179
  )
151
180
 
152
- logger.info(f"Adding Project[{project.id}] to deletion.")
181
+ logger.debug(f"Add project {project.id} to deletion.")
153
182
  await db.delete(project)
154
183
 
155
- logger.info("Committing changes to db...")
184
+ logger.debug("Commit changes to db")
156
185
  await db.commit()
157
186
 
158
- logger.info("Everything has been deleted correctly.")
159
- reset_logger_handlers(logger)
187
+ logger.debug("Everything has been deleted correctly.")
160
188
 
161
189
  return Response(status_code=status.HTTP_204_NO_CONTENT)
@@ -0,0 +1,311 @@
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 pydantic import EmailStr
7
+ from sqlmodel import select
8
+
9
+ from fractal_server.app.db import AsyncSession
10
+ from fractal_server.app.db import get_async_db
11
+ from fractal_server.app.models import UserOAuth
12
+ from fractal_server.app.models.v2 import LinkUserProjectV2
13
+ from fractal_server.app.models.v2 import ProjectV2
14
+ from fractal_server.app.routes.auth import current_user_act_ver_prof
15
+ from fractal_server.app.schemas.v2 import ProjectAccessRead
16
+ from fractal_server.app.schemas.v2 import ProjectGuestCreate
17
+ from fractal_server.app.schemas.v2 import ProjectGuestRead
18
+ from fractal_server.app.schemas.v2 import ProjectGuestUpdate
19
+ from fractal_server.app.schemas.v2 import ProjectInvitationRead
20
+
21
+ from ._aux_functions_sharing import get_link_or_404
22
+ from ._aux_functions_sharing import get_pending_invitation_or_404
23
+ from ._aux_functions_sharing import get_user_id_from_email_or_404
24
+ from ._aux_functions_sharing import raise_403_if_not_owner
25
+ from ._aux_functions_sharing import raise_422_if_link_exists
26
+
27
+ router = APIRouter()
28
+
29
+
30
+ @router.get(
31
+ "/project/{project_id}/guest/",
32
+ response_model=list[ProjectGuestRead],
33
+ )
34
+ async def get_project_guests(
35
+ project_id: int,
36
+ owner: UserOAuth = Depends(current_user_act_ver_prof),
37
+ db: AsyncSession = Depends(get_async_db),
38
+ ) -> list[ProjectGuestRead]:
39
+ """
40
+ Get the list of all the guests of your project (verified or not).
41
+ """
42
+ await raise_403_if_not_owner(user_id=owner.id, project_id=project_id, db=db)
43
+ # Get (email, is_verified, permissions) for all guests
44
+ res = await db.execute(
45
+ select(
46
+ UserOAuth.email,
47
+ LinkUserProjectV2.is_verified,
48
+ LinkUserProjectV2.permissions,
49
+ )
50
+ .join(LinkUserProjectV2, LinkUserProjectV2.user_id == UserOAuth.id)
51
+ .where(LinkUserProjectV2.project_id == project_id)
52
+ .where(LinkUserProjectV2.is_owner.is_(False))
53
+ .order_by(UserOAuth.email)
54
+ )
55
+ guest_tuples = res.all()
56
+ return [
57
+ dict(
58
+ email=guest_email,
59
+ is_verified=is_verified,
60
+ permissions=permissions,
61
+ )
62
+ for guest_email, is_verified, permissions in guest_tuples
63
+ ]
64
+
65
+
66
+ @router.post("/project/{project_id}/guest/", status_code=201)
67
+ async def invite_guest(
68
+ project_id: int,
69
+ email: EmailStr,
70
+ project_invitation: ProjectGuestCreate,
71
+ owner: UserOAuth = Depends(current_user_act_ver_prof),
72
+ db: AsyncSession = Depends(get_async_db),
73
+ ) -> Response:
74
+ """
75
+ Add a guest to your project.
76
+ """
77
+ await raise_403_if_not_owner(user_id=owner.id, project_id=project_id, db=db)
78
+
79
+ guest_id = await get_user_id_from_email_or_404(user_email=email, db=db)
80
+
81
+ await raise_422_if_link_exists(
82
+ user_id=guest_id,
83
+ project_id=project_id,
84
+ db=db,
85
+ )
86
+
87
+ db.add(
88
+ LinkUserProjectV2(
89
+ project_id=project_id,
90
+ user_id=guest_id,
91
+ is_owner=False,
92
+ is_verified=False,
93
+ permissions=project_invitation.permissions,
94
+ )
95
+ )
96
+ await db.commit()
97
+
98
+ return Response(status_code=status.HTTP_201_CREATED)
99
+
100
+
101
+ @router.patch("/project/{project_id}/guest/", status_code=200)
102
+ async def patch_guest(
103
+ project_id: int,
104
+ email: EmailStr,
105
+ update: ProjectGuestUpdate,
106
+ owner: UserOAuth = Depends(current_user_act_ver_prof),
107
+ db: AsyncSession = Depends(get_async_db),
108
+ ) -> Response:
109
+ """
110
+ Change guest's permissions on your project.
111
+ """
112
+ await raise_403_if_not_owner(user_id=owner.id, project_id=project_id, db=db)
113
+
114
+ guest_id = await get_user_id_from_email_or_404(user_email=email, db=db)
115
+
116
+ if guest_id == owner.id:
117
+ raise HTTPException(
118
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
119
+ detail="Cannot perform this operation on project owner.",
120
+ )
121
+
122
+ link = await get_link_or_404(
123
+ user_id=guest_id,
124
+ project_id=project_id,
125
+ db=db,
126
+ )
127
+
128
+ # Update link and commit
129
+ for key, value in update.model_dump(exclude_unset=True).items():
130
+ setattr(link, key, value)
131
+ await db.commit()
132
+
133
+ return Response(status_code=status.HTTP_200_OK)
134
+
135
+
136
+ @router.delete("/project/{project_id}/guest/", status_code=204)
137
+ async def revoke_guest_access(
138
+ project_id: int,
139
+ email: EmailStr,
140
+ owner: UserOAuth = Depends(current_user_act_ver_prof),
141
+ db: AsyncSession = Depends(get_async_db),
142
+ ) -> Response:
143
+ """
144
+ Remove a guest from your project.
145
+ """
146
+ await raise_403_if_not_owner(user_id=owner.id, project_id=project_id, db=db)
147
+
148
+ guest_id = await get_user_id_from_email_or_404(user_email=email, db=db)
149
+
150
+ if guest_id == owner.id:
151
+ raise HTTPException(
152
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
153
+ detail="Cannot perform this operation on project owner.",
154
+ )
155
+
156
+ link = await get_link_or_404(
157
+ user_id=guest_id,
158
+ project_id=project_id,
159
+ db=db,
160
+ )
161
+
162
+ # Delete link and commit
163
+ await db.delete(link)
164
+ await db.commit()
165
+
166
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
167
+
168
+
169
+ @router.get(
170
+ "/project/invitation/",
171
+ response_model=list[ProjectInvitationRead],
172
+ )
173
+ async def get_pending_invitations(
174
+ user: UserOAuth = Depends(current_user_act_ver_prof),
175
+ db: AsyncSession = Depends(get_async_db),
176
+ ) -> list[ProjectInvitationRead]:
177
+ """
178
+ See your current invitations.
179
+ """
180
+
181
+ res = await db.execute(
182
+ select(
183
+ ProjectV2.id,
184
+ ProjectV2.name,
185
+ LinkUserProjectV2.permissions,
186
+ (
187
+ select(UserOAuth.email)
188
+ .join(
189
+ LinkUserProjectV2,
190
+ UserOAuth.id == LinkUserProjectV2.user_id,
191
+ )
192
+ .where(LinkUserProjectV2.is_owner.is_(True))
193
+ .where(LinkUserProjectV2.project_id == ProjectV2.id)
194
+ .scalar_subquery()
195
+ .correlate(ProjectV2)
196
+ ),
197
+ )
198
+ .join(LinkUserProjectV2, LinkUserProjectV2.project_id == ProjectV2.id)
199
+ .where(LinkUserProjectV2.user_id == user.id)
200
+ .where(LinkUserProjectV2.is_verified.is_(False))
201
+ .order_by(ProjectV2.name)
202
+ )
203
+
204
+ guest_project_info = res.all()
205
+
206
+ return [
207
+ dict(
208
+ project_id=project_id,
209
+ project_name=project_name,
210
+ guest_permissions=guest_permissions,
211
+ owner_email=owner_email,
212
+ )
213
+ for (
214
+ project_id,
215
+ project_name,
216
+ guest_permissions,
217
+ owner_email,
218
+ ) in guest_project_info
219
+ ]
220
+
221
+
222
+ @router.get(
223
+ "/project/{project_id}/access/",
224
+ response_model=ProjectAccessRead,
225
+ )
226
+ async def get_access_info(
227
+ project_id: int,
228
+ user: UserOAuth = Depends(current_user_act_ver_prof),
229
+ db: AsyncSession = Depends(get_async_db),
230
+ ) -> ProjectAccessRead:
231
+ """
232
+ Returns information on your relationship with Project[`project_id`].
233
+ """
234
+
235
+ res = await db.execute(
236
+ select(
237
+ LinkUserProjectV2.is_owner,
238
+ LinkUserProjectV2.permissions,
239
+ (
240
+ select(UserOAuth.email)
241
+ .join(
242
+ LinkUserProjectV2,
243
+ UserOAuth.id == LinkUserProjectV2.user_id,
244
+ )
245
+ .where(LinkUserProjectV2.is_owner.is_(True))
246
+ .where(LinkUserProjectV2.project_id == project_id)
247
+ .scalar_subquery()
248
+ ),
249
+ )
250
+ .where(LinkUserProjectV2.project_id == project_id)
251
+ .where(LinkUserProjectV2.user_id == user.id)
252
+ .where(LinkUserProjectV2.is_verified.is_(True))
253
+ )
254
+
255
+ guest_project_info = res.one_or_none()
256
+
257
+ if guest_project_info is None:
258
+ raise HTTPException(
259
+ status_code=status.HTTP_404_NOT_FOUND,
260
+ detail=f"User has no access to project {project_id}.",
261
+ )
262
+
263
+ is_owner, permissions, owner_email = guest_project_info
264
+
265
+ return dict(
266
+ is_owner=is_owner,
267
+ permissions=permissions,
268
+ owner_email=owner_email,
269
+ )
270
+
271
+
272
+ @router.post("/project/{project_id}/access/accept/", status_code=200)
273
+ async def accept_project_invitation(
274
+ project_id: int,
275
+ user: UserOAuth = Depends(current_user_act_ver_prof),
276
+ db: AsyncSession = Depends(get_async_db),
277
+ ) -> Response:
278
+ """
279
+ Accept invitation to project `project_id`.
280
+ """
281
+ link = await get_pending_invitation_or_404(
282
+ user_id=user.id, project_id=project_id, db=db
283
+ )
284
+ link.is_verified = True
285
+ await db.commit()
286
+
287
+ return Response(status_code=status.HTTP_200_OK)
288
+
289
+
290
+ @router.delete("/project/{project_id}/access/", status_code=204)
291
+ async def leave_project(
292
+ project_id: int,
293
+ user: UserOAuth = Depends(current_user_act_ver_prof),
294
+ db: AsyncSession = Depends(get_async_db),
295
+ ) -> Response:
296
+ """
297
+ Decline invitation to project `project_id` or stop being a guest of that
298
+ project.
299
+ """
300
+ link = await get_link_or_404(user_id=user.id, project_id=project_id, db=db)
301
+
302
+ if link.is_owner:
303
+ raise HTTPException(
304
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
305
+ detail=f"You are the owner of project {project_id}.",
306
+ )
307
+
308
+ await db.delete(link)
309
+ await db.commit()
310
+
311
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
@@ -1,19 +1,19 @@
1
1
  from fastapi import APIRouter
2
2
  from fastapi import Depends
3
- from fastapi import HTTPException
4
- from fastapi import status
5
-
6
- from .....logger import set_logger
7
- from ....db import AsyncSession
8
- from ....db import get_async_db
9
- from ....models.v2 import JobV2
10
- from ....schemas.v2.status_legacy import LegacyStatusReadV2
11
- from ....schemas.v2.status_legacy import WorkflowTaskStatusTypeV2
12
- from ._aux_functions import _get_dataset_check_owner
13
- from ._aux_functions import _get_submitted_jobs_statement
14
- from ._aux_functions import _get_workflow_check_owner
3
+
4
+ from fractal_server.app.db import AsyncSession
5
+ from fractal_server.app.db import get_async_db
15
6
  from fractal_server.app.models import UserOAuth
7
+ from fractal_server.app.models.v2 import JobV2
16
8
  from fractal_server.app.routes.auth import current_user_act_ver_prof
9
+ from fractal_server.app.schemas.v2.sharing import ProjectPermissions
10
+ from fractal_server.app.schemas.v2.status_legacy import LegacyStatusRead
11
+ from fractal_server.app.schemas.v2.status_legacy import WorkflowTaskStatusType
12
+ from fractal_server.logger import set_logger
13
+
14
+ from ._aux_functions import _get_dataset_check_access
15
+ from ._aux_functions import _get_submitted_jobs_statement
16
+ from ._aux_functions import _get_workflow_check_access
17
17
 
18
18
  router = APIRouter()
19
19
 
@@ -22,7 +22,7 @@ logger = set_logger(__name__)
22
22
 
23
23
  @router.get(
24
24
  "/project/{project_id}/status-legacy/",
25
- response_model=LegacyStatusReadV2,
25
+ response_model=LegacyStatusRead,
26
26
  )
27
27
  async def get_workflowtask_status(
28
28
  project_id: int,
@@ -30,7 +30,7 @@ async def get_workflowtask_status(
30
30
  workflow_id: int,
31
31
  user: UserOAuth = Depends(current_user_act_ver_prof),
32
32
  db: AsyncSession = Depends(get_async_db),
33
- ) -> LegacyStatusReadV2 | None:
33
+ ) -> LegacyStatusRead | None:
34
34
  """
35
35
  Extract the status of all `WorkflowTaskV2` of a given `WorkflowV2` that ran
36
36
  on a given `DatasetV2`.
@@ -41,43 +41,33 @@ async def get_workflowtask_status(
41
41
  order). See fractal-server GitHub issues: 793, 1083.
42
42
  """
43
43
  # Get the dataset DB entry
44
- output = await _get_dataset_check_owner(
44
+ output = await _get_dataset_check_access(
45
45
  project_id=project_id,
46
46
  dataset_id=dataset_id,
47
47
  user_id=user.id,
48
+ required_permissions=ProjectPermissions.READ,
48
49
  db=db,
49
50
  )
50
51
  dataset = output["dataset"]
51
52
 
52
53
  # Get the workflow DB entry
53
- workflow = await _get_workflow_check_owner(
54
+ workflow = await _get_workflow_check_access(
54
55
  project_id=project_id,
55
56
  workflow_id=workflow_id,
56
57
  user_id=user.id,
58
+ required_permissions=ProjectPermissions.READ,
57
59
  db=db,
58
60
  )
59
61
 
60
62
  # Check whether there exists a submitted job associated to this
61
63
  # workflow/dataset pair. If it does exist, it will be used later.
62
64
  # If there are multiple jobs, raise an error.
63
- stm = _get_submitted_jobs_statement()
64
- stm = stm.where(JobV2.dataset_id == dataset_id)
65
- stm = stm.where(JobV2.workflow_id == workflow_id)
66
- res = await db.execute(stm)
67
- running_jobs = res.scalars().all()
68
- if len(running_jobs) == 0:
69
- running_job = None
70
- elif len(running_jobs) == 1:
71
- running_job = running_jobs[0]
72
- else:
73
- string_ids = str([job.id for job in running_jobs])[1:-1]
74
- raise HTTPException(
75
- status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
76
- detail=(
77
- f"Cannot get WorkflowTaskV2 statuses as DatasetV2 {dataset.id}"
78
- f" is linked to multiple active jobs: {string_ids}."
79
- ),
80
- )
65
+ res = await db.execute(
66
+ _get_submitted_jobs_statement()
67
+ .where(JobV2.dataset_id == dataset_id)
68
+ .where(JobV2.workflow_id == workflow_id)
69
+ )
70
+ running_job = res.scalars().one_or_none()
81
71
 
82
72
  # Initialize empty dictionary for WorkflowTaskV2 status
83
73
  workflow_tasks_status_dict: dict = {}
@@ -112,19 +102,19 @@ async def get_workflowtask_status(
112
102
  ]
113
103
  try:
114
104
  first_submitted_index = running_job_statuses.index(
115
- WorkflowTaskStatusTypeV2.SUBMITTED
105
+ WorkflowTaskStatusType.SUBMITTED
116
106
  )
117
107
  except ValueError:
118
108
  logger.warning(
119
109
  f"Job {running_job.id} is submitted but its task list does not"
120
- f" contain a {WorkflowTaskStatusTypeV2.SUBMITTED} task."
110
+ f" contain a {WorkflowTaskStatusType.SUBMITTED} task."
121
111
  )
122
112
  first_submitted_index = 0
123
113
 
124
114
  for wftask in running_job_wftasks[first_submitted_index:]:
125
- workflow_tasks_status_dict[
126
- wftask.id
127
- ] = WorkflowTaskStatusTypeV2.SUBMITTED
115
+ workflow_tasks_status_dict[wftask.id] = (
116
+ WorkflowTaskStatusType.SUBMITTED
117
+ )
128
118
 
129
119
  # The last workflow task that is included in the submitted job is also
130
120
  # the positional-last workflow task to be included in the response.
@@ -153,7 +143,7 @@ async def get_workflowtask_status(
153
143
  # If a wftask ID was not found, ignore it and continue
154
144
  continue
155
145
  clean_workflow_tasks_status_dict[str(wf_task.id)] = wf_task_status
156
- if wf_task_status == WorkflowTaskStatusTypeV2.FAILED:
146
+ if wf_task_status == WorkflowTaskStatusType.FAILED:
157
147
  # Starting from the beginning of `workflow.task_list`, stop the
158
148
  # first time that you hit a failed job
159
149
  break
@@ -162,5 +152,5 @@ async def get_workflowtask_status(
162
152
  # first time that you hit `last_valid_wftask_id``
163
153
  break
164
154
 
165
- response_body = LegacyStatusReadV2(status=clean_workflow_tasks_status_dict)
155
+ response_body = LegacyStatusRead(status=clean_workflow_tasks_status_dict)
166
156
  return response_body