fractal-server 2.16.5__py3-none-any.whl → 2.17.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 +178 -52
- fractal_server/app/db/__init__.py +9 -11
- fractal_server/app/models/security.py +30 -22
- fractal_server/app/models/user_settings.py +5 -4
- fractal_server/app/models/v2/__init__.py +4 -0
- fractal_server/app/models/v2/job.py +3 -4
- fractal_server/app/models/v2/profile.py +16 -0
- fractal_server/app/models/v2/project.py +5 -0
- fractal_server/app/models/v2/resource.py +130 -0
- fractal_server/app/models/v2/task_group.py +4 -0
- fractal_server/app/routes/admin/v2/__init__.py +4 -0
- fractal_server/app/routes/admin/v2/_aux_functions.py +55 -0
- fractal_server/app/routes/admin/v2/accounting.py +3 -3
- fractal_server/app/routes/admin/v2/impersonate.py +2 -2
- fractal_server/app/routes/admin/v2/job.py +51 -15
- fractal_server/app/routes/admin/v2/profile.py +100 -0
- fractal_server/app/routes/admin/v2/project.py +2 -2
- fractal_server/app/routes/admin/v2/resource.py +222 -0
- fractal_server/app/routes/admin/v2/task.py +59 -32
- fractal_server/app/routes/admin/v2/task_group.py +17 -12
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +52 -86
- fractal_server/app/routes/api/__init__.py +45 -8
- fractal_server/app/routes/api/v2/_aux_functions.py +17 -1
- fractal_server/app/routes/api/v2/_aux_functions_history.py +2 -2
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +55 -19
- fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +21 -17
- fractal_server/app/routes/api/v2/dataset.py +10 -19
- fractal_server/app/routes/api/v2/history.py +8 -8
- fractal_server/app/routes/api/v2/images.py +5 -5
- fractal_server/app/routes/api/v2/job.py +8 -8
- fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
- fractal_server/app/routes/api/v2/project.py +15 -7
- fractal_server/app/routes/api/v2/status_legacy.py +2 -2
- fractal_server/app/routes/api/v2/submit.py +49 -42
- fractal_server/app/routes/api/v2/task.py +26 -8
- fractal_server/app/routes/api/v2/task_collection.py +39 -50
- fractal_server/app/routes/api/v2/task_collection_custom.py +10 -6
- fractal_server/app/routes/api/v2/task_collection_pixi.py +34 -42
- fractal_server/app/routes/api/v2/task_group.py +19 -9
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +43 -86
- fractal_server/app/routes/api/v2/task_version_update.py +3 -3
- fractal_server/app/routes/api/v2/workflow.py +9 -9
- fractal_server/app/routes/api/v2/workflow_import.py +29 -16
- fractal_server/app/routes/api/v2/workflowtask.py +5 -5
- fractal_server/app/routes/auth/__init__.py +34 -5
- fractal_server/app/routes/auth/_aux_auth.py +39 -20
- fractal_server/app/routes/auth/current_user.py +56 -67
- fractal_server/app/routes/auth/group.py +29 -46
- fractal_server/app/routes/auth/oauth.py +55 -38
- fractal_server/app/routes/auth/register.py +2 -2
- fractal_server/app/routes/auth/router.py +4 -2
- fractal_server/app/routes/auth/users.py +29 -53
- fractal_server/app/routes/aux/_runner.py +2 -1
- fractal_server/app/routes/aux/validate_user_profile.py +62 -0
- fractal_server/app/schemas/__init__.py +0 -1
- fractal_server/app/schemas/user.py +43 -13
- fractal_server/app/schemas/user_group.py +2 -1
- fractal_server/app/schemas/v2/__init__.py +12 -0
- fractal_server/app/schemas/v2/profile.py +78 -0
- fractal_server/app/schemas/v2/resource.py +137 -0
- fractal_server/app/schemas/v2/task_collection.py +11 -3
- fractal_server/app/schemas/v2/task_group.py +5 -0
- fractal_server/app/security/__init__.py +174 -75
- fractal_server/app/security/signup_email.py +52 -34
- fractal_server/config/__init__.py +27 -0
- fractal_server/config/_data.py +68 -0
- fractal_server/config/_database.py +59 -0
- fractal_server/config/_email.py +133 -0
- fractal_server/config/_main.py +78 -0
- fractal_server/config/_oauth.py +69 -0
- fractal_server/config/_settings_config.py +7 -0
- fractal_server/data_migrations/2_17_0.py +339 -0
- fractal_server/images/tools.py +3 -3
- fractal_server/logger.py +3 -3
- fractal_server/main.py +17 -23
- fractal_server/migrations/naming_convention.py +1 -1
- fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py +195 -0
- fractal_server/runner/config/__init__.py +2 -0
- fractal_server/runner/config/_local.py +21 -0
- fractal_server/runner/config/_slurm.py +129 -0
- fractal_server/runner/config/slurm_mem_to_MB.py +63 -0
- fractal_server/runner/exceptions.py +4 -0
- fractal_server/runner/executors/base_runner.py +17 -7
- fractal_server/runner/executors/local/get_local_config.py +21 -86
- fractal_server/runner/executors/local/runner.py +48 -5
- fractal_server/runner/executors/slurm_common/_batching.py +2 -2
- fractal_server/runner/executors/slurm_common/base_slurm_runner.py +60 -26
- fractal_server/runner/executors/slurm_common/get_slurm_config.py +39 -55
- fractal_server/runner/executors/slurm_common/remote.py +1 -1
- fractal_server/runner/executors/slurm_common/slurm_config.py +214 -0
- fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -1
- fractal_server/runner/executors/slurm_ssh/runner.py +12 -14
- fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +2 -2
- fractal_server/runner/executors/slurm_sudo/runner.py +12 -12
- fractal_server/runner/v2/_local.py +36 -21
- fractal_server/runner/v2/_slurm_ssh.py +41 -4
- fractal_server/runner/v2/_slurm_sudo.py +42 -12
- fractal_server/runner/v2/db_tools.py +1 -1
- fractal_server/runner/v2/runner.py +3 -11
- fractal_server/runner/v2/runner_functions.py +42 -28
- fractal_server/runner/v2/submit_workflow.py +88 -109
- fractal_server/runner/versions.py +8 -3
- fractal_server/ssh/_fabric.py +6 -6
- fractal_server/tasks/config/__init__.py +3 -0
- fractal_server/tasks/config/_pixi.py +127 -0
- fractal_server/tasks/config/_python.py +51 -0
- fractal_server/tasks/v2/local/_utils.py +7 -7
- fractal_server/tasks/v2/local/collect.py +13 -5
- fractal_server/tasks/v2/local/collect_pixi.py +26 -10
- fractal_server/tasks/v2/local/deactivate.py +7 -1
- fractal_server/tasks/v2/local/deactivate_pixi.py +5 -1
- fractal_server/tasks/v2/local/delete.py +5 -1
- fractal_server/tasks/v2/local/reactivate.py +13 -5
- fractal_server/tasks/v2/local/reactivate_pixi.py +27 -9
- fractal_server/tasks/v2/ssh/_pixi_slurm_ssh.py +11 -10
- fractal_server/tasks/v2/ssh/_utils.py +6 -7
- fractal_server/tasks/v2/ssh/collect.py +19 -12
- fractal_server/tasks/v2/ssh/collect_pixi.py +34 -16
- fractal_server/tasks/v2/ssh/deactivate.py +12 -8
- fractal_server/tasks/v2/ssh/deactivate_pixi.py +14 -10
- fractal_server/tasks/v2/ssh/delete.py +12 -9
- fractal_server/tasks/v2/ssh/reactivate.py +18 -12
- fractal_server/tasks/v2/ssh/reactivate_pixi.py +36 -17
- fractal_server/tasks/v2/templates/4_pip_show.sh +4 -6
- fractal_server/tasks/v2/utils_database.py +2 -2
- fractal_server/tasks/v2/utils_pixi.py +3 -0
- fractal_server/tasks/v2/utils_python_interpreter.py +8 -16
- fractal_server/tasks/v2/utils_templates.py +7 -10
- fractal_server/utils.py +1 -1
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/METADATA +8 -10
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/RECORD +137 -118
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/WHEEL +1 -1
- fractal_server/app/routes/aux/validate_user_settings.py +0 -73
- fractal_server/app/schemas/user_settings.py +0 -67
- fractal_server/app/user_settings.py +0 -42
- fractal_server/config.py +0 -906
- fractal_server/data_migrations/2_14_10.py +0 -48
- fractal_server/runner/executors/slurm_common/_slurm_config.py +0 -471
- /fractal_server/{runner → app}/shutdown.py +0 -0
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,50 +1,66 @@
|
|
|
1
1
|
from fastapi import APIRouter
|
|
2
|
+
from httpx_oauth.clients.github import GitHubOAuth2
|
|
3
|
+
from httpx_oauth.clients.google import GoogleOAuth2
|
|
4
|
+
from httpx_oauth.clients.openid import OpenID
|
|
5
|
+
from httpx_oauth.clients.openid import OpenIDConfigurationError
|
|
2
6
|
|
|
3
7
|
from . import cookie_backend
|
|
4
8
|
from . import fastapi_users
|
|
5
|
-
from
|
|
6
|
-
from
|
|
9
|
+
from fractal_server.config import get_oauth_settings
|
|
10
|
+
from fractal_server.config import get_settings
|
|
11
|
+
from fractal_server.config import OAuthSettings
|
|
12
|
+
from fractal_server.syringe import Inject
|
|
7
13
|
|
|
8
|
-
router_oauth = APIRouter()
|
|
9
14
|
|
|
15
|
+
def _create_client_github(cfg: OAuthSettings) -> GitHubOAuth2:
|
|
16
|
+
return GitHubOAuth2(
|
|
17
|
+
client_id=cfg.OAUTH_CLIENT_ID.get_secret_value(),
|
|
18
|
+
client_secret=cfg.OAUTH_CLIENT_SECRET.get_secret_value(),
|
|
19
|
+
)
|
|
10
20
|
|
|
11
|
-
# OAUTH CLIENTS
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
def _create_client_google(cfg: OAuthSettings) -> GoogleOAuth2:
|
|
23
|
+
return GoogleOAuth2(
|
|
24
|
+
client_id=cfg.OAUTH_CLIENT_ID.get_secret_value(),
|
|
25
|
+
client_secret=cfg.OAUTH_CLIENT_SECRET.get_secret_value(),
|
|
26
|
+
)
|
|
17
27
|
|
|
18
|
-
# FIXME:Dependency injection should be wrapped within a function call to make
|
|
19
|
-
# it truly lazy. This function could then be called on startup of the FastAPI
|
|
20
|
-
# app (cf. fractal_server.main)
|
|
21
|
-
settings = Inject(get_settings)
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
def _create_client_oidc(cfg: OAuthSettings) -> OpenID:
|
|
30
|
+
try:
|
|
31
|
+
open_id = OpenID(
|
|
32
|
+
client_id=cfg.OAUTH_CLIENT_ID.get_secret_value(),
|
|
33
|
+
client_secret=cfg.OAUTH_CLIENT_SECRET.get_secret_value(),
|
|
34
|
+
openid_configuration_endpoint=cfg.OAUTH_OIDC_CONFIG_ENDPOINT.get_secret_value(), # noqa
|
|
35
|
+
)
|
|
36
|
+
except OpenIDConfigurationError as e:
|
|
37
|
+
OAUTH_OIDC_CONFIG_ENDPOINT = (
|
|
38
|
+
cfg.OAUTH_OIDC_CONFIG_ENDPOINT.get_secret_value()
|
|
39
|
+
)
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
f"Cannot initialize OpenID client. Original error: '{e}'. "
|
|
42
|
+
f"Hint: is {OAUTH_OIDC_CONFIG_ENDPOINT=} reachable?"
|
|
43
|
+
)
|
|
44
|
+
return open_id
|
|
25
45
|
|
|
26
|
-
if client_name == "google":
|
|
27
|
-
from httpx_oauth.clients.google import GoogleOAuth2
|
|
28
46
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
def get_oauth_router() -> APIRouter | None:
|
|
48
|
+
"""
|
|
49
|
+
Get the `APIRouter` object for OAuth endpoints.
|
|
50
|
+
"""
|
|
51
|
+
router_oauth = APIRouter()
|
|
52
|
+
settings = Inject(get_settings)
|
|
53
|
+
oauth_settings = Inject(get_oauth_settings)
|
|
54
|
+
if not oauth_settings.is_set:
|
|
55
|
+
return None
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
client_name = oauth_settings.OAUTH_CLIENT_NAME
|
|
58
|
+
if client_name == "google":
|
|
59
|
+
client = _create_client_google(oauth_settings)
|
|
60
|
+
elif client_name == "github":
|
|
61
|
+
client = _create_client_github(oauth_settings)
|
|
40
62
|
else:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
client = OpenID(
|
|
44
|
-
client_config.CLIENT_ID,
|
|
45
|
-
client_config.CLIENT_SECRET.get_secret_value(),
|
|
46
|
-
client_config.OIDC_CONFIGURATION_ENDPOINT,
|
|
47
|
-
)
|
|
63
|
+
client = _create_client_oidc(oauth_settings)
|
|
48
64
|
|
|
49
65
|
router_oauth.include_router(
|
|
50
66
|
fastapi_users.get_oauth_router(
|
|
@@ -53,13 +69,14 @@ for client_config in settings.OAUTH_CLIENTS_CONFIG:
|
|
|
53
69
|
settings.JWT_SECRET_KEY,
|
|
54
70
|
is_verified_by_default=False,
|
|
55
71
|
associate_by_email=True,
|
|
56
|
-
redirect_url=
|
|
72
|
+
redirect_url=oauth_settings.OAUTH_REDIRECT_URL,
|
|
57
73
|
),
|
|
58
74
|
prefix=f"/{client_name}",
|
|
59
75
|
)
|
|
60
76
|
|
|
77
|
+
# Add trailing slash to all routes' paths
|
|
78
|
+
for route in router_oauth.routes:
|
|
79
|
+
if not route.path.endswith("/"):
|
|
80
|
+
route.path = f"{route.path}/"
|
|
61
81
|
|
|
62
|
-
|
|
63
|
-
for route in router_oauth.routes:
|
|
64
|
-
if not route.path.endswith("/"):
|
|
65
|
-
route.path = f"{route.path}/"
|
|
82
|
+
return router_oauth
|
|
@@ -4,7 +4,7 @@ Definition of `/auth/register/` routes.
|
|
|
4
4
|
from fastapi import APIRouter
|
|
5
5
|
from fastapi import Depends
|
|
6
6
|
|
|
7
|
-
from . import
|
|
7
|
+
from . import current_superuser_act
|
|
8
8
|
from . import fastapi_users
|
|
9
9
|
from ...schemas.user import UserCreate
|
|
10
10
|
from ...schemas.user import UserRead
|
|
@@ -13,7 +13,7 @@ router_register = APIRouter()
|
|
|
13
13
|
|
|
14
14
|
router_register.include_router(
|
|
15
15
|
fastapi_users.get_register_router(UserRead, UserCreate),
|
|
16
|
-
dependencies=[Depends(
|
|
16
|
+
dependencies=[Depends(current_superuser_act)],
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
|
|
@@ -3,7 +3,7 @@ from fastapi import APIRouter
|
|
|
3
3
|
from .current_user import router_current_user
|
|
4
4
|
from .group import router_group
|
|
5
5
|
from .login import router_login
|
|
6
|
-
from .oauth import
|
|
6
|
+
from .oauth import get_oauth_router
|
|
7
7
|
from .register import router_register
|
|
8
8
|
from .users import router_users
|
|
9
9
|
|
|
@@ -14,4 +14,6 @@ router_auth.include_router(router_current_user)
|
|
|
14
14
|
router_auth.include_router(router_login)
|
|
15
15
|
router_auth.include_router(router_users)
|
|
16
16
|
router_auth.include_router(router_group)
|
|
17
|
-
|
|
17
|
+
router_oauth = get_oauth_router()
|
|
18
|
+
if router_oauth is not None:
|
|
19
|
+
router_auth.include_router(router_oauth)
|
|
@@ -6,31 +6,29 @@ from fastapi import Depends
|
|
|
6
6
|
from fastapi import HTTPException
|
|
7
7
|
from fastapi import status
|
|
8
8
|
from fastapi_users import exceptions
|
|
9
|
-
from fastapi_users import schemas
|
|
10
9
|
from fastapi_users.router.common import ErrorCode
|
|
11
10
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
11
|
from sqlmodel import func
|
|
13
12
|
from sqlmodel import select
|
|
14
13
|
|
|
15
|
-
from . import
|
|
14
|
+
from . import current_superuser_act
|
|
16
15
|
from ...db import get_async_db
|
|
17
16
|
from ...schemas.user import UserRead
|
|
18
17
|
from ...schemas.user import UserUpdate
|
|
19
|
-
from
|
|
20
|
-
from ._aux_auth import _get_default_usergroup_id
|
|
18
|
+
from ._aux_auth import _get_default_usergroup_id_or_none
|
|
21
19
|
from ._aux_auth import _get_single_user_with_groups
|
|
22
|
-
from ._aux_auth import FRACTAL_DEFAULT_GROUP_NAME
|
|
23
20
|
from fractal_server.app.models import LinkUserGroup
|
|
24
21
|
from fractal_server.app.models import UserGroup
|
|
25
22
|
from fractal_server.app.models import UserOAuth
|
|
26
|
-
from fractal_server.app.models import
|
|
23
|
+
from fractal_server.app.models.v2 import Profile
|
|
27
24
|
from fractal_server.app.routes.auth._aux_auth import _user_or_404
|
|
28
|
-
from fractal_server.app.schemas import UserSettingsRead
|
|
29
|
-
from fractal_server.app.schemas import UserSettingsUpdate
|
|
30
25
|
from fractal_server.app.schemas.user import UserUpdateGroups
|
|
31
26
|
from fractal_server.app.security import get_user_manager
|
|
32
27
|
from fractal_server.app.security import UserManager
|
|
28
|
+
from fractal_server.config import get_settings
|
|
33
29
|
from fractal_server.logger import set_logger
|
|
30
|
+
from fractal_server.syringe import Inject
|
|
31
|
+
|
|
34
32
|
|
|
35
33
|
router_users = APIRouter()
|
|
36
34
|
|
|
@@ -42,7 +40,7 @@ logger = set_logger(__name__)
|
|
|
42
40
|
async def get_user(
|
|
43
41
|
user_id: int,
|
|
44
42
|
group_ids_names: bool = True,
|
|
45
|
-
superuser: UserOAuth = Depends(
|
|
43
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
46
44
|
db: AsyncSession = Depends(get_async_db),
|
|
47
45
|
) -> UserRead:
|
|
48
46
|
user = await _user_or_404(user_id, db)
|
|
@@ -56,7 +54,7 @@ async def get_user(
|
|
|
56
54
|
async def patch_user(
|
|
57
55
|
user_id: int,
|
|
58
56
|
user_update: UserUpdate,
|
|
59
|
-
current_superuser: UserOAuth = Depends(
|
|
57
|
+
current_superuser: UserOAuth = Depends(current_superuser_act),
|
|
60
58
|
user_manager: UserManager = Depends(get_user_manager),
|
|
61
59
|
db: AsyncSession = Depends(get_async_db),
|
|
62
60
|
):
|
|
@@ -67,6 +65,14 @@ async def patch_user(
|
|
|
67
65
|
# Check that user exists
|
|
68
66
|
user_to_patch = await _user_or_404(user_id, db)
|
|
69
67
|
|
|
68
|
+
if user_update.profile_id is not None:
|
|
69
|
+
profile = await db.get(Profile, user_update.profile_id)
|
|
70
|
+
if profile is None:
|
|
71
|
+
raise HTTPException(
|
|
72
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
73
|
+
detail=f"Profile {user_update.profile_id} not found.",
|
|
74
|
+
)
|
|
75
|
+
|
|
70
76
|
# Modify user attributes
|
|
71
77
|
try:
|
|
72
78
|
user = await user_manager.update(
|
|
@@ -75,7 +81,7 @@ async def patch_user(
|
|
|
75
81
|
safe=False,
|
|
76
82
|
request=None,
|
|
77
83
|
)
|
|
78
|
-
validated_user =
|
|
84
|
+
validated_user = UserOAuth.model_validate(user.model_dump())
|
|
79
85
|
patched_user = await db.get(
|
|
80
86
|
UserOAuth, validated_user.id, populate_existing=True
|
|
81
87
|
)
|
|
@@ -103,13 +109,16 @@ async def patch_user(
|
|
|
103
109
|
|
|
104
110
|
@router_users.get("/users/", response_model=list[UserRead])
|
|
105
111
|
async def list_users(
|
|
106
|
-
|
|
112
|
+
profile_id: int | None = None,
|
|
113
|
+
user: UserOAuth = Depends(current_superuser_act),
|
|
107
114
|
db: AsyncSession = Depends(get_async_db),
|
|
108
115
|
):
|
|
109
116
|
"""
|
|
110
117
|
Return list of all users
|
|
111
118
|
"""
|
|
112
119
|
stm = select(UserOAuth)
|
|
120
|
+
if profile_id is not None:
|
|
121
|
+
stm = stm.where(UserOAuth.profile_id == profile_id)
|
|
113
122
|
res = await db.execute(stm)
|
|
114
123
|
user_list = res.scalars().unique().all()
|
|
115
124
|
|
|
@@ -136,9 +145,10 @@ async def list_users(
|
|
|
136
145
|
async def set_user_groups(
|
|
137
146
|
user_id: int,
|
|
138
147
|
user_update: UserUpdateGroups,
|
|
139
|
-
superuser: UserOAuth = Depends(
|
|
148
|
+
superuser: UserOAuth = Depends(current_superuser_act),
|
|
140
149
|
db: AsyncSession = Depends(get_async_db),
|
|
141
150
|
) -> UserRead:
|
|
151
|
+
settings = Inject(get_settings)
|
|
142
152
|
# Preliminary check that all objects exist in the db
|
|
143
153
|
user = await _user_or_404(user_id=user_id, db=db)
|
|
144
154
|
target_group_ids = user_update.group_ids
|
|
@@ -154,13 +164,16 @@ async def set_user_groups(
|
|
|
154
164
|
)
|
|
155
165
|
|
|
156
166
|
# Check that default group is not being removed
|
|
157
|
-
|
|
158
|
-
if
|
|
167
|
+
default_group_id_or_none = await _get_default_usergroup_id_or_none(db=db)
|
|
168
|
+
if (
|
|
169
|
+
default_group_id_or_none is not None
|
|
170
|
+
and default_group_id_or_none not in target_group_ids
|
|
171
|
+
):
|
|
159
172
|
raise HTTPException(
|
|
160
173
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
161
174
|
detail=(
|
|
162
175
|
f"Cannot remove user from "
|
|
163
|
-
f"'{FRACTAL_DEFAULT_GROUP_NAME}' group.",
|
|
176
|
+
f"'{settings.FRACTAL_DEFAULT_GROUP_NAME}' group.",
|
|
164
177
|
),
|
|
165
178
|
)
|
|
166
179
|
|
|
@@ -198,40 +211,3 @@ async def set_user_groups(
|
|
|
198
211
|
user_with_groups = await _get_single_user_with_groups(user, db)
|
|
199
212
|
|
|
200
213
|
return user_with_groups
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
@router_users.get(
|
|
204
|
-
"/users/{user_id}/settings/", response_model=UserSettingsRead
|
|
205
|
-
)
|
|
206
|
-
async def get_user_settings(
|
|
207
|
-
user_id: int,
|
|
208
|
-
superuser: UserOAuth = Depends(current_active_superuser),
|
|
209
|
-
db: AsyncSession = Depends(get_async_db),
|
|
210
|
-
) -> UserSettingsRead:
|
|
211
|
-
user = await _user_or_404(user_id=user_id, db=db)
|
|
212
|
-
verify_user_has_settings(user)
|
|
213
|
-
user_settings = await db.get(UserSettings, user.user_settings_id)
|
|
214
|
-
return user_settings
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
@router_users.patch(
|
|
218
|
-
"/users/{user_id}/settings/", response_model=UserSettingsRead
|
|
219
|
-
)
|
|
220
|
-
async def patch_user_settings(
|
|
221
|
-
user_id: int,
|
|
222
|
-
settings_update: UserSettingsUpdate,
|
|
223
|
-
superuser: UserOAuth = Depends(current_active_superuser),
|
|
224
|
-
db: AsyncSession = Depends(get_async_db),
|
|
225
|
-
) -> UserSettingsRead:
|
|
226
|
-
user = await _user_or_404(user_id=user_id, db=db)
|
|
227
|
-
verify_user_has_settings(user)
|
|
228
|
-
user_settings = await db.get(UserSettings, user.user_settings_id)
|
|
229
|
-
|
|
230
|
-
for k, v in settings_update.model_dump(exclude_unset=True).items():
|
|
231
|
-
setattr(user_settings, k, v)
|
|
232
|
-
|
|
233
|
-
db.add(user_settings)
|
|
234
|
-
await db.commit()
|
|
235
|
-
await db.refresh(user_settings)
|
|
236
|
-
|
|
237
|
-
return user_settings
|
|
@@ -3,10 +3,11 @@ from fastapi import status
|
|
|
3
3
|
|
|
4
4
|
from ....config import get_settings
|
|
5
5
|
from ....syringe import Inject
|
|
6
|
+
from fractal_server.app.schemas.v2 import ResourceType
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def _backend_supports_shutdown(backend: str) -> bool:
|
|
9
|
-
if backend in [
|
|
10
|
+
if backend in [ResourceType.SLURM_SUDO, ResourceType.SLURM_SSH]:
|
|
10
11
|
return True
|
|
11
12
|
else:
|
|
12
13
|
return False
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
from fastapi import status
|
|
3
|
+
from pydantic import ValidationError
|
|
4
|
+
|
|
5
|
+
from fractal_server.app.db import AsyncSession
|
|
6
|
+
from fractal_server.app.models import Profile
|
|
7
|
+
from fractal_server.app.models import Resource
|
|
8
|
+
from fractal_server.app.models import UserOAuth
|
|
9
|
+
from fractal_server.app.schemas.v2.profile import cast_serialize_profile
|
|
10
|
+
from fractal_server.app.schemas.v2.resource import cast_serialize_resource
|
|
11
|
+
from fractal_server.logger import set_logger
|
|
12
|
+
|
|
13
|
+
logger = set_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def user_has_profile_or_422(*, user: UserOAuth) -> None:
|
|
17
|
+
if user.profile_id is None:
|
|
18
|
+
raise HTTPException(
|
|
19
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
20
|
+
detail=(
|
|
21
|
+
f"User {user.email} is not associated to a computational "
|
|
22
|
+
"profile. Please contact an admin."
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def validate_user_profile(
|
|
28
|
+
*,
|
|
29
|
+
user: UserOAuth,
|
|
30
|
+
db: AsyncSession,
|
|
31
|
+
) -> tuple[Resource, Profile]:
|
|
32
|
+
"""
|
|
33
|
+
Validate profile and resource associated to a given user.
|
|
34
|
+
|
|
35
|
+
Note: this only returns non-db-bound objects.
|
|
36
|
+
"""
|
|
37
|
+
await user_has_profile_or_422(user=user)
|
|
38
|
+
profile = await db.get(Profile, user.profile_id)
|
|
39
|
+
resource = await db.get(Resource, profile.resource_id)
|
|
40
|
+
try:
|
|
41
|
+
cast_serialize_resource(
|
|
42
|
+
resource.model_dump(exclude={"id", "timestamp_created"}),
|
|
43
|
+
)
|
|
44
|
+
cast_serialize_profile(
|
|
45
|
+
profile.model_dump(exclude={"resource_id", "id"}),
|
|
46
|
+
)
|
|
47
|
+
db.expunge(resource)
|
|
48
|
+
db.expunge(profile)
|
|
49
|
+
|
|
50
|
+
return resource, profile
|
|
51
|
+
|
|
52
|
+
except ValidationError as e:
|
|
53
|
+
error_msg = (
|
|
54
|
+
"User resource/profile are not valid for "
|
|
55
|
+
f"resource type '{resource.type}'. "
|
|
56
|
+
f"Original error: {str(e)}"
|
|
57
|
+
)
|
|
58
|
+
logger.warning(error_msg)
|
|
59
|
+
raise HTTPException(
|
|
60
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
61
|
+
detail=error_msg,
|
|
62
|
+
)
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
1
3
|
from fastapi_users import schemas
|
|
4
|
+
from pydantic import AfterValidator
|
|
2
5
|
from pydantic import BaseModel
|
|
3
6
|
from pydantic import ConfigDict
|
|
4
7
|
from pydantic import EmailStr
|
|
5
8
|
from pydantic import Field
|
|
6
9
|
|
|
10
|
+
from fractal_server.string_tools import validate_cmd
|
|
11
|
+
from fractal_server.types import AbsolutePathStr
|
|
12
|
+
from fractal_server.types import ListUniqueNonEmptyString
|
|
7
13
|
from fractal_server.types import ListUniqueNonNegativeInt
|
|
8
14
|
from fractal_server.types import NonEmptyStr
|
|
9
15
|
|
|
10
|
-
__all__ = (
|
|
11
|
-
"UserRead",
|
|
12
|
-
"UserUpdate",
|
|
13
|
-
"UserUpdateGroups",
|
|
14
|
-
"UserCreate",
|
|
15
|
-
)
|
|
16
|
-
|
|
17
16
|
|
|
18
17
|
class OAuthAccountRead(BaseModel):
|
|
19
18
|
"""
|
|
@@ -36,12 +35,21 @@ class UserRead(schemas.BaseUser[int]):
|
|
|
36
35
|
Schema for `User` read from database.
|
|
37
36
|
|
|
38
37
|
Attributes:
|
|
39
|
-
|
|
38
|
+
group_ids_names:
|
|
39
|
+
oauth_accounts:
|
|
40
|
+
profile_id:
|
|
40
41
|
"""
|
|
41
42
|
|
|
42
|
-
username: str | None = None
|
|
43
43
|
group_ids_names: list[tuple[int, str]] | None = None
|
|
44
44
|
oauth_accounts: list[OAuthAccountRead]
|
|
45
|
+
profile_id: int | None = None
|
|
46
|
+
project_dir: str
|
|
47
|
+
slurm_accounts: list[str]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _validate_cmd(value: str) -> str:
|
|
51
|
+
validate_cmd(value)
|
|
52
|
+
return value
|
|
45
53
|
|
|
46
54
|
|
|
47
55
|
class UserUpdate(schemas.BaseUserUpdate):
|
|
@@ -49,16 +57,27 @@ class UserUpdate(schemas.BaseUserUpdate):
|
|
|
49
57
|
Schema for `User` update.
|
|
50
58
|
|
|
51
59
|
Attributes:
|
|
52
|
-
|
|
60
|
+
password:
|
|
61
|
+
email:
|
|
62
|
+
is_active:
|
|
63
|
+
is_superuser:
|
|
64
|
+
is_verified:
|
|
65
|
+
profile_id:
|
|
66
|
+
project_dir:
|
|
67
|
+
slurm_accounts:
|
|
53
68
|
"""
|
|
54
69
|
|
|
55
70
|
model_config = ConfigDict(extra="forbid")
|
|
56
|
-
username: NonEmptyStr = None
|
|
57
71
|
password: NonEmptyStr = None
|
|
58
72
|
email: EmailStr = None
|
|
59
73
|
is_active: bool = None
|
|
60
74
|
is_superuser: bool = None
|
|
61
75
|
is_verified: bool = None
|
|
76
|
+
profile_id: int | None = None
|
|
77
|
+
project_dir: Annotated[
|
|
78
|
+
AbsolutePathStr, AfterValidator(_validate_cmd)
|
|
79
|
+
] = None
|
|
80
|
+
slurm_accounts: ListUniqueNonEmptyString = None
|
|
62
81
|
|
|
63
82
|
|
|
64
83
|
class UserUpdateStrict(BaseModel):
|
|
@@ -66,9 +85,11 @@ class UserUpdateStrict(BaseModel):
|
|
|
66
85
|
Schema for `User` self-editing.
|
|
67
86
|
|
|
68
87
|
Attributes:
|
|
88
|
+
slurm_accounts:
|
|
69
89
|
"""
|
|
70
90
|
|
|
71
91
|
model_config = ConfigDict(extra="forbid")
|
|
92
|
+
slurm_accounts: ListUniqueNonEmptyString = None
|
|
72
93
|
|
|
73
94
|
|
|
74
95
|
class UserCreate(schemas.BaseUserCreate):
|
|
@@ -76,10 +97,12 @@ class UserCreate(schemas.BaseUserCreate):
|
|
|
76
97
|
Schema for `User` creation.
|
|
77
98
|
|
|
78
99
|
Attributes:
|
|
79
|
-
|
|
100
|
+
profile_id:
|
|
80
101
|
"""
|
|
81
102
|
|
|
82
|
-
|
|
103
|
+
profile_id: int | None = None
|
|
104
|
+
project_dir: Annotated[AbsolutePathStr, AfterValidator(_validate_cmd)]
|
|
105
|
+
slurm_accounts: list[str] = Field(default_factory=list)
|
|
83
106
|
|
|
84
107
|
|
|
85
108
|
class UserUpdateGroups(BaseModel):
|
|
@@ -91,3 +114,10 @@ class UserUpdateGroups(BaseModel):
|
|
|
91
114
|
model_config = ConfigDict(extra="forbid")
|
|
92
115
|
|
|
93
116
|
group_ids: ListUniqueNonNegativeInt = Field(min_length=1)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class UserProfileInfo(BaseModel):
|
|
120
|
+
has_profile: bool
|
|
121
|
+
resource_name: str | None = None
|
|
122
|
+
profile_name: str | None = None
|
|
123
|
+
username: str | None = None
|
|
@@ -7,6 +7,7 @@ from pydantic import field_serializer
|
|
|
7
7
|
from pydantic.types import AwareDatetime
|
|
8
8
|
|
|
9
9
|
from fractal_server.types import ListUniqueAbsolutePathStr
|
|
10
|
+
from fractal_server.types import NonEmptyStr
|
|
10
11
|
|
|
11
12
|
__all__ = (
|
|
12
13
|
"UserGroupRead",
|
|
@@ -50,7 +51,7 @@ class UserGroupCreate(BaseModel):
|
|
|
50
51
|
|
|
51
52
|
model_config = ConfigDict(extra="forbid")
|
|
52
53
|
|
|
53
|
-
name:
|
|
54
|
+
name: NonEmptyStr
|
|
54
55
|
viewer_paths: ListUniqueAbsolutePathStr = Field(default_factory=list)
|
|
55
56
|
|
|
56
57
|
|
|
@@ -22,9 +22,20 @@ from .job import JobStatusTypeV2 # noqa F401
|
|
|
22
22
|
from .job import JobUpdateV2 # noqa F401
|
|
23
23
|
from .manifest import ManifestV2 # noqa F401
|
|
24
24
|
from .manifest import TaskManifestV2 # noqa F401
|
|
25
|
+
from .profile import ProfileCreate # noqa F401
|
|
26
|
+
from .profile import ProfileRead # noqa F401
|
|
27
|
+
from .profile import ValidProfileLocal # noqa F401
|
|
28
|
+
from .profile import ValidProfileSlurmSSH # noqa F401
|
|
29
|
+
from .profile import ValidProfileSlurmSudo # noqa F401
|
|
25
30
|
from .project import ProjectCreateV2 # noqa F401
|
|
26
31
|
from .project import ProjectReadV2 # noqa F401
|
|
27
32
|
from .project import ProjectUpdateV2 # noqa F401
|
|
33
|
+
from .resource import ResourceCreate # noqa F401
|
|
34
|
+
from .resource import ResourceRead # noqa F401
|
|
35
|
+
from .resource import ResourceType # noqa F401
|
|
36
|
+
from .resource import ValidResourceLocal # noqa F401
|
|
37
|
+
from .resource import ValidResourceSlurmSSH # noqa F401
|
|
38
|
+
from .resource import ValidResourceSlurmSudo # noqa F401
|
|
28
39
|
from .status_legacy import WorkflowTaskStatusTypeV2 # noqa F401
|
|
29
40
|
from .task import TaskCreateV2 # noqa F401
|
|
30
41
|
from .task import TaskExportV2 # noqa F401
|
|
@@ -41,6 +52,7 @@ from .task_group import TaskGroupActivityStatusV2 # noqa F401
|
|
|
41
52
|
from .task_group import TaskGroupActivityV2Read # noqa F401
|
|
42
53
|
from .task_group import TaskGroupCreateV2 # noqa F401
|
|
43
54
|
from .task_group import TaskGroupCreateV2Strict # noqa F401
|
|
55
|
+
from .task_group import TaskGroupReadSuperuser # noqa F401
|
|
44
56
|
from .task_group import TaskGroupReadV2 # noqa F401
|
|
45
57
|
from .task_group import TaskGroupUpdateV2 # noqa F401
|
|
46
58
|
from .task_group import TaskGroupV2OriginEnum # noqa F401
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from pydantic import Discriminator
|
|
6
|
+
from pydantic import Tag
|
|
7
|
+
from pydantic import validate_call
|
|
8
|
+
|
|
9
|
+
from .resource import ResourceType
|
|
10
|
+
from fractal_server.types import AbsolutePathStr
|
|
11
|
+
from fractal_server.types import NonEmptyStr
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ValidProfileLocal(BaseModel):
|
|
15
|
+
name: NonEmptyStr
|
|
16
|
+
resource_type: ResourceType
|
|
17
|
+
username: None = None
|
|
18
|
+
ssh_key_path: None = None
|
|
19
|
+
jobs_remote_dir: None = None
|
|
20
|
+
tasks_remote_dir: None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ValidProfileSlurmSudo(BaseModel):
|
|
24
|
+
name: NonEmptyStr
|
|
25
|
+
resource_type: ResourceType
|
|
26
|
+
username: NonEmptyStr
|
|
27
|
+
ssh_key_path: None = None
|
|
28
|
+
jobs_remote_dir: None = None
|
|
29
|
+
tasks_remote_dir: None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ValidProfileSlurmSSH(BaseModel):
|
|
33
|
+
name: NonEmptyStr
|
|
34
|
+
resource_type: ResourceType
|
|
35
|
+
username: NonEmptyStr
|
|
36
|
+
ssh_key_path: AbsolutePathStr
|
|
37
|
+
jobs_remote_dir: AbsolutePathStr
|
|
38
|
+
tasks_remote_dir: AbsolutePathStr
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_discriminator_value(v: Any) -> str:
|
|
42
|
+
# See https://github.com/fastapi/fastapi/discussions/12941
|
|
43
|
+
if isinstance(v, dict):
|
|
44
|
+
return v.get("resource_type", None)
|
|
45
|
+
return getattr(v, "resource_type", None)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
ProfileCreate = Annotated[
|
|
49
|
+
Annotated[ValidProfileLocal, Tag(ResourceType.LOCAL)]
|
|
50
|
+
| Annotated[ValidProfileSlurmSudo, Tag(ResourceType.SLURM_SUDO)]
|
|
51
|
+
| Annotated[ValidProfileSlurmSSH, Tag(ResourceType.SLURM_SSH)],
|
|
52
|
+
Discriminator(get_discriminator_value),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ProfileRead(BaseModel):
|
|
57
|
+
id: int
|
|
58
|
+
name: str
|
|
59
|
+
resource_id: int
|
|
60
|
+
resource_type: str
|
|
61
|
+
username: str | None = None
|
|
62
|
+
ssh_key_path: str | None = None
|
|
63
|
+
jobs_remote_dir: str | None = None
|
|
64
|
+
tasks_remote_dir: str | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@validate_call
|
|
68
|
+
def cast_serialize_profile(_data: ProfileCreate) -> dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Cast/serialize round-trip for `Profile` data.
|
|
71
|
+
|
|
72
|
+
We use `@validate_call` because `ProfileeCreate` is a `Union` type and it
|
|
73
|
+
cannot be instantiated directly.
|
|
74
|
+
|
|
75
|
+
Return:
|
|
76
|
+
Serialized version of a valid profile object.
|
|
77
|
+
"""
|
|
78
|
+
return _data.model_dump()
|