fractal-server 2.17.1a1__py3-none-any.whl → 2.17.2__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 +19 -18
- fractal_server/app/db/__init__.py +3 -3
- fractal_server/app/models/__init__.py +1 -0
- fractal_server/app/models/linkuserproject.py +3 -1
- fractal_server/app/models/security.py +21 -3
- 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 +4 -0
- 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 +4 -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 +1 -2
- fractal_server/app/routes/admin/v2/accounting.py +1 -1
- fractal_server/app/routes/admin/v2/job.py +9 -9
- 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/task.py +28 -18
- fractal_server/app/routes/admin/v2/task_group.py +0 -1
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +1 -2
- fractal_server/app/routes/api/__init__.py +1 -0
- fractal_server/app/routes/api/v2/__init__.py +5 -6
- fractal_server/app/routes/api/v2/_aux_functions.py +70 -63
- fractal_server/app/routes/api/v2/_aux_functions_history.py +43 -20
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +2 -4
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +5 -7
- fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +1 -2
- fractal_server/app/routes/api/v2/dataset.py +13 -32
- fractal_server/app/routes/api/v2/history.py +35 -21
- fractal_server/app/routes/api/v2/images.py +3 -2
- fractal_server/app/routes/api/v2/job.py +17 -14
- fractal_server/app/routes/api/v2/pre_submission_checks.py +5 -4
- fractal_server/app/routes/api/v2/project.py +22 -17
- fractal_server/app/routes/api/v2/status_legacy.py +12 -11
- fractal_server/app/routes/api/v2/submit.py +11 -12
- fractal_server/app/routes/api/v2/task.py +4 -3
- fractal_server/app/routes/api/v2/task_collection.py +28 -30
- fractal_server/app/routes/api/v2/task_collection_custom.py +8 -7
- fractal_server/app/routes/api/v2/task_collection_pixi.py +1 -2
- fractal_server/app/routes/api/v2/task_group.py +7 -6
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +6 -6
- fractal_server/app/routes/api/v2/task_version_update.py +13 -12
- fractal_server/app/routes/api/v2/workflow.py +14 -31
- fractal_server/app/routes/api/v2/workflow_import.py +17 -19
- fractal_server/app/routes/api/v2/workflowtask.py +10 -12
- fractal_server/app/routes/auth/__init__.py +1 -3
- fractal_server/app/routes/auth/_aux_auth.py +1 -2
- fractal_server/app/routes/auth/current_user.py +4 -5
- fractal_server/app/routes/auth/group.py +7 -5
- 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/users.py +10 -10
- 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 +3 -3
- fractal_server/app/schemas/v2/accounting.py +11 -0
- fractal_server/app/schemas/v2/dataset.py +28 -4
- fractal_server/app/schemas/v2/dumps.py +1 -0
- fractal_server/app/schemas/v2/manifest.py +4 -3
- fractal_server/app/schemas/v2/profile.py +53 -2
- fractal_server/app/schemas/v2/resource.py +109 -13
- fractal_server/app/schemas/v2/task.py +0 -1
- fractal_server/app/schemas/v2/task_collection.py +1 -1
- fractal_server/app/schemas/v2/workflowtask.py +4 -3
- fractal_server/app/security/__init__.py +4 -7
- fractal_server/app/security/signup_email.py +4 -5
- fractal_server/config/_data.py +36 -25
- fractal_server/config/_database.py +19 -20
- fractal_server/config/_email.py +30 -38
- fractal_server/config/_main.py +33 -52
- fractal_server/config/_oauth.py +17 -21
- fractal_server/exceptions.py +4 -0
- fractal_server/images/models.py +3 -3
- fractal_server/images/status_tools.py +4 -2
- fractal_server/logger.py +1 -1
- fractal_server/main.py +4 -3
- 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/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/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/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/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 +37 -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 +5 -10
- 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 +1 -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 +9 -4
- fractal_server/runner/v2/_slurm_ssh.py +11 -5
- fractal_server/runner/v2/_slurm_sudo.py +11 -5
- fractal_server/runner/v2/db_tools.py +0 -1
- fractal_server/runner/v2/deduplicate_list.py +2 -1
- fractal_server/runner/v2/runner.py +11 -14
- fractal_server/runner/v2/runner_functions.py +11 -14
- fractal_server/runner/v2/submit_workflow.py +7 -6
- 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 +1 -1
- fractal_server/tasks/v2/local/collect.py +10 -12
- fractal_server/tasks/v2/local/collect_pixi.py +9 -10
- fractal_server/tasks/v2/local/deactivate.py +7 -8
- fractal_server/tasks/v2/local/deactivate_pixi.py +4 -4
- fractal_server/tasks/v2/local/delete.py +1 -3
- fractal_server/tasks/v2/local/reactivate.py +7 -7
- fractal_server/tasks/v2/local/reactivate_pixi.py +7 -7
- fractal_server/tasks/v2/ssh/_utils.py +3 -3
- fractal_server/tasks/v2/ssh/collect.py +14 -19
- fractal_server/tasks/v2/ssh/collect_pixi.py +17 -19
- fractal_server/tasks/v2/ssh/deactivate.py +10 -8
- fractal_server/tasks/v2/ssh/deactivate_pixi.py +6 -5
- fractal_server/tasks/v2/ssh/delete.py +7 -5
- fractal_server/tasks/v2/ssh/reactivate.py +11 -11
- fractal_server/tasks/v2/ssh/reactivate_pixi.py +8 -9
- 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 +3 -3
- fractal_server/tasks/v2/utils_package_names.py +1 -2
- fractal_server/tasks/v2/utils_pixi.py +1 -3
- fractal_server/types/__init__.py +76 -1
- fractal_server/types/validators/_common_validators.py +1 -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.17.2.dist-info}/METADATA +1 -1
- fractal_server-2.17.2.dist-info/RECORD +265 -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.17.2.dist-info}/WHEEL +0 -0
- {fractal_server-2.17.1a1.dist-info → fractal_server-2.17.2.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.17.1a1.dist-info → fractal_server-2.17.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,14 +6,8 @@ from fastapi import HTTPException
|
|
|
6
6
|
from fastapi import Response
|
|
7
7
|
from fastapi import status
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from ._aux_functions import _get_workflow_check_owner
|
|
12
|
-
from ._aux_functions import _get_workflow_task_check_owner
|
|
13
|
-
from ._aux_functions import _workflow_has_submitted_job
|
|
14
|
-
from ._aux_functions import _workflow_insert_task
|
|
15
|
-
from ._aux_functions_tasks import _check_type_filters_compatibility
|
|
16
|
-
from ._aux_functions_tasks import _get_task_read_access
|
|
9
|
+
from fractal_server.app.db import AsyncSession
|
|
10
|
+
from fractal_server.app.db import get_async_db
|
|
17
11
|
from fractal_server.app.models import UserOAuth
|
|
18
12
|
from fractal_server.app.routes.auth import current_user_act_ver_prof
|
|
19
13
|
from fractal_server.app.schemas.v2 import TaskType
|
|
@@ -21,6 +15,13 @@ from fractal_server.app.schemas.v2 import WorkflowTaskCreateV2
|
|
|
21
15
|
from fractal_server.app.schemas.v2 import WorkflowTaskReadV2
|
|
22
16
|
from fractal_server.app.schemas.v2 import WorkflowTaskUpdateV2
|
|
23
17
|
|
|
18
|
+
from ._aux_functions import _get_workflow_check_owner
|
|
19
|
+
from ._aux_functions import _get_workflow_task_check_owner
|
|
20
|
+
from ._aux_functions import _workflow_has_submitted_job
|
|
21
|
+
from ._aux_functions import _workflow_insert_task
|
|
22
|
+
from ._aux_functions_tasks import _check_type_filters_compatibility
|
|
23
|
+
from ._aux_functions_tasks import _get_task_read_access
|
|
24
|
+
|
|
24
25
|
router = APIRouter()
|
|
25
26
|
|
|
26
27
|
|
|
@@ -63,10 +64,7 @@ async def create_workflowtask(
|
|
|
63
64
|
),
|
|
64
65
|
)
|
|
65
66
|
elif task.type == TaskType.NON_PARALLEL:
|
|
66
|
-
if
|
|
67
|
-
wftask.meta_parallel is not None
|
|
68
|
-
or wftask.args_parallel is not None
|
|
69
|
-
):
|
|
67
|
+
if wftask.meta_parallel is not None or wftask.args_parallel is not None:
|
|
70
68
|
raise HTTPException(
|
|
71
69
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
72
70
|
detail=(
|
|
@@ -12,7 +12,6 @@ from fractal_server.app.security import get_user_manager
|
|
|
12
12
|
from fractal_server.config import get_settings
|
|
13
13
|
from fractal_server.syringe import Inject
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
bearer_transport = BearerTransport(tokenUrl="/auth/token/login")
|
|
17
16
|
cookie_transport = CookieTransport(cookie_samesite="none")
|
|
18
17
|
|
|
@@ -71,8 +70,7 @@ async def current_user_act_ver_prof(
|
|
|
71
70
|
raise HTTPException(
|
|
72
71
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
73
72
|
detail=(
|
|
74
|
-
f"Forbidden access "
|
|
75
|
-
f"({user.is_verified=} {user.profile_id=})."
|
|
73
|
+
f"Forbidden access ({user.is_verified=} {user.profile_id=})."
|
|
76
74
|
),
|
|
77
75
|
)
|
|
78
76
|
return user
|
|
@@ -13,7 +13,6 @@ from fractal_server.config import get_settings
|
|
|
13
13
|
from fractal_server.logger import set_logger
|
|
14
14
|
from fractal_server.syringe import Inject
|
|
15
15
|
|
|
16
|
-
|
|
17
16
|
logger = set_logger(__name__)
|
|
18
17
|
|
|
19
18
|
|
|
@@ -36,7 +35,7 @@ async def _get_single_user_with_groups(
|
|
|
36
35
|
|
|
37
36
|
stm_groups = (
|
|
38
37
|
select(UserGroup)
|
|
39
|
-
.join(LinkUserGroup)
|
|
38
|
+
.join(LinkUserGroup, LinkUserGroup.group_id == UserGroup.id)
|
|
40
39
|
.where(LinkUserGroup.user_id == user.id)
|
|
41
40
|
.order_by(asc(LinkUserGroup.timestamp_created))
|
|
42
41
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Definition of `/auth/current-user/` endpoints
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import os
|
|
5
6
|
|
|
6
7
|
from fastapi import APIRouter
|
|
@@ -23,8 +24,8 @@ from fractal_server.app.schemas import UserProfileInfo
|
|
|
23
24
|
from fractal_server.app.schemas.user import UserRead
|
|
24
25
|
from fractal_server.app.schemas.user import UserUpdate
|
|
25
26
|
from fractal_server.app.schemas.user import UserUpdateStrict
|
|
26
|
-
from fractal_server.app.security import get_user_manager
|
|
27
27
|
from fractal_server.app.security import UserManager
|
|
28
|
+
from fractal_server.app.security import get_user_manager
|
|
28
29
|
from fractal_server.config import DataAuthScheme
|
|
29
30
|
from fractal_server.config import get_data_settings
|
|
30
31
|
from fractal_server.syringe import Inject
|
|
@@ -87,9 +88,8 @@ async def get_current_user_profile_info(
|
|
|
87
88
|
) -> UserProfileInfo:
|
|
88
89
|
stm = (
|
|
89
90
|
select(Resource, Profile)
|
|
90
|
-
.join(UserOAuth)
|
|
91
|
+
.join(UserOAuth, Profile.id == UserOAuth.profile_id)
|
|
91
92
|
.where(Resource.id == Profile.resource_id)
|
|
92
|
-
.where(Profile.id == UserOAuth.profile_id)
|
|
93
93
|
.where(UserOAuth.id == current_user.id)
|
|
94
94
|
)
|
|
95
95
|
res = await db.execute(stm)
|
|
@@ -146,8 +146,7 @@ async def get_current_user_allowed_viewer_paths(
|
|
|
146
146
|
# Returns the union of `viewer_paths` for all user's groups
|
|
147
147
|
cmd = (
|
|
148
148
|
select(UserGroup.viewer_paths)
|
|
149
|
-
.join(LinkUserGroup)
|
|
150
|
-
.where(LinkUserGroup.group_id == UserGroup.id)
|
|
149
|
+
.join(LinkUserGroup, LinkUserGroup.group_id == UserGroup.id)
|
|
151
150
|
.where(LinkUserGroup.user_id == current_user.id)
|
|
152
151
|
)
|
|
153
152
|
res = await db.execute(cmd)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Definition of `/auth/group/` routes
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from fastapi import APIRouter
|
|
5
6
|
from fastapi import Depends
|
|
6
7
|
from fastapi import HTTPException
|
|
@@ -9,11 +10,6 @@ from fastapi import status
|
|
|
9
10
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
11
|
from sqlmodel import select
|
|
11
12
|
|
|
12
|
-
from . import current_superuser_act
|
|
13
|
-
from ._aux_auth import _get_default_usergroup_id_or_none
|
|
14
|
-
from ._aux_auth import _get_single_usergroup_with_user_ids
|
|
15
|
-
from ._aux_auth import _user_or_404
|
|
16
|
-
from ._aux_auth import _usergroup_or_404
|
|
17
13
|
from fractal_server.app.db import get_async_db
|
|
18
14
|
from fractal_server.app.models import LinkUserGroup
|
|
19
15
|
from fractal_server.app.models import UserGroup
|
|
@@ -25,6 +21,12 @@ from fractal_server.config import get_settings
|
|
|
25
21
|
from fractal_server.logger import set_logger
|
|
26
22
|
from fractal_server.syringe import Inject
|
|
27
23
|
|
|
24
|
+
from . import current_superuser_act
|
|
25
|
+
from ._aux_auth import _get_default_usergroup_id_or_none
|
|
26
|
+
from ._aux_auth import _get_single_usergroup_with_user_ids
|
|
27
|
+
from ._aux_auth import _user_or_404
|
|
28
|
+
from ._aux_auth import _usergroup_or_404
|
|
29
|
+
|
|
28
30
|
logger = set_logger(__name__)
|
|
29
31
|
|
|
30
32
|
|
|
@@ -4,13 +4,14 @@ from httpx_oauth.clients.google import GoogleOAuth2
|
|
|
4
4
|
from httpx_oauth.clients.openid import OpenID
|
|
5
5
|
from httpx_oauth.clients.openid import OpenIDConfigurationError
|
|
6
6
|
|
|
7
|
-
from . import
|
|
8
|
-
from . import fastapi_users
|
|
7
|
+
from fractal_server.config import OAuthSettings
|
|
9
8
|
from fractal_server.config import get_oauth_settings
|
|
10
9
|
from fractal_server.config import get_settings
|
|
11
|
-
from fractal_server.config import OAuthSettings
|
|
12
10
|
from fractal_server.syringe import Inject
|
|
13
11
|
|
|
12
|
+
from . import cookie_backend
|
|
13
|
+
from . import fastapi_users
|
|
14
|
+
|
|
14
15
|
|
|
15
16
|
def _create_client_github(cfg: OAuthSettings) -> GitHubOAuth2:
|
|
16
17
|
return GitHubOAuth2(
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Definition of `/auth/register/` routes.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from fastapi import APIRouter
|
|
5
6
|
from fastapi import Depends
|
|
6
7
|
|
|
8
|
+
from fractal_server.app.schemas.user import UserCreate
|
|
9
|
+
from fractal_server.app.schemas.user import UserRead
|
|
10
|
+
|
|
7
11
|
from . import current_superuser_act
|
|
8
12
|
from . import fastapi_users
|
|
9
|
-
from ...schemas.user import UserCreate
|
|
10
|
-
from ...schemas.user import UserRead
|
|
11
13
|
|
|
12
14
|
router_register = APIRouter()
|
|
13
15
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Definition of `/auth/users/` routes
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from fastapi import APIRouter
|
|
5
6
|
from fastapi import Depends
|
|
6
7
|
from fastapi import HTTPException
|
|
@@ -11,24 +12,24 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
11
12
|
from sqlmodel import func
|
|
12
13
|
from sqlmodel import select
|
|
13
14
|
|
|
14
|
-
from . import
|
|
15
|
-
from ...db import get_async_db
|
|
16
|
-
from ...schemas.user import UserRead
|
|
17
|
-
from ...schemas.user import UserUpdate
|
|
18
|
-
from ._aux_auth import _get_default_usergroup_id_or_none
|
|
19
|
-
from ._aux_auth import _get_single_user_with_groups
|
|
15
|
+
from fractal_server.app.db import get_async_db
|
|
20
16
|
from fractal_server.app.models import LinkUserGroup
|
|
21
17
|
from fractal_server.app.models import UserGroup
|
|
22
18
|
from fractal_server.app.models import UserOAuth
|
|
23
19
|
from fractal_server.app.models.v2 import Profile
|
|
24
20
|
from fractal_server.app.routes.auth._aux_auth import _user_or_404
|
|
21
|
+
from fractal_server.app.schemas.user import UserRead
|
|
22
|
+
from fractal_server.app.schemas.user import UserUpdate
|
|
25
23
|
from fractal_server.app.schemas.user import UserUpdateGroups
|
|
26
|
-
from fractal_server.app.security import get_user_manager
|
|
27
24
|
from fractal_server.app.security import UserManager
|
|
25
|
+
from fractal_server.app.security import get_user_manager
|
|
28
26
|
from fractal_server.config import get_settings
|
|
29
27
|
from fractal_server.logger import set_logger
|
|
30
28
|
from fractal_server.syringe import Inject
|
|
31
29
|
|
|
30
|
+
from . import current_superuser_act
|
|
31
|
+
from ._aux_auth import _get_default_usergroup_id_or_none
|
|
32
|
+
from ._aux_auth import _get_single_user_with_groups
|
|
32
33
|
|
|
33
34
|
router_users = APIRouter()
|
|
34
35
|
|
|
@@ -197,13 +198,12 @@ async def set_user_groups(
|
|
|
197
198
|
# Remove/create links as needed
|
|
198
199
|
for link in links_to_remove:
|
|
199
200
|
logger.info(
|
|
200
|
-
f"Removing LinkUserGroup with {link.user_id=} "
|
|
201
|
-
f"and {link.group_id=}."
|
|
201
|
+
f"Removing LinkUserGroup with {link.user_id=} and {link.group_id=}."
|
|
202
202
|
)
|
|
203
203
|
await db.delete(link)
|
|
204
204
|
for group_id in ids_links_to_add:
|
|
205
205
|
logger.info(
|
|
206
|
-
f"Creating new LinkUserGroup with {user_id=}
|
|
206
|
+
f"Creating new LinkUserGroup with {user_id=} and {group_id=}."
|
|
207
207
|
)
|
|
208
208
|
db.add(LinkUserGroup(user_id=user_id, group_id=group_id))
|
|
209
209
|
await db.commit()
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from fastapi import HTTPException
|
|
2
2
|
from fastapi import status
|
|
3
3
|
|
|
4
|
-
from ....config import get_settings
|
|
5
|
-
from ....syringe import Inject
|
|
6
4
|
from fractal_server.app.schemas.v2 import ResourceType
|
|
5
|
+
from fractal_server.config import get_settings
|
|
6
|
+
from fractal_server.syringe import Inject
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _backend_supports_shutdown(backend: str) -> bool:
|
|
@@ -4,8 +4,8 @@ from typing import TypeVar
|
|
|
4
4
|
from fastapi import HTTPException
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
from pydantic import Field
|
|
7
|
-
from pydantic import model_validator
|
|
8
7
|
from pydantic import ValidationError
|
|
8
|
+
from pydantic import model_validator
|
|
9
9
|
|
|
10
10
|
T = TypeVar("T")
|
|
11
11
|
|
|
@@ -74,9 +74,9 @@ class UserUpdate(schemas.BaseUserUpdate):
|
|
|
74
74
|
is_superuser: bool = None
|
|
75
75
|
is_verified: bool = None
|
|
76
76
|
profile_id: int | None = None
|
|
77
|
-
project_dir: Annotated[
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
project_dir: Annotated[AbsolutePathStr, AfterValidator(_validate_cmd)] = (
|
|
78
|
+
None
|
|
79
|
+
)
|
|
80
80
|
slurm_accounts: ListUniqueNonEmptyString = None
|
|
81
81
|
|
|
82
82
|
|
|
@@ -6,6 +6,17 @@ from pydantic.types import AwareDatetime
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class AccountingRecordRead(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
AccountingRecordRead
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
id:
|
|
14
|
+
user_id:
|
|
15
|
+
timestamp:
|
|
16
|
+
num_tasks:
|
|
17
|
+
num_new_images:
|
|
18
|
+
"""
|
|
19
|
+
|
|
9
20
|
id: int
|
|
10
21
|
user_id: int
|
|
11
22
|
timestamp: AwareDatetime
|
|
@@ -8,22 +8,38 @@ from pydantic.types import AwareDatetime
|
|
|
8
8
|
|
|
9
9
|
from fractal_server.app.schemas.v2.project import ProjectReadV2
|
|
10
10
|
from fractal_server.images import SingleImage
|
|
11
|
-
from fractal_server.types import AttributeFilters
|
|
12
11
|
from fractal_server.types import NonEmptyStr
|
|
13
12
|
from fractal_server.types import ZarrDirStr
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class DatasetCreateV2(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
DatasetCreateV2
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
name:
|
|
21
|
+
zarr_dir:
|
|
22
|
+
"""
|
|
23
|
+
|
|
17
24
|
model_config = ConfigDict(extra="forbid")
|
|
18
25
|
|
|
19
26
|
name: NonEmptyStr
|
|
20
|
-
|
|
21
27
|
zarr_dir: ZarrDirStr | None = None
|
|
22
28
|
|
|
23
|
-
attribute_filters: AttributeFilters = Field(default_factory=dict)
|
|
24
|
-
|
|
25
29
|
|
|
26
30
|
class DatasetReadV2(BaseModel):
|
|
31
|
+
"""
|
|
32
|
+
DatasetReadV2
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
id:
|
|
36
|
+
name:
|
|
37
|
+
project_id:
|
|
38
|
+
project:
|
|
39
|
+
timestamp_created:
|
|
40
|
+
zarr_dir:
|
|
41
|
+
"""
|
|
42
|
+
|
|
27
43
|
id: int
|
|
28
44
|
name: str
|
|
29
45
|
|
|
@@ -40,6 +56,14 @@ class DatasetReadV2(BaseModel):
|
|
|
40
56
|
|
|
41
57
|
|
|
42
58
|
class DatasetUpdateV2(BaseModel):
|
|
59
|
+
"""
|
|
60
|
+
DatasetUpdateV2
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
name:
|
|
64
|
+
zarr_dir:
|
|
65
|
+
"""
|
|
66
|
+
|
|
43
67
|
model_config = ConfigDict(extra="forbid")
|
|
44
68
|
|
|
45
69
|
name: NonEmptyStr = None
|
|
@@ -4,11 +4,12 @@ from pydantic import BaseModel
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
from pydantic import model_validator
|
|
6
6
|
|
|
7
|
-
from .task import TaskType
|
|
8
7
|
from fractal_server.types import DictStrAny
|
|
9
8
|
from fractal_server.types import HttpUrlStr
|
|
10
9
|
from fractal_server.types import NonEmptyStr
|
|
11
10
|
|
|
11
|
+
from .task import TaskType
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
class TaskManifestV2(BaseModel):
|
|
14
15
|
"""
|
|
@@ -119,9 +120,9 @@ class ManifestV2(BaseModel):
|
|
|
119
120
|
manifests as the schema evolves. This is for instance used by
|
|
120
121
|
Fractal to determine which subclass of the present base class needs
|
|
121
122
|
be used to read and validate the input.
|
|
122
|
-
task_list
|
|
123
|
+
task_list:
|
|
123
124
|
The list of tasks, represented as specified by subclasses of the
|
|
124
|
-
_TaskManifestBase (a.k.a. TaskManifestType)
|
|
125
|
+
`_TaskManifestBase` (a.k.a. `TaskManifestType`)
|
|
125
126
|
has_args_schemas:
|
|
126
127
|
`True` if the manifest includes JSON Schemas for the arguments of
|
|
127
128
|
each task.
|
|
@@ -6,12 +6,21 @@ from pydantic import Discriminator
|
|
|
6
6
|
from pydantic import Tag
|
|
7
7
|
from pydantic import validate_call
|
|
8
8
|
|
|
9
|
-
from .resource import ResourceType
|
|
10
9
|
from fractal_server.types import AbsolutePathStr
|
|
11
10
|
from fractal_server.types import NonEmptyStr
|
|
12
11
|
|
|
12
|
+
from .resource import ResourceType
|
|
13
|
+
|
|
13
14
|
|
|
14
15
|
class ValidProfileLocal(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Valid local profile.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
name: Profile name.
|
|
21
|
+
resource_type: Type of the corresponding resource.
|
|
22
|
+
"""
|
|
23
|
+
|
|
15
24
|
name: NonEmptyStr
|
|
16
25
|
resource_type: ResourceType
|
|
17
26
|
username: None = None
|
|
@@ -21,6 +30,17 @@ class ValidProfileLocal(BaseModel):
|
|
|
21
30
|
|
|
22
31
|
|
|
23
32
|
class ValidProfileSlurmSudo(BaseModel):
|
|
33
|
+
"""
|
|
34
|
+
Valid SLURM/sudo profile.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
name: Profile name.
|
|
38
|
+
resource_type: Type of the corresponding resource.
|
|
39
|
+
username:
|
|
40
|
+
SLURM user to impersonate (e.g. as in
|
|
41
|
+
`sudo -u username sbatch /some/script.sh`).
|
|
42
|
+
"""
|
|
43
|
+
|
|
24
44
|
name: NonEmptyStr
|
|
25
45
|
resource_type: ResourceType
|
|
26
46
|
username: NonEmptyStr
|
|
@@ -30,6 +50,23 @@ class ValidProfileSlurmSudo(BaseModel):
|
|
|
30
50
|
|
|
31
51
|
|
|
32
52
|
class ValidProfileSlurmSSH(BaseModel):
|
|
53
|
+
"""
|
|
54
|
+
Valid SLURM/sudo profile.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
name: Profile name.
|
|
58
|
+
resource_type: Type of the corresponding resource.
|
|
59
|
+
username:
|
|
60
|
+
SLURM user to impersonate (e.g. as in
|
|
61
|
+
`ssh username@cluster sbatch /some/script.sh`).
|
|
62
|
+
ssh_key_path:
|
|
63
|
+
Local path of SSH private key for user `username`.
|
|
64
|
+
tasks_remote_dir:
|
|
65
|
+
Base folder for task environments on the remote SLURM cluster.
|
|
66
|
+
jobs_remote_dir:
|
|
67
|
+
Base folder for job directories on the remote SLURM cluster.
|
|
68
|
+
"""
|
|
69
|
+
|
|
33
70
|
name: NonEmptyStr
|
|
34
71
|
resource_type: ResourceType
|
|
35
72
|
username: NonEmptyStr
|
|
@@ -54,6 +91,20 @@ ProfileCreate = Annotated[
|
|
|
54
91
|
|
|
55
92
|
|
|
56
93
|
class ProfileRead(BaseModel):
|
|
94
|
+
"""
|
|
95
|
+
Profile schema for GET endpoints.
|
|
96
|
+
|
|
97
|
+
Attributes:
|
|
98
|
+
id:
|
|
99
|
+
name:
|
|
100
|
+
resource_id:
|
|
101
|
+
resource_type:
|
|
102
|
+
username:
|
|
103
|
+
ssh_key_path:
|
|
104
|
+
jobs_remote_dir:
|
|
105
|
+
tasks_remote_dir:
|
|
106
|
+
"""
|
|
107
|
+
|
|
57
108
|
id: int
|
|
58
109
|
name: str
|
|
59
110
|
resource_id: int
|
|
@@ -69,7 +120,7 @@ def cast_serialize_profile(_data: ProfileCreate) -> dict[str, Any]:
|
|
|
69
120
|
"""
|
|
70
121
|
Cast/serialize round-trip for `Profile` data.
|
|
71
122
|
|
|
72
|
-
We use `@validate_call` because `
|
|
123
|
+
We use `@validate_call` because `ProfileCreate` is a `Union` type and it
|
|
73
124
|
cannot be instantiated directly.
|
|
74
125
|
|
|
75
126
|
Return:
|
|
@@ -7,8 +7,8 @@ from typing import Self
|
|
|
7
7
|
from pydantic import AfterValidator
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
from pydantic import Discriminator
|
|
10
|
-
from pydantic import model_validator
|
|
11
10
|
from pydantic import Tag
|
|
11
|
+
from pydantic import model_validator
|
|
12
12
|
from pydantic import validate_call
|
|
13
13
|
from pydantic.types import AwareDatetime
|
|
14
14
|
|
|
@@ -21,23 +21,46 @@ from fractal_server.types import NonEmptyStr
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class ResourceType(StrEnum):
|
|
24
|
+
"""
|
|
25
|
+
Enum for the possible resource types.
|
|
26
|
+
"""
|
|
27
|
+
|
|
24
28
|
SLURM_SUDO = "slurm_sudo"
|
|
29
|
+
"""
|
|
30
|
+
Enum entry for resource type `slurm_sudo`.
|
|
31
|
+
"""
|
|
32
|
+
|
|
25
33
|
SLURM_SSH = "slurm_ssh"
|
|
34
|
+
"""
|
|
35
|
+
Enum entry for resource type `slurm_ssh`.
|
|
36
|
+
"""
|
|
37
|
+
|
|
26
38
|
LOCAL = "local"
|
|
39
|
+
"""
|
|
40
|
+
Enum entry for resource type `local`.
|
|
41
|
+
"""
|
|
27
42
|
|
|
28
43
|
|
|
29
44
|
def cast_serialize_pixi_settings(
|
|
30
|
-
|
|
45
|
+
value: dict[NonEmptyStr, Any],
|
|
31
46
|
) -> dict[NonEmptyStr, Any]:
|
|
32
47
|
"""
|
|
33
|
-
|
|
48
|
+
Cast/serialize round trip for `tasks_pixi_config` through the
|
|
49
|
+
`TasksPixiSettings` schema.
|
|
50
|
+
|
|
51
|
+
Arguments:
|
|
52
|
+
value: Current `tasks_pixi_config` value.
|
|
34
53
|
"""
|
|
35
|
-
if
|
|
36
|
-
|
|
37
|
-
return
|
|
54
|
+
if value != {}:
|
|
55
|
+
value = TasksPixiSettings(**value).model_dump()
|
|
56
|
+
return value
|
|
38
57
|
|
|
39
58
|
|
|
40
|
-
class
|
|
59
|
+
class ValidResourceBase(BaseModel):
|
|
60
|
+
"""
|
|
61
|
+
Base resource schema.
|
|
62
|
+
"""
|
|
63
|
+
|
|
41
64
|
type: ResourceType
|
|
42
65
|
name: NonEmptyStr
|
|
43
66
|
|
|
@@ -61,27 +84,90 @@ class _ValidResourceBase(BaseModel):
|
|
|
61
84
|
and self.type == ResourceType.SLURM_SSH
|
|
62
85
|
and self.tasks_pixi_config["SLURM_CONFIG"] is None
|
|
63
86
|
):
|
|
64
|
-
raise ValueError(
|
|
65
|
-
"`tasks_pixi_config` must include `SLURM_CONFIG`."
|
|
66
|
-
)
|
|
87
|
+
raise ValueError("`tasks_pixi_config` must include `SLURM_CONFIG`.")
|
|
67
88
|
return self
|
|
68
89
|
|
|
69
90
|
|
|
70
|
-
class ValidResourceLocal(
|
|
91
|
+
class ValidResourceLocal(ValidResourceBase):
|
|
92
|
+
"""
|
|
93
|
+
Valid local resource.
|
|
94
|
+
|
|
95
|
+
Attributes:
|
|
96
|
+
name: Resource name.
|
|
97
|
+
type: Resource type.
|
|
98
|
+
tasks_python_config:
|
|
99
|
+
Configuration of Python interpreters used for task collection.
|
|
100
|
+
tasks_pixi_config:
|
|
101
|
+
Configuration of `pixi` interpreters used for task collection.
|
|
102
|
+
tasks_local_dir:
|
|
103
|
+
Local base folder for task environments.
|
|
104
|
+
jobs_local_dir:
|
|
105
|
+
Local base folder for job folders.
|
|
106
|
+
jobs_runner_config:
|
|
107
|
+
Runner configuration.
|
|
108
|
+
|
|
109
|
+
"""
|
|
110
|
+
|
|
71
111
|
type: Literal[ResourceType.LOCAL]
|
|
72
112
|
jobs_runner_config: JobRunnerConfigLocal
|
|
73
113
|
jobs_slurm_python_worker: None = None
|
|
74
114
|
host: None = None
|
|
75
115
|
|
|
76
116
|
|
|
77
|
-
class ValidResourceSlurmSudo(
|
|
117
|
+
class ValidResourceSlurmSudo(ValidResourceBase):
|
|
118
|
+
"""
|
|
119
|
+
Valid SLURM-sudo resource.
|
|
120
|
+
|
|
121
|
+
Attributes:
|
|
122
|
+
name: Resource name.
|
|
123
|
+
type: Resource type.
|
|
124
|
+
tasks_python_config:
|
|
125
|
+
Configuration of Python interpreters used for task collection.
|
|
126
|
+
tasks_pixi_config:
|
|
127
|
+
Configuration of `pixi` interpreters used for task collection.
|
|
128
|
+
tasks_local_dir:
|
|
129
|
+
Local base folder for task environments.
|
|
130
|
+
jobs_local_dir:
|
|
131
|
+
Local base folder for job folders.
|
|
132
|
+
jobs_runner_config:
|
|
133
|
+
Runner configuration.
|
|
134
|
+
jobs_poll_interval:
|
|
135
|
+
`squeue` polling interval.
|
|
136
|
+
jobs_slurm_python_worker:
|
|
137
|
+
Python worker to be used in SLURM jobs.
|
|
138
|
+
"""
|
|
139
|
+
|
|
78
140
|
type: Literal[ResourceType.SLURM_SUDO]
|
|
79
141
|
jobs_slurm_python_worker: AbsolutePathStr
|
|
80
142
|
jobs_runner_config: JobRunnerConfigSLURM
|
|
81
143
|
host: None = None
|
|
82
144
|
|
|
83
145
|
|
|
84
|
-
class ValidResourceSlurmSSH(
|
|
146
|
+
class ValidResourceSlurmSSH(ValidResourceBase):
|
|
147
|
+
"""
|
|
148
|
+
Valid SLURM-SSH resource.
|
|
149
|
+
|
|
150
|
+
Attributes:
|
|
151
|
+
name: Resource name
|
|
152
|
+
type: Resource type.
|
|
153
|
+
tasks_python_config:
|
|
154
|
+
Configuration of Python interpreters used for task collection.
|
|
155
|
+
tasks_pixi_config:
|
|
156
|
+
Configuration of `pixi` interpreters used for task collection.
|
|
157
|
+
tasks_local_dir:
|
|
158
|
+
Local base folder for task environments.
|
|
159
|
+
jobs_local_dir:
|
|
160
|
+
Local base folder for job folders.
|
|
161
|
+
jobs_runner_config:
|
|
162
|
+
Runner configuration.
|
|
163
|
+
jobs_poll_interval:
|
|
164
|
+
`squeue` polling interval.
|
|
165
|
+
jobs_slurm_python_worker:
|
|
166
|
+
Python worker to be used in SLURM jobs.
|
|
167
|
+
host:
|
|
168
|
+
Hostname or IP address of remote SLURM cluster.
|
|
169
|
+
"""
|
|
170
|
+
|
|
85
171
|
type: Literal[ResourceType.SLURM_SSH]
|
|
86
172
|
host: NonEmptyStr
|
|
87
173
|
jobs_slurm_python_worker: AbsolutePathStr
|
|
@@ -101,9 +187,16 @@ ResourceCreate = Annotated[
|
|
|
101
187
|
| Annotated[ValidResourceSlurmSSH, Tag(ResourceType.SLURM_SSH)],
|
|
102
188
|
Discriminator(get_discriminator_value),
|
|
103
189
|
]
|
|
190
|
+
"""
|
|
191
|
+
Schema for resources in API request bodies.
|
|
192
|
+
"""
|
|
104
193
|
|
|
105
194
|
|
|
106
195
|
class ResourceRead(BaseModel):
|
|
196
|
+
"""
|
|
197
|
+
Schema for resources in API response bodies.
|
|
198
|
+
"""
|
|
199
|
+
|
|
107
200
|
id: int
|
|
108
201
|
|
|
109
202
|
type: str
|
|
@@ -131,6 +224,9 @@ def cast_serialize_resource(_data: ResourceCreate) -> dict[str, Any]:
|
|
|
131
224
|
We use `@validate_call` because `ResourceCreate` is a `Union` type and it
|
|
132
225
|
cannot be instantiated directly.
|
|
133
226
|
|
|
227
|
+
Args:
|
|
228
|
+
_data:
|
|
229
|
+
|
|
134
230
|
Return:
|
|
135
231
|
Serialized version of a valid resource object.
|
|
136
232
|
"""
|