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
@@ -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
 
@@ -99,8 +99,7 @@ async def get_package_version_from_pypi(
99
99
  raise HTTPException(
100
100
  status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
101
101
  detail=(
102
- f"Could not get {url} (status_code {res.status_code})."
103
- f"\n{hint}"
102
+ f"Could not get {url} (status_code {res.status_code}).\n{hint}"
104
103
  ),
105
104
  )
106
105
  try:
@@ -144,8 +143,7 @@ async def get_package_version_from_pypi(
144
143
  raise HTTPException(
145
144
  status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
146
145
  detail=(
147
- f"No version starting with {version} found.\n"
148
- f"{hint}"
146
+ f"No version starting with {version} found.\n{hint}"
149
147
  ),
150
148
  )
151
149
  else:
@@ -173,7 +171,7 @@ async def check_no_ongoing_activity(
173
171
  stm = (
174
172
  select(TaskGroupActivityV2)
175
173
  .where(TaskGroupActivityV2.taskgroupv2_id == task_group_id)
176
- .where(TaskGroupActivityV2.status == TaskGroupActivityStatusV2.ONGOING)
174
+ .where(TaskGroupActivityV2.status == TaskGroupActivityStatus.ONGOING)
177
175
  )
178
176
  res = await db.execute(stm)
179
177
  ongoing_activities = res.scalars().all()
@@ -215,7 +213,7 @@ async def check_no_submitted_job(
215
213
  .join(TaskV2, WorkflowTaskV2.task_id == TaskV2.id)
216
214
  .where(WorkflowTaskV2.order >= JobV2.first_task_index)
217
215
  .where(WorkflowTaskV2.order <= JobV2.last_task_index)
218
- .where(JobV2.status == JobStatusTypeV2.SUBMITTED)
216
+ .where(JobV2.status == JobStatusType.SUBMITTED)
219
217
  .where(TaskV2.taskgroupv2_id == task_group_id)
220
218
  )
221
219
  res = await db.execute(stm)
@@ -2,6 +2,7 @@
2
2
  Auxiliary functions to get task and task-group object from the database or
3
3
  perform simple checks
4
4
  """
5
+
5
6
  from typing import Any
6
7
 
7
8
  from fastapi import HTTPException
@@ -26,7 +27,7 @@ from fractal_server.app.routes.auth._aux_auth import (
26
27
  from fractal_server.app.routes.auth._aux_auth import (
27
28
  _verify_user_belongs_to_group,
28
29
  )
29
- from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
30
+ from fractal_server.app.schemas.v2 import TaskGroupActivityAction
30
31
  from fractal_server.images.tools import merge_type_filters
31
32
  from fractal_server.logger import set_logger
32
33
 
@@ -74,8 +75,7 @@ async def _get_task_group_read_access(
74
75
  forbidden_exception = HTTPException(
75
76
  status_code=status.HTTP_403_FORBIDDEN,
76
77
  detail=(
77
- "Current user has no read access to TaskGroupV2 "
78
- f"{task_group_id}."
78
+ f"Current user has no read access to TaskGroupV2 {task_group_id}."
79
79
  ),
80
80
  )
81
81
 
@@ -86,13 +86,11 @@ async def _get_task_group_read_access(
86
86
  else:
87
87
  stm = (
88
88
  select(LinkUserGroup)
89
- .join(UserOAuth)
90
- .join(Profile)
89
+ .join(UserOAuth, UserOAuth.id == LinkUserGroup.user_id)
90
+ .join(Profile, Profile.id == UserOAuth.profile_id)
91
91
  .where(LinkUserGroup.group_id == task_group.user_group_id)
92
- .where(LinkUserGroup.user_id == user_id)
93
92
  .where(UserOAuth.id == user_id)
94
- .where(Profile.id == UserOAuth.profile_id)
95
- .where(task_group.resource_id == Profile.resource_id)
93
+ .where(Profile.resource_id == task_group.resource_id)
96
94
  )
97
95
  res = await db.execute(stm)
98
96
  link = res.unique().scalars().one_or_none()
@@ -254,7 +252,7 @@ async def _get_collection_task_group_activity_status_message(
254
252
  res = await db.execute(
255
253
  select(TaskGroupActivityV2)
256
254
  .where(TaskGroupActivityV2.taskgroupv2_id == task_group_id)
257
- .where(TaskGroupActivityV2.action == TaskGroupActivityActionV2.COLLECT)
255
+ .where(TaskGroupActivityV2.action == TaskGroupActivityAction.COLLECT)
258
256
  )
259
257
  task_group_activity_list = res.scalars().all()
260
258
  if len(task_group_activity_list) > 1:
@@ -10,7 +10,6 @@ from fractal_server.exceptions import UnreachableBranchError
10
10
  from fractal_server.logger import set_logger
11
11
  from fractal_server.syringe import Inject
12
12
 
13
-
14
13
  logger = set_logger(__name__)
15
14
 
16
15
 
@@ -84,7 +83,7 @@ async def _disambiguate_task_groups(
84
83
  res = await db.execute(stm)
85
84
  oldest_user_group_id = res.scalars().first()
86
85
  logger.debug(
87
- "[_disambiguate_task_groups] " f"Result: {oldest_user_group_id=}."
86
+ f"[_disambiguate_task_groups] Result: {oldest_user_group_id=}."
88
87
  )
89
88
  task_group = next(
90
89
  iter(
@@ -1,3 +1,6 @@
1
+ import os
2
+ from pathlib import Path
3
+
1
4
  from fastapi import APIRouter
2
5
  from fastapi import Depends
3
6
  from fastapi import HTTPException
@@ -5,89 +8,106 @@ from fastapi import Response
5
8
  from fastapi import status
6
9
  from sqlmodel import select
7
10
 
8
- from ....db import AsyncSession
9
- from ....db import get_async_db
10
- from ....models.v2 import DatasetV2
11
- from ....models.v2 import JobV2
12
- from ....models.v2 import ProjectV2
13
- from ....schemas.v2 import DatasetCreateV2
14
- from ....schemas.v2 import DatasetReadV2
15
- from ....schemas.v2 import DatasetUpdateV2
16
- from ....schemas.v2.dataset import DatasetExportV2
17
- from ....schemas.v2.dataset import DatasetImportV2
18
- from ._aux_functions import _get_dataset_check_owner
19
- from ._aux_functions import _get_project_check_owner
20
- from ._aux_functions import _get_submitted_jobs_statement
11
+ from fractal_server.app.db import AsyncSession
12
+ from fractal_server.app.db import get_async_db
21
13
  from fractal_server.app.models import UserOAuth
14
+ from fractal_server.app.models.v2 import DatasetV2
15
+ from fractal_server.app.models.v2 import JobV2
22
16
  from fractal_server.app.routes.auth import current_user_act_ver_prof
17
+ from fractal_server.app.schemas.v2 import DatasetCreate
18
+ from fractal_server.app.schemas.v2 import DatasetRead
19
+ from fractal_server.app.schemas.v2 import DatasetUpdate
20
+ from fractal_server.app.schemas.v2.dataset import DatasetExport
21
+ from fractal_server.app.schemas.v2.dataset import DatasetImport
22
+ from fractal_server.app.schemas.v2.sharing import ProjectPermissions
23
23
  from fractal_server.string_tools import sanitize_string
24
24
  from fractal_server.urls import normalize_url
25
25
 
26
+ from ._aux_functions import _get_dataset_check_access
27
+ from ._aux_functions import _get_project_check_access
28
+ from ._aux_functions import _get_submitted_jobs_statement
29
+
26
30
  router = APIRouter()
27
31
 
28
32
 
29
33
  @router.post(
30
34
  "/project/{project_id}/dataset/",
31
- response_model=DatasetReadV2,
35
+ response_model=DatasetRead,
32
36
  status_code=status.HTTP_201_CREATED,
33
37
  )
34
38
  async def create_dataset(
35
39
  project_id: int,
36
- dataset: DatasetCreateV2,
40
+ dataset: DatasetCreate,
37
41
  user: UserOAuth = Depends(current_user_act_ver_prof),
38
42
  db: AsyncSession = Depends(get_async_db),
39
- ) -> DatasetReadV2 | None:
43
+ ) -> DatasetRead | None:
40
44
  """
41
45
  Add new dataset to current project
42
46
  """
43
- project = await _get_project_check_owner(
44
- project_id=project_id, user_id=user.id, db=db
47
+ project = await _get_project_check_access(
48
+ project_id=project_id,
49
+ user_id=user.id,
50
+ required_permissions=ProjectPermissions.WRITE,
51
+ db=db,
45
52
  )
46
53
 
47
- if dataset.zarr_dir is None:
48
- db_dataset = DatasetV2(
49
- project_id=project_id,
50
- zarr_dir="__PLACEHOLDER__",
51
- **dataset.model_dump(exclude={"zarr_dir"}),
52
- )
53
- db.add(db_dataset)
54
- await db.commit()
55
- await db.refresh(db_dataset)
56
- path = (
57
- f"{user.project_dir}/fractal/"
58
- f"{project_id}_{sanitize_string(project.name)}/"
54
+ db_dataset = DatasetV2(
55
+ project_id=project_id,
56
+ zarr_dir="__PLACEHOLDER__",
57
+ **dataset.model_dump(exclude={"project_dir", "zarr_subfolder"}),
58
+ )
59
+ db.add(db_dataset)
60
+ await db.commit()
61
+ await db.refresh(db_dataset)
62
+
63
+ if dataset.project_dir is None:
64
+ project_dir = user.project_dirs[0]
65
+ else:
66
+ if dataset.project_dir not in user.project_dirs:
67
+ await db.delete(db_dataset)
68
+ await db.commit()
69
+ raise HTTPException(
70
+ status_code=status.HTTP_403_FORBIDDEN,
71
+ detail=f"You are not allowed to use {dataset.project_dir=}.",
72
+ )
73
+ project_dir = dataset.project_dir
74
+
75
+ if dataset.zarr_subfolder is None:
76
+ zarr_subfolder = (
77
+ f"fractal/{project_id}_{sanitize_string(project.name)}/"
59
78
  f"{db_dataset.id}_{sanitize_string(db_dataset.name)}"
60
79
  )
61
- normalized_path = normalize_url(path)
62
- db_dataset.zarr_dir = normalized_path
63
-
64
- db.add(db_dataset)
65
- await db.commit()
66
- await db.refresh(db_dataset)
67
80
  else:
68
- db_dataset = DatasetV2(project_id=project_id, **dataset.model_dump())
69
- db.add(db_dataset)
70
- await db.commit()
71
- await db.refresh(db_dataset)
81
+ zarr_subfolder = dataset.zarr_subfolder
82
+
83
+ zarr_dir = os.path.join(project_dir, zarr_subfolder)
84
+ db_dataset.zarr_dir = normalize_url(zarr_dir)
85
+
86
+ db.add(db_dataset)
87
+ await db.commit()
88
+ await db.refresh(db_dataset)
72
89
 
73
90
  return db_dataset
74
91
 
75
92
 
76
93
  @router.get(
77
94
  "/project/{project_id}/dataset/",
78
- response_model=list[DatasetReadV2],
95
+ response_model=list[DatasetRead],
79
96
  )
80
97
  async def read_dataset_list(
81
98
  project_id: int,
82
99
  user: UserOAuth = Depends(current_user_act_ver_prof),
83
100
  db: AsyncSession = Depends(get_async_db),
84
- ) -> list[DatasetReadV2] | None:
101
+ ) -> list[DatasetRead] | None:
85
102
  """
86
103
  Get dataset list for given project
87
104
  """
88
105
  # Access control
89
- project = await _get_project_check_owner(
90
- project_id=project_id, user_id=user.id, db=db
106
+ project = await _get_project_check_access(
107
+ project_id=project_id,
108
+ user_id=user.id,
109
+ required_permissions=ProjectPermissions.READ,
110
+ db=db,
91
111
  )
92
112
  # Find datasets of the current project. Note: this select/where approach
93
113
  # has much better scaling than refreshing all elements of
@@ -96,72 +116,62 @@ async def read_dataset_list(
96
116
  stm = select(DatasetV2).where(DatasetV2.project_id == project.id)
97
117
  res = await db.execute(stm)
98
118
  dataset_list = res.scalars().all()
99
- await db.close()
100
119
  return dataset_list
101
120
 
102
121
 
103
122
  @router.get(
104
123
  "/project/{project_id}/dataset/{dataset_id}/",
105
- response_model=DatasetReadV2,
124
+ response_model=DatasetRead,
106
125
  )
107
126
  async def read_dataset(
108
127
  project_id: int,
109
128
  dataset_id: int,
110
129
  user: UserOAuth = Depends(current_user_act_ver_prof),
111
130
  db: AsyncSession = Depends(get_async_db),
112
- ) -> DatasetReadV2 | None:
131
+ ) -> DatasetRead | None:
113
132
  """
114
133
  Get info on a dataset associated to the current project
115
134
  """
116
- output = await _get_dataset_check_owner(
135
+ output = await _get_dataset_check_access(
117
136
  project_id=project_id,
118
137
  dataset_id=dataset_id,
119
138
  user_id=user.id,
139
+ required_permissions=ProjectPermissions.READ,
120
140
  db=db,
121
141
  )
122
142
  dataset = output["dataset"]
123
- await db.close()
124
143
  return dataset
125
144
 
126
145
 
127
146
  @router.patch(
128
147
  "/project/{project_id}/dataset/{dataset_id}/",
129
- response_model=DatasetReadV2,
148
+ response_model=DatasetRead,
130
149
  )
131
150
  async def update_dataset(
132
151
  project_id: int,
133
152
  dataset_id: int,
134
- dataset_update: DatasetUpdateV2,
153
+ dataset_update: DatasetUpdate,
135
154
  user: UserOAuth = Depends(current_user_act_ver_prof),
136
155
  db: AsyncSession = Depends(get_async_db),
137
- ) -> DatasetReadV2 | None:
156
+ ) -> DatasetRead | None:
138
157
  """
139
158
  Edit a dataset associated to the current project
140
159
  """
141
160
 
142
- output = await _get_dataset_check_owner(
161
+ output = await _get_dataset_check_access(
143
162
  project_id=project_id,
144
163
  dataset_id=dataset_id,
145
164
  user_id=user.id,
165
+ required_permissions=ProjectPermissions.WRITE,
146
166
  db=db,
147
167
  )
148
168
  db_dataset = output["dataset"]
149
169
 
150
- if (dataset_update.zarr_dir is not None) and (len(db_dataset.images) != 0):
151
- raise HTTPException(
152
- status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
153
- detail=(
154
- "Cannot modify `zarr_dir` because the dataset has a non-empty "
155
- "image list."
156
- ),
157
- )
158
-
159
170
  for key, value in dataset_update.model_dump(exclude_unset=True).items():
160
171
  setattr(db_dataset, key, value)
161
172
 
162
173
  await db.commit()
163
174
  await db.refresh(db_dataset)
164
- await db.close()
165
175
  return db_dataset
166
176
 
167
177
 
@@ -178,10 +188,11 @@ async def delete_dataset(
178
188
  """
179
189
  Delete a dataset associated to the current project
180
190
  """
181
- output = await _get_dataset_check_owner(
191
+ output = await _get_dataset_check_access(
182
192
  project_id=project_id,
183
193
  dataset_id=dataset_id,
184
194
  user_id=user.id,
195
+ required_permissions=ProjectPermissions.WRITE,
185
196
  db=db,
186
197
  )
187
198
  dataset = output["dataset"]
@@ -208,46 +219,26 @@ async def delete_dataset(
208
219
  return Response(status_code=status.HTTP_204_NO_CONTENT)
209
220
 
210
221
 
211
- @router.get("/dataset/", response_model=list[DatasetReadV2])
212
- async def get_user_datasets(
213
- user: UserOAuth = Depends(current_user_act_ver_prof),
214
- db: AsyncSession = Depends(get_async_db),
215
- ) -> list[DatasetReadV2]:
216
- """
217
- Returns all the datasets of the current user
218
- """
219
- stm = select(DatasetV2)
220
- stm = stm.join(ProjectV2).where(
221
- ProjectV2.user_list.any(UserOAuth.id == user.id)
222
- )
223
-
224
- res = await db.execute(stm)
225
- dataset_list = res.scalars().all()
226
- await db.close()
227
- return dataset_list
228
-
229
-
230
222
  @router.get(
231
223
  "/project/{project_id}/dataset/{dataset_id}/export/",
232
- response_model=DatasetExportV2,
224
+ response_model=DatasetExport,
233
225
  )
234
226
  async def export_dataset(
235
227
  project_id: int,
236
228
  dataset_id: int,
237
229
  user: UserOAuth = Depends(current_user_act_ver_prof),
238
230
  db: AsyncSession = Depends(get_async_db),
239
- ) -> DatasetExportV2 | None:
231
+ ) -> DatasetExport | None:
240
232
  """
241
233
  Export an existing dataset
242
234
  """
243
- dict_dataset_project = await _get_dataset_check_owner(
235
+ dict_dataset_project = await _get_dataset_check_access(
244
236
  project_id=project_id,
245
237
  dataset_id=dataset_id,
246
238
  user_id=user.id,
239
+ required_permissions=ProjectPermissions.READ,
247
240
  db=db,
248
241
  )
249
- await db.close()
250
-
251
242
  dataset = dict_dataset_project["dataset"]
252
243
 
253
244
  return dataset
@@ -255,35 +246,38 @@ async def export_dataset(
255
246
 
256
247
  @router.post(
257
248
  "/project/{project_id}/dataset/import/",
258
- response_model=DatasetReadV2,
249
+ response_model=DatasetRead,
259
250
  status_code=status.HTTP_201_CREATED,
260
251
  )
261
252
  async def import_dataset(
262
253
  project_id: int,
263
- dataset: DatasetImportV2,
254
+ dataset: DatasetImport,
264
255
  user: UserOAuth = Depends(current_user_act_ver_prof),
265
256
  db: AsyncSession = Depends(get_async_db),
266
- ) -> DatasetReadV2 | None:
257
+ ) -> DatasetRead | None:
267
258
  """
268
259
  Import an existing dataset into a project
269
260
  """
270
261
 
271
262
  # Preliminary checks
272
- await _get_project_check_owner(
263
+ await _get_project_check_access(
273
264
  project_id=project_id,
274
265
  user_id=user.id,
266
+ required_permissions=ProjectPermissions.WRITE,
275
267
  db=db,
276
268
  )
277
269
 
278
- for image in dataset.images:
279
- if not image.zarr_url.startswith(dataset.zarr_dir):
280
- raise HTTPException(
281
- status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
282
- detail=(
283
- f"Cannot import dataset: zarr_url {image.zarr_url} is not "
284
- f"relative to zarr_dir={dataset.zarr_dir}."
285
- ),
286
- )
270
+ if not any(
271
+ Path(dataset.zarr_dir).is_relative_to(project_dir)
272
+ for project_dir in user.project_dirs
273
+ ):
274
+ raise HTTPException(
275
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
276
+ detail=(
277
+ f"{dataset.zarr_dir=} is not relative to any of user's project "
278
+ "dirs."
279
+ ),
280
+ )
287
281
 
288
282
  # Create new Dataset
289
283
  db_dataset = DatasetV2(
@@ -293,6 +287,5 @@ async def import_dataset(
293
287
  db.add(db_dataset)
294
288
  await db.commit()
295
289
  await db.refresh(db_dataset)
296
- await db.close()
297
290
 
298
291
  return db_dataset