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.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +21 -19
- fractal_server/app/db/__init__.py +3 -3
- fractal_server/app/models/__init__.py +1 -0
- fractal_server/app/models/linkuserproject.py +43 -1
- fractal_server/app/models/security.py +28 -8
- fractal_server/app/models/v2/__init__.py +3 -1
- fractal_server/app/models/v2/accounting.py +9 -1
- fractal_server/app/models/v2/dataset.py +5 -1
- fractal_server/app/models/v2/history.py +15 -1
- fractal_server/app/models/v2/job.py +17 -2
- fractal_server/app/models/v2/profile.py +29 -0
- fractal_server/app/models/v2/project.py +4 -10
- fractal_server/app/models/v2/resource.py +17 -0
- fractal_server/app/models/v2/task_group.py +4 -3
- fractal_server/app/models/v2/workflow.py +2 -1
- fractal_server/app/routes/admin/v2/__init__.py +12 -13
- fractal_server/app/routes/admin/v2/accounting.py +3 -3
- fractal_server/app/routes/admin/v2/job.py +35 -24
- fractal_server/app/routes/admin/v2/profile.py +3 -2
- fractal_server/app/routes/admin/v2/resource.py +5 -5
- fractal_server/app/routes/admin/v2/sharing.py +103 -0
- fractal_server/app/routes/admin/v2/task.py +37 -26
- fractal_server/app/routes/admin/v2/task_group.py +94 -17
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +21 -22
- fractal_server/app/routes/api/__init__.py +1 -9
- fractal_server/app/routes/api/v2/__init__.py +49 -50
- fractal_server/app/routes/api/v2/_aux_functions.py +132 -124
- fractal_server/app/routes/api/v2/_aux_functions_history.py +51 -23
- fractal_server/app/routes/api/v2/_aux_functions_sharing.py +97 -0
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +6 -8
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +7 -9
- fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +1 -2
- fractal_server/app/routes/api/v2/dataset.py +95 -102
- fractal_server/app/routes/api/v2/history.py +59 -33
- fractal_server/app/routes/api/v2/images.py +24 -9
- fractal_server/app/routes/api/v2/job.py +52 -33
- fractal_server/app/routes/api/v2/pre_submission_checks.py +16 -8
- fractal_server/app/routes/api/v2/project.py +65 -37
- fractal_server/app/routes/api/v2/sharing.py +311 -0
- fractal_server/app/routes/api/v2/status_legacy.py +31 -41
- fractal_server/app/routes/api/v2/submit.py +82 -78
- fractal_server/app/routes/api/v2/task.py +19 -20
- fractal_server/app/routes/api/v2/task_collection.py +41 -43
- fractal_server/app/routes/api/v2/task_collection_custom.py +19 -20
- fractal_server/app/routes/api/v2/task_collection_pixi.py +10 -11
- fractal_server/app/routes/api/v2/task_group.py +25 -24
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +32 -32
- fractal_server/app/routes/api/v2/task_version_update.py +23 -19
- fractal_server/app/routes/api/v2/workflow.py +50 -55
- fractal_server/app/routes/api/v2/workflow_import.py +37 -37
- fractal_server/app/routes/api/v2/workflowtask.py +32 -26
- fractal_server/app/routes/auth/__init__.py +1 -3
- fractal_server/app/routes/auth/_aux_auth.py +101 -2
- fractal_server/app/routes/auth/current_user.py +2 -66
- fractal_server/app/routes/auth/group.py +8 -35
- fractal_server/app/routes/auth/login.py +1 -0
- fractal_server/app/routes/auth/oauth.py +4 -3
- fractal_server/app/routes/auth/register.py +4 -2
- fractal_server/app/routes/auth/router.py +2 -0
- fractal_server/app/routes/auth/users.py +19 -10
- fractal_server/app/routes/auth/viewer_paths.py +43 -0
- fractal_server/app/routes/aux/_job.py +1 -1
- fractal_server/app/routes/aux/_runner.py +2 -2
- fractal_server/app/routes/pagination.py +1 -1
- 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/accounting.py +11 -0
- fractal_server/app/schemas/v2/dataset.py +57 -11
- fractal_server/app/schemas/v2/dumps.py +10 -9
- fractal_server/app/schemas/v2/job.py +11 -11
- fractal_server/app/schemas/v2/manifest.py +4 -3
- fractal_server/app/schemas/v2/profile.py +53 -2
- fractal_server/app/schemas/v2/project.py +3 -3
- fractal_server/app/schemas/v2/resource.py +121 -16
- 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 -7
- fractal_server/app/schemas/v2/task_collection.py +5 -5
- 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 +16 -15
- fractal_server/app/security/__init__.py +5 -8
- fractal_server/app/security/signup_email.py +4 -5
- fractal_server/app/shutdown.py +6 -6
- fractal_server/config/__init__.py +0 -6
- fractal_server/config/_data.py +0 -68
- fractal_server/config/_database.py +19 -20
- fractal_server/config/_email.py +30 -38
- fractal_server/config/_main.py +38 -52
- fractal_server/config/_oauth.py +17 -21
- fractal_server/data_migrations/2_18_0.py +30 -0
- fractal_server/exceptions.py +4 -0
- fractal_server/images/models.py +4 -5
- fractal_server/images/status_tools.py +4 -2
- fractal_server/logger.py +1 -1
- fractal_server/main.py +75 -13
- fractal_server/migrations/versions/034a469ec2eb_task_groups.py +4 -8
- fractal_server/migrations/versions/091b01f51f88_add_usergroup_and_linkusergroup_table.py +1 -1
- fractal_server/migrations/versions/0f5f85bb2ae7_add_pre_pinned_packages.py +1 -0
- fractal_server/migrations/versions/19eca0dd47a9_user_settings_project_dir.py +1 -1
- fractal_server/migrations/versions/1a83a5260664_rename.py +1 -1
- fractal_server/migrations/versions/1eac13a26c83_drop_v1_tables.py +1 -0
- fractal_server/migrations/versions/316140ff7ee1_remove_usersettings_cache_dir.py +1 -1
- fractal_server/migrations/versions/40d6d6511b20_add_index_to_history_models.py +47 -0
- fractal_server/migrations/versions/45fbb391d7af_make_resource_id_fk_non_nullable.py +1 -1
- fractal_server/migrations/versions/47351f8c7ebc_drop_dataset_filters.py +1 -0
- fractal_server/migrations/versions/49d0856e9569_drop_table.py +2 -3
- fractal_server/migrations/versions/4c308bcaea2b_add_task_args_schema_and_task_args_.py +1 -1
- fractal_server/migrations/versions/4cedeb448a53_workflowtask_foreign_keys_not_nullables.py +1 -1
- fractal_server/migrations/versions/501961cfcd85_remove_link_between_v1_and_v2_tasks_.py +2 -1
- fractal_server/migrations/versions/50a13d6138fd_initial_schema.py +7 -19
- fractal_server/migrations/versions/5bf02391cfef_v2.py +4 -10
- fractal_server/migrations/versions/70e77f1c38b0_add_applyworkflow_first_task_index_and_.py +1 -0
- fractal_server/migrations/versions/71eefd1dd202_add_slurm_accounts.py +1 -1
- fractal_server/migrations/versions/7673fe18c05d_remove_project_dir_server_default.py +1 -1
- fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py +60 -0
- fractal_server/migrations/versions/791ce783d3d8_add_indices.py +1 -1
- fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py +1 -0
- fractal_server/migrations/versions/84bf0fffde30_add_dumps_to_applyworkflow.py +1 -0
- fractal_server/migrations/versions/88270f589c9b_add_prevent_new_submissions.py +39 -0
- fractal_server/migrations/versions/8e8f227a3e36_update_taskv2_post_2_7_0.py +2 -4
- fractal_server/migrations/versions/8f79bd162e35_add_docs_info_and_docs_link_to_task_.py +1 -1
- fractal_server/migrations/versions/94a47ea2d3ff_remove_cache_dir_slurm_user_and_slurm_.py +1 -0
- fractal_server/migrations/versions/969d84257cac_add_historyrun_task_id.py +1 -1
- fractal_server/migrations/versions/97f444d47249_add_applyworkflow_project_dump.py +1 -1
- fractal_server/migrations/versions/981d588fe248_add_executor_error_log.py +1 -1
- fractal_server/migrations/versions/99ea79d9e5d2_add_dataset_history.py +2 -4
- fractal_server/migrations/versions/9c5ae74c9b98_add_user_settings_table.py +1 -1
- fractal_server/migrations/versions/9db60297b8b2_set_ondelete.py +1 -1
- fractal_server/migrations/versions/9fd26a2b0de4_add_workflow_timestamp_created.py +1 -1
- fractal_server/migrations/versions/a7f4d6137b53_add_workflow_dump_to_applyworkflow.py +1 -1
- fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py +1 -0
- fractal_server/migrations/versions/af8673379a5c_drop_old_filter_columns.py +1 -0
- fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py +1 -1
- fractal_server/migrations/versions/b3ffb095f973_json_to_jsonb.py +1 -0
- fractal_server/migrations/versions/bc0e8b3327a7_project_sharing.py +72 -0
- fractal_server/migrations/versions/c90a7c76e996_job_id_in_history_run.py +1 -1
- fractal_server/migrations/versions/caba9fb1ea5e_drop_useroauth_user_settings_id.py +1 -1
- fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py +4 -9
- fractal_server/migrations/versions/d4fe3708d309_make_applyworkflow_workflow_dump_non_.py +1 -0
- fractal_server/migrations/versions/da2cb2ac4255_user_group_viewer_paths.py +1 -1
- fractal_server/migrations/versions/db09233ad13a_split_filters_and_keep_old_columns.py +1 -0
- fractal_server/migrations/versions/e0e717ae2f26_delete_linkuserproject_ondelete_project.py +50 -0
- fractal_server/migrations/versions/e75cac726012_make_applyworkflow_start_timestamp_not_.py +1 -0
- fractal_server/migrations/versions/e81103413827_add_job_type_filters.py +1 -1
- fractal_server/migrations/versions/efa89c30e0a4_add_project_timestamp_created.py +1 -0
- fractal_server/migrations/versions/f0702066b007_one_submitted_job_per_dataset.py +40 -0
- fractal_server/migrations/versions/f37aceb45062_make_historyunit_logfile_required.py +1 -1
- fractal_server/migrations/versions/f384e1c0cf5d_drop_task_default_args_columns.py +1 -0
- fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py +4 -9
- fractal_server/runner/config/_local.py +8 -5
- fractal_server/runner/config/_slurm.py +39 -33
- fractal_server/runner/config/slurm_mem_to_MB.py +0 -1
- fractal_server/runner/executors/base_runner.py +29 -4
- fractal_server/runner/executors/local/get_local_config.py +1 -0
- fractal_server/runner/executors/local/runner.py +14 -13
- fractal_server/runner/executors/slurm_common/_batching.py +9 -20
- fractal_server/runner/executors/slurm_common/base_slurm_runner.py +53 -27
- fractal_server/runner/executors/slurm_common/get_slurm_config.py +14 -7
- fractal_server/runner/executors/slurm_common/remote.py +3 -1
- fractal_server/runner/executors/slurm_common/slurm_config.py +2 -0
- fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -3
- fractal_server/runner/executors/slurm_ssh/runner.py +16 -11
- fractal_server/runner/executors/slurm_ssh/tar_commands.py +1 -0
- fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +1 -0
- fractal_server/runner/executors/slurm_sudo/runner.py +16 -11
- fractal_server/runner/task_files.py +9 -3
- fractal_server/runner/v2/_local.py +12 -6
- fractal_server/runner/v2/_slurm_ssh.py +14 -7
- fractal_server/runner/v2/_slurm_sudo.py +14 -7
- fractal_server/runner/v2/db_tools.py +0 -1
- fractal_server/runner/v2/deduplicate_list.py +2 -1
- fractal_server/runner/v2/runner.py +44 -28
- fractal_server/runner/v2/runner_functions.py +22 -28
- fractal_server/runner/v2/submit_workflow.py +29 -15
- fractal_server/ssh/_fabric.py +6 -13
- fractal_server/string_tools.py +0 -1
- fractal_server/syringe.py +1 -1
- fractal_server/tasks/config/_pixi.py +1 -1
- fractal_server/tasks/config/_python.py +16 -9
- fractal_server/tasks/utils.py +0 -1
- fractal_server/tasks/v2/local/_utils.py +3 -3
- fractal_server/tasks/v2/local/collect.py +15 -18
- fractal_server/tasks/v2/local/collect_pixi.py +14 -16
- fractal_server/tasks/v2/local/deactivate.py +14 -15
- fractal_server/tasks/v2/local/deactivate_pixi.py +7 -7
- fractal_server/tasks/v2/local/delete.py +6 -8
- fractal_server/tasks/v2/local/reactivate.py +12 -12
- fractal_server/tasks/v2/local/reactivate_pixi.py +12 -12
- fractal_server/tasks/v2/ssh/_utils.py +3 -3
- fractal_server/tasks/v2/ssh/collect.py +19 -24
- fractal_server/tasks/v2/ssh/collect_pixi.py +22 -24
- fractal_server/tasks/v2/ssh/deactivate.py +17 -15
- fractal_server/tasks/v2/ssh/deactivate_pixi.py +8 -7
- fractal_server/tasks/v2/ssh/delete.py +12 -10
- fractal_server/tasks/v2/ssh/reactivate.py +16 -16
- fractal_server/tasks/v2/ssh/reactivate_pixi.py +13 -14
- fractal_server/tasks/v2/templates/1_create_venv.sh +2 -0
- fractal_server/tasks/v2/templates/2_pip_install.sh +2 -0
- fractal_server/tasks/v2/templates/3_pip_freeze.sh +2 -0
- fractal_server/tasks/v2/templates/4_pip_show.sh +2 -0
- fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +3 -1
- fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +2 -0
- fractal_server/tasks/v2/templates/pixi_1_extract.sh +2 -0
- fractal_server/tasks/v2/templates/pixi_2_install.sh +2 -0
- fractal_server/tasks/v2/templates/pixi_3_post_install.sh +2 -0
- fractal_server/tasks/v2/utils_background.py +10 -10
- fractal_server/tasks/v2/utils_database.py +5 -5
- fractal_server/tasks/v2/utils_package_names.py +1 -2
- fractal_server/tasks/v2/utils_pixi.py +1 -3
- fractal_server/types/__init__.py +98 -1
- fractal_server/types/validators/__init__.py +3 -0
- fractal_server/types/validators/_common_validators.py +33 -3
- fractal_server/types/validators/_workflow_task_arguments_validators.py +1 -2
- fractal_server/utils.py +1 -0
- fractal_server/zip_tools.py +34 -0
- {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/METADATA +3 -2
- fractal_server-2.18.0.dist-info/RECORD +275 -0
- fractal_server/app/routes/admin/v2/project.py +0 -41
- fractal_server-2.17.1a1.dist-info/RECORD +0 -264
- {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Auxiliary functions to get object from the database or perform simple checks
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from typing import Any
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import TypedDict
|
|
6
7
|
|
|
7
8
|
from fastapi import HTTPException
|
|
8
9
|
from fastapi import status
|
|
9
|
-
from sqlalchemy.exc import MultipleResultsFound
|
|
10
10
|
from sqlalchemy.orm.attributes import flag_modified
|
|
11
11
|
from sqlmodel import select
|
|
12
12
|
from sqlmodel.sql.expression import SelectOfScalar
|
|
13
13
|
|
|
14
|
-
from ....models.v2 import DatasetV2
|
|
15
|
-
from ....models.v2 import JobV2
|
|
16
|
-
from ....models.v2 import LinkUserProjectV2
|
|
17
|
-
from ....models.v2 import ProjectV2
|
|
18
|
-
from ....models.v2 import TaskV2
|
|
19
|
-
from ....models.v2 import WorkflowTaskV2
|
|
20
|
-
from ....models.v2 import WorkflowV2
|
|
21
|
-
from ....schemas.v2 import JobStatusTypeV2
|
|
22
14
|
from fractal_server.app.db import AsyncSession
|
|
23
15
|
from fractal_server.app.models import Profile
|
|
24
16
|
from fractal_server.app.models import Resource
|
|
25
17
|
from fractal_server.app.models import UserOAuth
|
|
18
|
+
from fractal_server.app.models.v2 import DatasetV2
|
|
19
|
+
from fractal_server.app.models.v2 import JobV2
|
|
20
|
+
from fractal_server.app.models.v2 import LinkUserProjectV2
|
|
21
|
+
from fractal_server.app.models.v2 import ProjectV2
|
|
22
|
+
from fractal_server.app.models.v2 import TaskV2
|
|
23
|
+
from fractal_server.app.models.v2 import WorkflowTaskV2
|
|
24
|
+
from fractal_server.app.models.v2 import WorkflowV2
|
|
25
|
+
from fractal_server.app.schemas.v2 import JobStatusType
|
|
26
|
+
from fractal_server.app.schemas.v2 import ProjectPermissions
|
|
26
27
|
from fractal_server.logger import set_logger
|
|
27
28
|
|
|
28
29
|
logger = set_logger(__name__)
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
async def
|
|
32
|
+
async def _get_project_check_access(
|
|
32
33
|
*,
|
|
33
34
|
project_id: int,
|
|
34
35
|
user_id: int,
|
|
36
|
+
required_permissions: ProjectPermissions,
|
|
35
37
|
db: AsyncSession,
|
|
36
38
|
) -> ProjectV2:
|
|
37
39
|
"""
|
|
@@ -40,6 +42,7 @@ async def _get_project_check_owner(
|
|
|
40
42
|
Args:
|
|
41
43
|
project_id:
|
|
42
44
|
user_id:
|
|
45
|
+
required_permissions:
|
|
43
46
|
db:
|
|
44
47
|
|
|
45
48
|
Returns:
|
|
@@ -47,31 +50,42 @@ async def _get_project_check_owner(
|
|
|
47
50
|
|
|
48
51
|
Raises:
|
|
49
52
|
HTTPException(status_code=403_FORBIDDEN):
|
|
50
|
-
If the user is not a member of the project
|
|
53
|
+
- If the user is not a member of the project;
|
|
54
|
+
- If the user has not accepted the invitation yet;
|
|
55
|
+
- If the user has not the target permissions.
|
|
51
56
|
HTTPException(status_code=404_NOT_FOUND):
|
|
52
57
|
If the project does not exist
|
|
53
58
|
"""
|
|
54
59
|
project = await db.get(ProjectV2, project_id)
|
|
55
|
-
|
|
56
|
-
link_user_project = await db.get(LinkUserProjectV2, (project_id, user_id))
|
|
57
|
-
if not project:
|
|
60
|
+
if project is None:
|
|
58
61
|
raise HTTPException(
|
|
59
62
|
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
|
|
60
63
|
)
|
|
61
|
-
|
|
64
|
+
|
|
65
|
+
link_user_project = await db.get(LinkUserProjectV2, (project_id, user_id))
|
|
66
|
+
if (
|
|
67
|
+
link_user_project is None
|
|
68
|
+
or not link_user_project.is_verified
|
|
69
|
+
or required_permissions not in link_user_project.permissions
|
|
70
|
+
):
|
|
62
71
|
raise HTTPException(
|
|
63
72
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
64
|
-
detail=
|
|
73
|
+
detail=(
|
|
74
|
+
"You are not authorized to perform this action. "
|
|
75
|
+
"If you think this is by mistake, "
|
|
76
|
+
"please contact the project owner."
|
|
77
|
+
),
|
|
65
78
|
)
|
|
66
79
|
|
|
67
80
|
return project
|
|
68
81
|
|
|
69
82
|
|
|
70
|
-
async def
|
|
83
|
+
async def _get_workflow_check_access(
|
|
71
84
|
*,
|
|
72
85
|
workflow_id: int,
|
|
73
86
|
project_id: int,
|
|
74
87
|
user_id: int,
|
|
88
|
+
required_permissions: ProjectPermissions,
|
|
75
89
|
db: AsyncSession,
|
|
76
90
|
) -> WorkflowV2:
|
|
77
91
|
"""
|
|
@@ -88,39 +102,43 @@ async def _get_workflow_check_owner(
|
|
|
88
102
|
|
|
89
103
|
Raises:
|
|
90
104
|
HTTPException(status_code=404_NOT_FOUND):
|
|
91
|
-
If the workflow
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
If the project or the workflow do not exist or if they are not
|
|
106
|
+
associated
|
|
107
|
+
HTTPException(status_code=403_FORBIDDEN):
|
|
108
|
+
If the user is not a member of the project
|
|
94
109
|
"""
|
|
95
110
|
|
|
96
111
|
# Access control for project
|
|
97
|
-
|
|
98
|
-
project_id=project_id,
|
|
112
|
+
await _get_project_check_access(
|
|
113
|
+
project_id=project_id,
|
|
114
|
+
user_id=user_id,
|
|
115
|
+
required_permissions=required_permissions,
|
|
116
|
+
db=db,
|
|
99
117
|
)
|
|
100
118
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
119
|
+
res = await db.execute(
|
|
120
|
+
select(WorkflowV2)
|
|
121
|
+
.where(WorkflowV2.id == workflow_id)
|
|
122
|
+
.where(WorkflowV2.project_id == project_id)
|
|
123
|
+
.execution_options(populate_existing=True) # See issue 1087
|
|
124
|
+
)
|
|
125
|
+
workflow = res.scalars().one_or_none()
|
|
104
126
|
|
|
105
127
|
if not workflow:
|
|
106
128
|
raise HTTPException(
|
|
107
129
|
status_code=status.HTTP_404_NOT_FOUND, detail="Workflow not found"
|
|
108
130
|
)
|
|
109
|
-
if workflow.project_id != project.id:
|
|
110
|
-
raise HTTPException(
|
|
111
|
-
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
112
|
-
detail=(f"Invalid {project_id=} for {workflow_id=}."),
|
|
113
|
-
)
|
|
114
131
|
|
|
115
132
|
return workflow
|
|
116
133
|
|
|
117
134
|
|
|
118
|
-
async def
|
|
135
|
+
async def _get_workflow_task_check_access(
|
|
119
136
|
*,
|
|
120
137
|
project_id: int,
|
|
121
138
|
workflow_id: int,
|
|
122
139
|
workflow_task_id: int,
|
|
123
140
|
user_id: int,
|
|
141
|
+
required_permissions: ProjectPermissions,
|
|
124
142
|
db: AsyncSession,
|
|
125
143
|
) -> tuple[WorkflowTaskV2, WorkflowV2]:
|
|
126
144
|
"""
|
|
@@ -138,34 +156,34 @@ async def _get_workflow_task_check_owner(
|
|
|
138
156
|
|
|
139
157
|
Raises:
|
|
140
158
|
HTTPException(status_code=404_NOT_FOUND):
|
|
141
|
-
If the
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
If the project, the workflow or the workflowtask do not exist or
|
|
160
|
+
if they are not associated
|
|
161
|
+
HTTPException(status_code=403_FORBIDDEN):
|
|
162
|
+
If the user is not a member of the project
|
|
144
163
|
"""
|
|
145
164
|
|
|
146
165
|
# Access control for workflow
|
|
147
|
-
workflow = await
|
|
166
|
+
workflow = await _get_workflow_check_access(
|
|
148
167
|
workflow_id=workflow_id,
|
|
149
168
|
project_id=project_id,
|
|
150
169
|
user_id=user_id,
|
|
170
|
+
required_permissions=required_permissions,
|
|
151
171
|
db=db,
|
|
152
172
|
)
|
|
153
173
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
174
|
+
res = await db.execute(
|
|
175
|
+
select(WorkflowTaskV2)
|
|
176
|
+
.where(WorkflowTaskV2.id == workflow_task_id)
|
|
177
|
+
.where(WorkflowTaskV2.workflow_id == workflow_id)
|
|
178
|
+
)
|
|
179
|
+
workflow_task = res.scalars().one_or_none()
|
|
180
|
+
|
|
181
|
+
if workflow_task is None:
|
|
157
182
|
raise HTTPException(
|
|
158
183
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
159
184
|
detail="WorkflowTask not found",
|
|
160
185
|
)
|
|
161
186
|
|
|
162
|
-
# If WorkflowTask is not part of the expected Workflow, exit
|
|
163
|
-
if workflow_id != workflow_task.workflow_id:
|
|
164
|
-
raise HTTPException(
|
|
165
|
-
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
166
|
-
detail=f"Invalid {workflow_id=} for {workflow_task_id=}",
|
|
167
|
-
)
|
|
168
|
-
|
|
169
187
|
return workflow_task, workflow
|
|
170
188
|
|
|
171
189
|
|
|
@@ -220,9 +238,10 @@ async def _check_project_exists(
|
|
|
220
238
|
"""
|
|
221
239
|
stm = (
|
|
222
240
|
select(ProjectV2)
|
|
223
|
-
.join(LinkUserProjectV2)
|
|
241
|
+
.join(LinkUserProjectV2, LinkUserProjectV2.project_id == ProjectV2.id)
|
|
224
242
|
.where(ProjectV2.name == project_name)
|
|
225
243
|
.where(LinkUserProjectV2.user_id == user_id)
|
|
244
|
+
.where(LinkUserProjectV2.is_owner.is_(True))
|
|
226
245
|
)
|
|
227
246
|
res = await db.execute(stm)
|
|
228
247
|
if res.scalars().all():
|
|
@@ -232,13 +251,19 @@ async def _check_project_exists(
|
|
|
232
251
|
)
|
|
233
252
|
|
|
234
253
|
|
|
235
|
-
|
|
254
|
+
class DatasetOrProject(TypedDict):
|
|
255
|
+
dataset: DatasetV2
|
|
256
|
+
project: ProjectV2
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def _get_dataset_check_access(
|
|
236
260
|
*,
|
|
237
261
|
project_id: int,
|
|
238
262
|
dataset_id: int,
|
|
239
263
|
user_id: int,
|
|
264
|
+
required_permissions: ProjectPermissions,
|
|
240
265
|
db: AsyncSession,
|
|
241
|
-
) ->
|
|
266
|
+
) -> DatasetOrProject:
|
|
242
267
|
"""
|
|
243
268
|
Get a dataset and a project, after access control on the project
|
|
244
269
|
|
|
@@ -253,38 +278,49 @@ async def _get_dataset_check_owner(
|
|
|
253
278
|
`project`).
|
|
254
279
|
|
|
255
280
|
Raises:
|
|
256
|
-
HTTPException(status_code=
|
|
257
|
-
If the dataset
|
|
281
|
+
HTTPException(status_code=404_UNPROCESSABLE_ENTITY):
|
|
282
|
+
If the project or the dataset do not exist or if they are not
|
|
283
|
+
associated
|
|
284
|
+
HTTPException(status_code=403_FORBIDDEN):
|
|
285
|
+
If the user is not a member of the project
|
|
258
286
|
"""
|
|
259
287
|
# Access control for project
|
|
260
|
-
project = await
|
|
261
|
-
project_id=project_id,
|
|
288
|
+
project = await _get_project_check_access(
|
|
289
|
+
project_id=project_id,
|
|
290
|
+
user_id=user_id,
|
|
291
|
+
required_permissions=required_permissions,
|
|
292
|
+
db=db,
|
|
262
293
|
)
|
|
263
294
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
295
|
+
res = await db.execute(
|
|
296
|
+
select(DatasetV2)
|
|
297
|
+
.where(DatasetV2.id == dataset_id)
|
|
298
|
+
.where(DatasetV2.project_id == project_id)
|
|
299
|
+
.execution_options(populate_existing=True) # See issue 1087
|
|
300
|
+
)
|
|
301
|
+
dataset = res.scalars().one_or_none()
|
|
267
302
|
|
|
268
|
-
if
|
|
303
|
+
if dataset is None:
|
|
269
304
|
raise HTTPException(
|
|
270
305
|
status_code=status.HTTP_404_NOT_FOUND, detail="Dataset not found"
|
|
271
306
|
)
|
|
272
|
-
if dataset.project_id != project_id:
|
|
273
|
-
raise HTTPException(
|
|
274
|
-
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
275
|
-
detail=f"Invalid {project_id=} for {dataset_id=}",
|
|
276
|
-
)
|
|
277
307
|
|
|
278
308
|
return dict(dataset=dataset, project=project)
|
|
279
309
|
|
|
280
310
|
|
|
281
|
-
|
|
311
|
+
class JobAndProject(TypedDict):
|
|
312
|
+
job: JobV2
|
|
313
|
+
project: ProjectV2
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
async def _get_job_check_access(
|
|
282
317
|
*,
|
|
283
318
|
project_id: int,
|
|
284
319
|
job_id: int,
|
|
285
320
|
user_id: int,
|
|
321
|
+
required_permissions: ProjectPermissions,
|
|
286
322
|
db: AsyncSession,
|
|
287
|
-
) ->
|
|
323
|
+
) -> JobAndProject:
|
|
288
324
|
"""
|
|
289
325
|
Get a job and a project, after access control on the project
|
|
290
326
|
|
|
@@ -299,26 +335,32 @@ async def _get_job_check_owner(
|
|
|
299
335
|
`project`).
|
|
300
336
|
|
|
301
337
|
Raises:
|
|
302
|
-
HTTPException(status_code=
|
|
303
|
-
If the job
|
|
338
|
+
HTTPException(status_code=404_UNPROCESSABLE_ENTITY):
|
|
339
|
+
If the project or the job do not exist or if they are not
|
|
340
|
+
associated
|
|
341
|
+
HTTPException(status_code=403_FORBIDDEN):
|
|
342
|
+
If the user is not a member of the project
|
|
304
343
|
"""
|
|
305
344
|
# Access control for project
|
|
306
|
-
project = await
|
|
345
|
+
project = await _get_project_check_access(
|
|
307
346
|
project_id=project_id,
|
|
308
347
|
user_id=user_id,
|
|
348
|
+
required_permissions=required_permissions,
|
|
309
349
|
db=db,
|
|
310
350
|
)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
351
|
+
|
|
352
|
+
res = await db.execute(
|
|
353
|
+
select(JobV2)
|
|
354
|
+
.where(JobV2.id == job_id)
|
|
355
|
+
.where(JobV2.project_id == project_id)
|
|
356
|
+
)
|
|
357
|
+
job = res.scalars().one_or_none()
|
|
358
|
+
|
|
359
|
+
if job is None:
|
|
314
360
|
raise HTTPException(
|
|
315
361
|
status_code=status.HTTP_404_NOT_FOUND, detail="Job not found"
|
|
316
362
|
)
|
|
317
|
-
|
|
318
|
-
raise HTTPException(
|
|
319
|
-
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
320
|
-
detail=f"Invalid {project_id=} for {job_id=}",
|
|
321
|
-
)
|
|
363
|
+
|
|
322
364
|
return dict(job=job, project=project)
|
|
323
365
|
|
|
324
366
|
|
|
@@ -328,7 +370,7 @@ def _get_submitted_jobs_statement() -> SelectOfScalar:
|
|
|
328
370
|
A sqlmodel statement that selects all `Job`s with
|
|
329
371
|
`Job.status` equal to `submitted`.
|
|
330
372
|
"""
|
|
331
|
-
stm = select(JobV2).where(JobV2.status ==
|
|
373
|
+
stm = select(JobV2).where(JobV2.status == JobStatusType.SUBMITTED)
|
|
332
374
|
return stm
|
|
333
375
|
|
|
334
376
|
|
|
@@ -338,7 +380,7 @@ async def _workflow_has_submitted_job(
|
|
|
338
380
|
) -> bool:
|
|
339
381
|
res = await db.execute(
|
|
340
382
|
select(JobV2.id)
|
|
341
|
-
.where(JobV2.status ==
|
|
383
|
+
.where(JobV2.status == JobStatusType.SUBMITTED)
|
|
342
384
|
.where(JobV2.workflow_id == workflow_id)
|
|
343
385
|
.limit(1)
|
|
344
386
|
)
|
|
@@ -411,14 +453,18 @@ async def _workflow_insert_task(
|
|
|
411
453
|
flag_modified(db_workflow, "task_list")
|
|
412
454
|
await db.commit()
|
|
413
455
|
|
|
414
|
-
|
|
415
|
-
|
|
456
|
+
wf_task = await db.get(
|
|
457
|
+
WorkflowTaskV2,
|
|
458
|
+
wf_task.id,
|
|
459
|
+
populate_existing=True, # See issue 1087
|
|
460
|
+
)
|
|
416
461
|
|
|
417
462
|
return wf_task
|
|
418
463
|
|
|
419
464
|
|
|
420
|
-
async def
|
|
421
|
-
db: AsyncSession,
|
|
465
|
+
async def clean_app_job_list(
|
|
466
|
+
db: AsyncSession,
|
|
467
|
+
jobs_list: list[int],
|
|
422
468
|
) -> list[int]:
|
|
423
469
|
"""
|
|
424
470
|
Remove from a job list all jobs with status different from submitted.
|
|
@@ -430,14 +476,14 @@ async def clean_app_job_list_v2(
|
|
|
430
476
|
Return:
|
|
431
477
|
List of IDs for submitted jobs.
|
|
432
478
|
"""
|
|
479
|
+
logger.info(f"[clean_app_job_list] START - {jobs_list=}.")
|
|
433
480
|
stmt = select(JobV2).where(JobV2.id.in_(jobs_list))
|
|
434
481
|
result = await db.execute(stmt)
|
|
435
482
|
db_jobs_list = result.scalars().all()
|
|
436
483
|
submitted_job_ids = [
|
|
437
|
-
job.id
|
|
438
|
-
for job in db_jobs_list
|
|
439
|
-
if job.status == JobStatusTypeV2.SUBMITTED
|
|
484
|
+
job.id for job in db_jobs_list if job.status == JobStatusType.SUBMITTED
|
|
440
485
|
]
|
|
486
|
+
logger.info(f"[clean_app_job_list] END - {submitted_job_ids=}.")
|
|
441
487
|
return submitted_job_ids
|
|
442
488
|
|
|
443
489
|
|
|
@@ -507,49 +553,11 @@ async def _get_workflowtask_or_404(
|
|
|
507
553
|
return wftask
|
|
508
554
|
|
|
509
555
|
|
|
510
|
-
async def _get_submitted_job_or_none(
|
|
511
|
-
*,
|
|
512
|
-
dataset_id: int,
|
|
513
|
-
workflow_id: int,
|
|
514
|
-
db: AsyncSession,
|
|
515
|
-
) -> JobV2 | None:
|
|
516
|
-
"""
|
|
517
|
-
Get the submitted job for given dataset/workflow, if any.
|
|
518
|
-
|
|
519
|
-
This function also handles the invalid branch where more than one job
|
|
520
|
-
is found.
|
|
521
|
-
|
|
522
|
-
Args:
|
|
523
|
-
dataset_id:
|
|
524
|
-
workflow_id:
|
|
525
|
-
db:
|
|
526
|
-
"""
|
|
527
|
-
res = await db.execute(
|
|
528
|
-
_get_submitted_jobs_statement()
|
|
529
|
-
.where(JobV2.dataset_id == dataset_id)
|
|
530
|
-
.where(JobV2.workflow_id == workflow_id)
|
|
531
|
-
)
|
|
532
|
-
try:
|
|
533
|
-
return res.scalars().one_or_none()
|
|
534
|
-
except MultipleResultsFound as e:
|
|
535
|
-
error_msg = (
|
|
536
|
-
"Multiple running jobs found for "
|
|
537
|
-
f"{dataset_id=} and {workflow_id=}."
|
|
538
|
-
)
|
|
539
|
-
logger.error(f"{error_msg} Original error: {str(e)}.")
|
|
540
|
-
raise HTTPException(
|
|
541
|
-
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
542
|
-
detail=error_msg,
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
|
|
546
556
|
async def _get_user_resource_id(user_id: int, db: AsyncSession) -> int | None:
|
|
547
557
|
res = await db.execute(
|
|
548
558
|
select(Resource.id)
|
|
549
|
-
.join(Profile)
|
|
550
|
-
.join(UserOAuth)
|
|
551
|
-
.where(Resource.id == Profile.resource_id)
|
|
552
|
-
.where(Profile.id == UserOAuth.profile_id)
|
|
559
|
+
.join(Profile, Resource.id == Profile.resource_id)
|
|
560
|
+
.join(UserOAuth, Profile.id == UserOAuth.profile_id)
|
|
553
561
|
.where(UserOAuth.id == user_id)
|
|
554
562
|
)
|
|
555
563
|
resource_id = res.scalar_one_or_none()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import Literal
|
|
3
4
|
|
|
@@ -12,16 +13,15 @@ from fractal_server.app.models.v2 import HistoryUnit
|
|
|
12
13
|
from fractal_server.app.models.v2 import WorkflowV2
|
|
13
14
|
from fractal_server.app.routes.api.v2._aux_functions import _get_dataset_or_404
|
|
14
15
|
from fractal_server.app.routes.api.v2._aux_functions import (
|
|
15
|
-
|
|
16
|
-
)
|
|
17
|
-
from fractal_server.app.routes.api.v2._aux_functions import (
|
|
18
|
-
_get_workflow_or_404,
|
|
16
|
+
_get_project_check_access,
|
|
19
17
|
)
|
|
18
|
+
from fractal_server.app.routes.api.v2._aux_functions import _get_workflow_or_404
|
|
20
19
|
from fractal_server.app.routes.api.v2._aux_functions import (
|
|
21
20
|
_get_workflowtask_or_404,
|
|
22
21
|
)
|
|
22
|
+
from fractal_server.app.schemas.v2.sharing import ProjectPermissions
|
|
23
23
|
from fractal_server.logger import set_logger
|
|
24
|
-
|
|
24
|
+
from fractal_server.zip_tools import _read_single_file_from_zip
|
|
25
25
|
|
|
26
26
|
logger = set_logger(__name__)
|
|
27
27
|
|
|
@@ -66,27 +66,51 @@ async def get_history_run_or_404(
|
|
|
66
66
|
|
|
67
67
|
def read_log_file(
|
|
68
68
|
*,
|
|
69
|
-
|
|
70
|
-
wftask: WorkflowTaskV2,
|
|
69
|
+
task_name: str,
|
|
71
70
|
dataset_id: int,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
f"{dataset_id} are not available."
|
|
81
|
-
)
|
|
71
|
+
logfile: str,
|
|
72
|
+
job_working_dir: str,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Returns the contents of a Job's log file, either directly from the working
|
|
76
|
+
directory or from the corresponding ZIP archive.
|
|
77
|
+
|
|
78
|
+
The function first checks if `logfile` exists on disk.
|
|
82
79
|
|
|
80
|
+
If not, it checks if the Job working directory has been zipped and tries to
|
|
81
|
+
read `logfile` from within the archive.
|
|
82
|
+
(Note: it is assumed that `logfile` is relative to `job_working_dir`)
|
|
83
|
+
"""
|
|
84
|
+
archive_path = os.path.normpath(job_working_dir) + ".zip"
|
|
83
85
|
try:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
if Path(logfile).exists():
|
|
87
|
+
with open(logfile) as f:
|
|
88
|
+
return f.read()
|
|
89
|
+
elif Path(archive_path).exists():
|
|
90
|
+
relative_logfile = (
|
|
91
|
+
Path(logfile).relative_to(job_working_dir).as_posix()
|
|
92
|
+
)
|
|
93
|
+
return _read_single_file_from_zip(
|
|
94
|
+
file_path=relative_logfile, archive_path=archive_path
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
else:
|
|
98
|
+
logger.error(
|
|
99
|
+
f"Error while retrieving logs for {logfile=} and "
|
|
100
|
+
f"{archive_path=}: both files do not exist."
|
|
101
|
+
)
|
|
102
|
+
return (
|
|
103
|
+
f"Logs for task '{task_name}' in dataset "
|
|
104
|
+
f"{dataset_id} are not available."
|
|
105
|
+
)
|
|
86
106
|
except Exception as e:
|
|
107
|
+
logger.error(
|
|
108
|
+
f"Error while retrieving logs for {logfile=} and {archive_path=}. "
|
|
109
|
+
f"Original error: {str(e)}"
|
|
110
|
+
)
|
|
87
111
|
return (
|
|
88
|
-
f"Error while retrieving logs for task '{
|
|
89
|
-
f"in dataset {dataset_id}.
|
|
112
|
+
f"Error while retrieving logs for task '{task_name}' "
|
|
113
|
+
f"in dataset {dataset_id}."
|
|
90
114
|
)
|
|
91
115
|
|
|
92
116
|
|
|
@@ -96,6 +120,7 @@ async def _verify_workflow_and_dataset_access(
|
|
|
96
120
|
workflow_id: int,
|
|
97
121
|
dataset_id: int,
|
|
98
122
|
user_id: int,
|
|
123
|
+
required_permissions: ProjectPermissions,
|
|
99
124
|
db: AsyncSession,
|
|
100
125
|
) -> dict[Literal["dataset", "workflow"], DatasetV2 | WorkflowV2]:
|
|
101
126
|
"""
|
|
@@ -108,9 +133,10 @@ async def _verify_workflow_and_dataset_access(
|
|
|
108
133
|
user_id:
|
|
109
134
|
db:
|
|
110
135
|
"""
|
|
111
|
-
await
|
|
136
|
+
await _get_project_check_access(
|
|
112
137
|
project_id=project_id,
|
|
113
138
|
user_id=user_id,
|
|
139
|
+
required_permissions=required_permissions,
|
|
114
140
|
db=db,
|
|
115
141
|
)
|
|
116
142
|
workflow = await _get_workflow_or_404(
|
|
@@ -135,12 +161,13 @@ async def _verify_workflow_and_dataset_access(
|
|
|
135
161
|
return dict(dataset=dataset, workflow=workflow)
|
|
136
162
|
|
|
137
163
|
|
|
138
|
-
async def
|
|
164
|
+
async def get_wftask_check_access(
|
|
139
165
|
*,
|
|
140
166
|
project_id: int,
|
|
141
167
|
dataset_id: int,
|
|
142
168
|
workflowtask_id: int,
|
|
143
169
|
user_id: int,
|
|
170
|
+
required_permissions: ProjectPermissions,
|
|
144
171
|
db: AsyncSession,
|
|
145
172
|
) -> WorkflowTaskV2:
|
|
146
173
|
"""
|
|
@@ -161,6 +188,7 @@ async def get_wftask_check_owner(
|
|
|
161
188
|
project_id=project_id,
|
|
162
189
|
dataset_id=dataset_id,
|
|
163
190
|
workflow_id=wftask.workflow_id,
|
|
191
|
+
required_permissions=required_permissions,
|
|
164
192
|
user_id=user_id,
|
|
165
193
|
db=db,
|
|
166
194
|
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
from fastapi import status
|
|
3
|
+
from sqlmodel import select
|
|
4
|
+
|
|
5
|
+
from fractal_server.app.db import AsyncSession
|
|
6
|
+
from fractal_server.app.models import UserOAuth
|
|
7
|
+
from fractal_server.app.models.v2 import LinkUserProjectV2
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def raise_403_if_not_owner(
|
|
11
|
+
*,
|
|
12
|
+
user_id: int,
|
|
13
|
+
project_id: int,
|
|
14
|
+
db: AsyncSession,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Raises 403 if User[`user_id`] is not owner of Project[`project_id`],
|
|
18
|
+
regardless of whether the User or Project exists.
|
|
19
|
+
"""
|
|
20
|
+
res = await db.execute(
|
|
21
|
+
select(LinkUserProjectV2)
|
|
22
|
+
.where(LinkUserProjectV2.project_id == project_id)
|
|
23
|
+
.where(LinkUserProjectV2.user_id == user_id)
|
|
24
|
+
.where(LinkUserProjectV2.is_owner.is_(True))
|
|
25
|
+
)
|
|
26
|
+
link = res.scalars().one_or_none()
|
|
27
|
+
if link is None:
|
|
28
|
+
raise HTTPException(
|
|
29
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
30
|
+
detail="Current user is not the project owner.",
|
|
31
|
+
)
|
|
32
|
+
return link
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def get_link_or_404(
|
|
36
|
+
*, user_id: int, project_id: int, db: AsyncSession
|
|
37
|
+
) -> LinkUserProjectV2:
|
|
38
|
+
"""
|
|
39
|
+
Raises 404 if User[`user_id`] is not linked to Project[`project_id`],
|
|
40
|
+
regardless of whether the User or Project exists.
|
|
41
|
+
"""
|
|
42
|
+
link = await db.get(LinkUserProjectV2, (project_id, user_id))
|
|
43
|
+
if link is None:
|
|
44
|
+
raise HTTPException(
|
|
45
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
46
|
+
detail="User is not linked to project.",
|
|
47
|
+
)
|
|
48
|
+
return link
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def get_pending_invitation_or_404(
|
|
52
|
+
*, user_id: int, project_id: int, db: AsyncSession
|
|
53
|
+
) -> LinkUserProjectV2:
|
|
54
|
+
"""
|
|
55
|
+
Raises 404 if User[`user_id`] has not a pending invitation to
|
|
56
|
+
Project[`project_id`], regardless of whether the User or Project exists.
|
|
57
|
+
"""
|
|
58
|
+
link = await get_link_or_404(user_id=user_id, project_id=project_id, db=db)
|
|
59
|
+
if link.is_verified:
|
|
60
|
+
raise HTTPException(
|
|
61
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
62
|
+
detail="No pending invitation for user on this project.",
|
|
63
|
+
)
|
|
64
|
+
return link
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def raise_422_if_link_exists(
|
|
68
|
+
*, user_id: int, project_id: int, db: AsyncSession
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Raises 422 if User[`user_id`] is linked Project[`project_id`], regardless
|
|
72
|
+
of whether the User or Project exists.
|
|
73
|
+
"""
|
|
74
|
+
link = await db.get(LinkUserProjectV2, (project_id, user_id))
|
|
75
|
+
if link is not None:
|
|
76
|
+
raise HTTPException(
|
|
77
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
78
|
+
detail="User is already associated to project.",
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def get_user_id_from_email_or_404(
|
|
84
|
+
*, user_email: str, db: AsyncSession
|
|
85
|
+
) -> int:
|
|
86
|
+
"""
|
|
87
|
+
Raises 404 if there is no User with email `user_email`.
|
|
88
|
+
"""
|
|
89
|
+
res = await db.execute(
|
|
90
|
+
select(UserOAuth.id).where(UserOAuth.email == user_email)
|
|
91
|
+
)
|
|
92
|
+
user_id = res.scalar_one_or_none()
|
|
93
|
+
if user_id is None:
|
|
94
|
+
raise HTTPException(
|
|
95
|
+
status_code=status.HTTP_404_NOT_FOUND, detail="User not found."
|
|
96
|
+
)
|
|
97
|
+
return user_id
|