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.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +2 -1
- fractal_server/app/models/linkuserproject.py +40 -0
- fractal_server/app/models/security.py +7 -5
- fractal_server/app/models/v2/job.py +13 -2
- fractal_server/app/models/v2/resource.py +13 -0
- fractal_server/app/routes/admin/v2/__init__.py +11 -11
- fractal_server/app/routes/admin/v2/accounting.py +2 -2
- fractal_server/app/routes/admin/v2/job.py +34 -23
- fractal_server/app/routes/admin/v2/sharing.py +103 -0
- fractal_server/app/routes/admin/v2/task.py +9 -8
- fractal_server/app/routes/admin/v2/task_group.py +94 -16
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +20 -20
- fractal_server/app/routes/api/__init__.py +0 -9
- fractal_server/app/routes/api/v2/__init__.py +47 -47
- fractal_server/app/routes/api/v2/_aux_functions.py +65 -64
- fractal_server/app/routes/api/v2/_aux_functions_history.py +8 -3
- fractal_server/app/routes/api/v2/_aux_functions_sharing.py +97 -0
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +4 -4
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +2 -2
- fractal_server/app/routes/api/v2/dataset.py +89 -77
- fractal_server/app/routes/api/v2/history.py +28 -16
- fractal_server/app/routes/api/v2/images.py +22 -8
- fractal_server/app/routes/api/v2/job.py +40 -24
- fractal_server/app/routes/api/v2/pre_submission_checks.py +13 -6
- fractal_server/app/routes/api/v2/project.py +48 -25
- fractal_server/app/routes/api/v2/sharing.py +311 -0
- fractal_server/app/routes/api/v2/status_legacy.py +22 -33
- fractal_server/app/routes/api/v2/submit.py +76 -71
- fractal_server/app/routes/api/v2/task.py +15 -17
- fractal_server/app/routes/api/v2/task_collection.py +18 -18
- fractal_server/app/routes/api/v2/task_collection_custom.py +11 -13
- fractal_server/app/routes/api/v2/task_collection_pixi.py +9 -9
- fractal_server/app/routes/api/v2/task_group.py +18 -18
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +26 -26
- fractal_server/app/routes/api/v2/task_version_update.py +12 -9
- fractal_server/app/routes/api/v2/workflow.py +41 -29
- fractal_server/app/routes/api/v2/workflow_import.py +25 -23
- fractal_server/app/routes/api/v2/workflowtask.py +25 -17
- fractal_server/app/routes/auth/_aux_auth.py +100 -0
- fractal_server/app/routes/auth/current_user.py +0 -63
- fractal_server/app/routes/auth/group.py +1 -30
- fractal_server/app/routes/auth/router.py +2 -0
- fractal_server/app/routes/auth/users.py +9 -0
- fractal_server/app/routes/auth/viewer_paths.py +43 -0
- fractal_server/app/schemas/user.py +29 -12
- fractal_server/app/schemas/user_group.py +0 -15
- fractal_server/app/schemas/v2/__init__.py +55 -48
- fractal_server/app/schemas/v2/dataset.py +35 -13
- fractal_server/app/schemas/v2/dumps.py +9 -9
- fractal_server/app/schemas/v2/job.py +11 -11
- fractal_server/app/schemas/v2/project.py +3 -3
- fractal_server/app/schemas/v2/resource.py +13 -4
- fractal_server/app/schemas/v2/sharing.py +99 -0
- fractal_server/app/schemas/v2/status_legacy.py +3 -3
- fractal_server/app/schemas/v2/task.py +6 -6
- fractal_server/app/schemas/v2/task_collection.py +4 -4
- fractal_server/app/schemas/v2/task_group.py +16 -16
- fractal_server/app/schemas/v2/workflow.py +16 -16
- fractal_server/app/schemas/v2/workflowtask.py +14 -14
- fractal_server/app/security/__init__.py +1 -1
- fractal_server/app/shutdown.py +6 -6
- fractal_server/config/__init__.py +0 -6
- fractal_server/config/_data.py +0 -79
- fractal_server/config/_main.py +6 -1
- fractal_server/data_migrations/2_18_0.py +30 -0
- fractal_server/images/models.py +1 -2
- fractal_server/main.py +72 -11
- fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py +60 -0
- fractal_server/migrations/versions/88270f589c9b_add_prevent_new_submissions.py +39 -0
- fractal_server/migrations/versions/bc0e8b3327a7_project_sharing.py +72 -0
- fractal_server/migrations/versions/f0702066b007_one_submitted_job_per_dataset.py +40 -0
- fractal_server/runner/config/_slurm.py +2 -0
- fractal_server/runner/executors/slurm_common/_batching.py +4 -10
- fractal_server/runner/executors/slurm_common/slurm_config.py +1 -0
- fractal_server/runner/executors/slurm_ssh/runner.py +1 -1
- fractal_server/runner/executors/slurm_sudo/runner.py +1 -1
- fractal_server/runner/v2/_local.py +4 -3
- fractal_server/runner/v2/_slurm_ssh.py +4 -3
- fractal_server/runner/v2/_slurm_sudo.py +4 -3
- fractal_server/runner/v2/runner.py +36 -17
- fractal_server/runner/v2/runner_functions.py +11 -14
- fractal_server/runner/v2/submit_workflow.py +22 -9
- fractal_server/tasks/v2/local/_utils.py +2 -2
- fractal_server/tasks/v2/local/collect.py +5 -6
- fractal_server/tasks/v2/local/collect_pixi.py +5 -6
- fractal_server/tasks/v2/local/deactivate.py +7 -7
- fractal_server/tasks/v2/local/deactivate_pixi.py +3 -3
- fractal_server/tasks/v2/local/delete.py +5 -5
- fractal_server/tasks/v2/local/reactivate.py +5 -5
- fractal_server/tasks/v2/local/reactivate_pixi.py +5 -5
- fractal_server/tasks/v2/ssh/collect.py +5 -5
- fractal_server/tasks/v2/ssh/collect_pixi.py +5 -5
- fractal_server/tasks/v2/ssh/deactivate.py +7 -7
- fractal_server/tasks/v2/ssh/deactivate_pixi.py +2 -2
- fractal_server/tasks/v2/ssh/delete.py +5 -5
- fractal_server/tasks/v2/ssh/reactivate.py +5 -5
- fractal_server/tasks/v2/ssh/reactivate_pixi.py +5 -5
- fractal_server/tasks/v2/utils_background.py +7 -7
- fractal_server/tasks/v2/utils_database.py +5 -5
- fractal_server/types/__init__.py +22 -0
- fractal_server/types/validators/__init__.py +3 -0
- fractal_server/types/validators/_common_validators.py +32 -0
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/METADATA +3 -2
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/RECORD +108 -98
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -18,14 +18,15 @@ from fractal_server.app.models.v2 import LinkUserProjectV2
|
|
|
18
18
|
from fractal_server.app.routes.auth import current_user_act_ver_prof
|
|
19
19
|
from fractal_server.app.routes.aux._job import _write_shutdown_file
|
|
20
20
|
from fractal_server.app.routes.aux._runner import _check_shutdown_is_supported
|
|
21
|
-
from fractal_server.app.schemas.v2 import
|
|
22
|
-
from fractal_server.app.schemas.v2 import
|
|
21
|
+
from fractal_server.app.schemas.v2 import JobRead
|
|
22
|
+
from fractal_server.app.schemas.v2 import JobStatusType
|
|
23
|
+
from fractal_server.app.schemas.v2.sharing import ProjectPermissions
|
|
23
24
|
from fractal_server.runner.filenames import WORKFLOW_LOG_FILENAME
|
|
24
25
|
from fractal_server.zip_tools import _zip_folder_to_byte_stream_iterator
|
|
25
26
|
|
|
26
|
-
from ._aux_functions import
|
|
27
|
-
from ._aux_functions import
|
|
28
|
-
from ._aux_functions import
|
|
27
|
+
from ._aux_functions import _get_job_check_access
|
|
28
|
+
from ._aux_functions import _get_project_check_access
|
|
29
|
+
from ._aux_functions import _get_workflow_check_access
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
# https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
|
|
@@ -38,12 +39,12 @@ async def zip_folder_threaded(folder: str) -> Iterator[bytes]:
|
|
|
38
39
|
router = APIRouter()
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
@router.get("/job/", response_model=list[
|
|
42
|
+
@router.get("/job/", response_model=list[JobRead])
|
|
42
43
|
async def get_user_jobs(
|
|
43
44
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
44
45
|
log: bool = True,
|
|
45
46
|
db: AsyncSession = Depends(get_async_db),
|
|
46
|
-
) -> list[
|
|
47
|
+
) -> list[JobRead]:
|
|
47
48
|
"""
|
|
48
49
|
Returns all the jobs of the current user
|
|
49
50
|
"""
|
|
@@ -53,6 +54,7 @@ async def get_user_jobs(
|
|
|
53
54
|
LinkUserProjectV2, LinkUserProjectV2.project_id == JobV2.project_id
|
|
54
55
|
)
|
|
55
56
|
.where(LinkUserProjectV2.user_id == user.id)
|
|
57
|
+
.where(LinkUserProjectV2.is_owner.is_(True))
|
|
56
58
|
)
|
|
57
59
|
res = await db.execute(stm)
|
|
58
60
|
job_list = res.scalars().all()
|
|
@@ -66,19 +68,23 @@ async def get_user_jobs(
|
|
|
66
68
|
|
|
67
69
|
@router.get(
|
|
68
70
|
"/project/{project_id}/workflow/{workflow_id}/job/",
|
|
69
|
-
response_model=list[
|
|
71
|
+
response_model=list[JobRead],
|
|
70
72
|
)
|
|
71
73
|
async def get_workflow_jobs(
|
|
72
74
|
project_id: int,
|
|
73
75
|
workflow_id: int,
|
|
74
76
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
75
77
|
db: AsyncSession = Depends(get_async_db),
|
|
76
|
-
) -> list[
|
|
78
|
+
) -> list[JobRead] | None:
|
|
77
79
|
"""
|
|
78
80
|
Returns all the jobs related to a specific workflow
|
|
79
81
|
"""
|
|
80
|
-
await
|
|
81
|
-
project_id=project_id,
|
|
82
|
+
await _get_workflow_check_access(
|
|
83
|
+
project_id=project_id,
|
|
84
|
+
workflow_id=workflow_id,
|
|
85
|
+
user_id=user.id,
|
|
86
|
+
required_permissions=ProjectPermissions.READ,
|
|
87
|
+
db=db,
|
|
82
88
|
)
|
|
83
89
|
stm = select(JobV2).where(JobV2.workflow_id == workflow_id)
|
|
84
90
|
res = await db.execute(stm)
|
|
@@ -93,9 +99,13 @@ async def get_latest_job(
|
|
|
93
99
|
dataset_id: int,
|
|
94
100
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
95
101
|
db: AsyncSession = Depends(get_async_db),
|
|
96
|
-
) ->
|
|
97
|
-
await
|
|
98
|
-
project_id=project_id,
|
|
102
|
+
) -> JobRead:
|
|
103
|
+
await _get_workflow_check_access(
|
|
104
|
+
project_id=project_id,
|
|
105
|
+
workflow_id=workflow_id,
|
|
106
|
+
user_id=user.id,
|
|
107
|
+
required_permissions=ProjectPermissions.READ,
|
|
108
|
+
db=db,
|
|
99
109
|
)
|
|
100
110
|
stm = (
|
|
101
111
|
select(JobV2)
|
|
@@ -117,7 +127,7 @@ async def get_latest_job(
|
|
|
117
127
|
|
|
118
128
|
@router.get(
|
|
119
129
|
"/project/{project_id}/job/{job_id}/",
|
|
120
|
-
response_model=
|
|
130
|
+
response_model=JobRead,
|
|
121
131
|
)
|
|
122
132
|
async def read_job(
|
|
123
133
|
project_id: int,
|
|
@@ -125,21 +135,22 @@ async def read_job(
|
|
|
125
135
|
show_tmp_logs: bool = False,
|
|
126
136
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
127
137
|
db: AsyncSession = Depends(get_async_db),
|
|
128
|
-
) ->
|
|
138
|
+
) -> JobRead | None:
|
|
129
139
|
"""
|
|
130
140
|
Return info on an existing job
|
|
131
141
|
"""
|
|
132
142
|
|
|
133
|
-
output = await
|
|
143
|
+
output = await _get_job_check_access(
|
|
134
144
|
project_id=project_id,
|
|
135
145
|
job_id=job_id,
|
|
136
146
|
user_id=user.id,
|
|
147
|
+
required_permissions=ProjectPermissions.READ,
|
|
137
148
|
db=db,
|
|
138
149
|
)
|
|
139
150
|
job = output["job"]
|
|
140
151
|
await db.close()
|
|
141
152
|
|
|
142
|
-
if show_tmp_logs and (job.status ==
|
|
153
|
+
if show_tmp_logs and (job.status == JobStatusType.SUBMITTED):
|
|
143
154
|
try:
|
|
144
155
|
with open(f"{job.working_dir}/{WORKFLOW_LOG_FILENAME}") as f:
|
|
145
156
|
job.log = f.read()
|
|
@@ -162,10 +173,11 @@ async def download_job_logs(
|
|
|
162
173
|
"""
|
|
163
174
|
Download zipped job folder
|
|
164
175
|
"""
|
|
165
|
-
output = await
|
|
176
|
+
output = await _get_job_check_access(
|
|
166
177
|
project_id=project_id,
|
|
167
178
|
job_id=job_id,
|
|
168
179
|
user_id=user.id,
|
|
180
|
+
required_permissions=ProjectPermissions.READ,
|
|
169
181
|
db=db,
|
|
170
182
|
)
|
|
171
183
|
job = output["job"]
|
|
@@ -182,19 +194,22 @@ async def download_job_logs(
|
|
|
182
194
|
|
|
183
195
|
@router.get(
|
|
184
196
|
"/project/{project_id}/job/",
|
|
185
|
-
response_model=list[
|
|
197
|
+
response_model=list[JobRead],
|
|
186
198
|
)
|
|
187
199
|
async def get_job_list(
|
|
188
200
|
project_id: int,
|
|
189
201
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
190
202
|
log: bool = True,
|
|
191
203
|
db: AsyncSession = Depends(get_async_db),
|
|
192
|
-
) -> list[
|
|
204
|
+
) -> list[JobRead] | None:
|
|
193
205
|
"""
|
|
194
206
|
Get job list for given project
|
|
195
207
|
"""
|
|
196
|
-
project = await
|
|
197
|
-
project_id=project_id,
|
|
208
|
+
project = await _get_project_check_access(
|
|
209
|
+
project_id=project_id,
|
|
210
|
+
user_id=user.id,
|
|
211
|
+
required_permissions=ProjectPermissions.READ,
|
|
212
|
+
db=db,
|
|
198
213
|
)
|
|
199
214
|
|
|
200
215
|
stm = select(JobV2).where(JobV2.project_id == project.id)
|
|
@@ -225,10 +240,11 @@ async def stop_job(
|
|
|
225
240
|
_check_shutdown_is_supported()
|
|
226
241
|
|
|
227
242
|
# Get job from DB
|
|
228
|
-
output = await
|
|
243
|
+
output = await _get_job_check_access(
|
|
229
244
|
project_id=project_id,
|
|
230
245
|
job_id=job_id,
|
|
231
246
|
user_id=user.id,
|
|
247
|
+
required_permissions=ProjectPermissions.EXECUTE,
|
|
232
248
|
db=db,
|
|
233
249
|
)
|
|
234
250
|
job = output["job"]
|
|
@@ -11,14 +11,15 @@ from fractal_server.app.models import UserOAuth
|
|
|
11
11
|
from fractal_server.app.routes.auth import current_user_act_ver_prof
|
|
12
12
|
from fractal_server.app.schemas.v2 import HistoryUnitStatus
|
|
13
13
|
from fractal_server.app.schemas.v2 import TaskType
|
|
14
|
+
from fractal_server.app.schemas.v2.sharing import ProjectPermissions
|
|
14
15
|
from fractal_server.images.status_tools import IMAGE_STATUS_KEY
|
|
15
16
|
from fractal_server.images.status_tools import enrich_images_unsorted_async
|
|
16
17
|
from fractal_server.images.tools import aggregate_types
|
|
17
18
|
from fractal_server.images.tools import filter_image_list
|
|
18
19
|
from fractal_server.types import AttributeFilters
|
|
19
20
|
|
|
20
|
-
from ._aux_functions import
|
|
21
|
-
from ._aux_functions import
|
|
21
|
+
from ._aux_functions import _get_dataset_check_access
|
|
22
|
+
from ._aux_functions import _get_workflow_task_check_access
|
|
22
23
|
from .images import ImageQuery
|
|
23
24
|
|
|
24
25
|
router = APIRouter()
|
|
@@ -37,8 +38,12 @@ async def verify_unique_types(
|
|
|
37
38
|
db: AsyncSession = Depends(get_async_db),
|
|
38
39
|
) -> list[str]:
|
|
39
40
|
# Get dataset
|
|
40
|
-
output = await
|
|
41
|
-
project_id=project_id,
|
|
41
|
+
output = await _get_dataset_check_access(
|
|
42
|
+
project_id=project_id,
|
|
43
|
+
dataset_id=dataset_id,
|
|
44
|
+
user_id=user.id,
|
|
45
|
+
required_permissions=ProjectPermissions.READ,
|
|
46
|
+
db=db,
|
|
42
47
|
)
|
|
43
48
|
dataset = output["dataset"]
|
|
44
49
|
|
|
@@ -97,11 +102,12 @@ async def check_non_processed_images(
|
|
|
97
102
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
98
103
|
db: AsyncSession = Depends(get_async_db),
|
|
99
104
|
) -> JSONResponse:
|
|
100
|
-
db_workflow_task, db_workflow = await
|
|
105
|
+
db_workflow_task, db_workflow = await _get_workflow_task_check_access(
|
|
101
106
|
project_id=project_id,
|
|
102
107
|
workflow_task_id=workflowtask_id,
|
|
103
108
|
workflow_id=workflow_id,
|
|
104
109
|
user_id=user.id,
|
|
110
|
+
required_permissions=ProjectPermissions.READ,
|
|
105
111
|
db=db,
|
|
106
112
|
)
|
|
107
113
|
|
|
@@ -121,10 +127,11 @@ async def check_non_processed_images(
|
|
|
121
127
|
# Skip check if previous task is converter
|
|
122
128
|
return JSONResponse(status_code=200, content=[])
|
|
123
129
|
|
|
124
|
-
res = await
|
|
130
|
+
res = await _get_dataset_check_access(
|
|
125
131
|
project_id=project_id,
|
|
126
132
|
dataset_id=dataset_id,
|
|
127
133
|
user_id=user.id,
|
|
134
|
+
required_permissions=ProjectPermissions.READ,
|
|
128
135
|
db=db,
|
|
129
136
|
)
|
|
130
137
|
dataset = res["dataset"]
|
|
@@ -15,21 +15,23 @@ from fractal_server.app.routes.auth import current_user_act_ver_prof
|
|
|
15
15
|
from fractal_server.app.routes.aux.validate_user_profile import (
|
|
16
16
|
validate_user_profile,
|
|
17
17
|
)
|
|
18
|
-
from fractal_server.app.schemas.v2 import
|
|
19
|
-
from fractal_server.app.schemas.v2 import
|
|
20
|
-
from fractal_server.app.schemas.v2 import
|
|
21
|
-
from fractal_server.
|
|
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
22
|
from fractal_server.logger import set_logger
|
|
23
23
|
|
|
24
24
|
from ._aux_functions import _check_project_exists
|
|
25
|
-
from ._aux_functions import
|
|
25
|
+
from ._aux_functions import _get_project_check_access
|
|
26
26
|
from ._aux_functions import _get_submitted_jobs_statement
|
|
27
27
|
|
|
28
|
+
logger = set_logger(__name__)
|
|
28
29
|
router = APIRouter()
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
@router.get("/project/", response_model=list[
|
|
32
|
+
@router.get("/project/", response_model=list[ProjectRead])
|
|
32
33
|
async def get_list_project(
|
|
34
|
+
is_owner: bool = True,
|
|
33
35
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
34
36
|
db: AsyncSession = Depends(get_async_db),
|
|
35
37
|
) -> list[ProjectV2]:
|
|
@@ -40,6 +42,8 @@ async def get_list_project(
|
|
|
40
42
|
select(ProjectV2)
|
|
41
43
|
.join(LinkUserProjectV2, LinkUserProjectV2.project_id == ProjectV2.id)
|
|
42
44
|
.where(LinkUserProjectV2.user_id == user.id)
|
|
45
|
+
.where(LinkUserProjectV2.is_owner == is_owner)
|
|
46
|
+
.where(LinkUserProjectV2.is_verified.is_(True))
|
|
43
47
|
)
|
|
44
48
|
res = await db.execute(stm)
|
|
45
49
|
project_list = res.scalars().all()
|
|
@@ -47,12 +51,12 @@ async def get_list_project(
|
|
|
47
51
|
return project_list
|
|
48
52
|
|
|
49
53
|
|
|
50
|
-
@router.post("/project/", response_model=
|
|
54
|
+
@router.post("/project/", response_model=ProjectRead, status_code=201)
|
|
51
55
|
async def create_project(
|
|
52
|
-
project:
|
|
56
|
+
project: ProjectCreate,
|
|
53
57
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
54
58
|
db: AsyncSession = Depends(get_async_db),
|
|
55
|
-
) ->
|
|
59
|
+
) -> ProjectRead | None:
|
|
56
60
|
"""
|
|
57
61
|
Create new project
|
|
58
62
|
"""
|
|
@@ -73,7 +77,13 @@ async def create_project(
|
|
|
73
77
|
db.add(db_project)
|
|
74
78
|
await db.flush()
|
|
75
79
|
|
|
76
|
-
link = LinkUserProjectV2(
|
|
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
|
+
)
|
|
77
87
|
db.add(link)
|
|
78
88
|
|
|
79
89
|
await db.commit()
|
|
@@ -82,31 +92,37 @@ async def create_project(
|
|
|
82
92
|
return db_project
|
|
83
93
|
|
|
84
94
|
|
|
85
|
-
@router.get("/project/{project_id}/", response_model=
|
|
95
|
+
@router.get("/project/{project_id}/", response_model=ProjectRead)
|
|
86
96
|
async def read_project(
|
|
87
97
|
project_id: int,
|
|
88
98
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
89
99
|
db: AsyncSession = Depends(get_async_db),
|
|
90
|
-
) ->
|
|
100
|
+
) -> ProjectRead | None:
|
|
91
101
|
"""
|
|
92
102
|
Return info on an existing project
|
|
93
103
|
"""
|
|
94
|
-
project = await
|
|
95
|
-
project_id=project_id,
|
|
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,
|
|
96
109
|
)
|
|
97
110
|
await db.close()
|
|
98
111
|
return project
|
|
99
112
|
|
|
100
113
|
|
|
101
|
-
@router.patch("/project/{project_id}/", response_model=
|
|
114
|
+
@router.patch("/project/{project_id}/", response_model=ProjectRead)
|
|
102
115
|
async def update_project(
|
|
103
116
|
project_id: int,
|
|
104
|
-
project_update:
|
|
117
|
+
project_update: ProjectUpdate,
|
|
105
118
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
106
119
|
db: AsyncSession = Depends(get_async_db),
|
|
107
120
|
):
|
|
108
|
-
project = await
|
|
109
|
-
project_id=project_id,
|
|
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,
|
|
110
126
|
)
|
|
111
127
|
|
|
112
128
|
# Check that there is no project with the same user and name
|
|
@@ -134,10 +150,18 @@ async def delete_project(
|
|
|
134
150
|
Delete project
|
|
135
151
|
"""
|
|
136
152
|
|
|
137
|
-
project = await
|
|
138
|
-
project_id=project_id,
|
|
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,
|
|
139
158
|
)
|
|
140
|
-
|
|
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
|
+
)
|
|
141
165
|
|
|
142
166
|
# Fail if there exist jobs that are submitted and in relation with the
|
|
143
167
|
# current project.
|
|
@@ -154,13 +178,12 @@ async def delete_project(
|
|
|
154
178
|
),
|
|
155
179
|
)
|
|
156
180
|
|
|
157
|
-
logger.
|
|
181
|
+
logger.debug(f"Add project {project.id} to deletion.")
|
|
158
182
|
await db.delete(project)
|
|
159
183
|
|
|
160
|
-
logger.
|
|
184
|
+
logger.debug("Commit changes to db")
|
|
161
185
|
await db.commit()
|
|
162
186
|
|
|
163
|
-
logger.
|
|
164
|
-
reset_logger_handlers(logger)
|
|
187
|
+
logger.debug("Everything has been deleted correctly.")
|
|
165
188
|
|
|
166
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)
|