fractal-server 2.16.5__py3-none-any.whl → 2.17.0a0__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 +129 -22
- fractal_server/app/db/__init__.py +9 -11
- fractal_server/app/models/security.py +7 -3
- fractal_server/app/models/user_settings.py +0 -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 +3 -0
- fractal_server/app/models/v2/resource.py +130 -0
- fractal_server/app/models/v2/task_group.py +3 -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/profile.py +86 -0
- fractal_server/app/routes/admin/v2/resource.py +229 -0
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +48 -82
- fractal_server/app/routes/api/__init__.py +26 -7
- fractal_server/app/routes/api/v2/_aux_functions.py +27 -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 +7 -7
- fractal_server/app/routes/api/v2/project.py +5 -1
- fractal_server/app/routes/api/v2/submit.py +32 -24
- fractal_server/app/routes/api/v2/task.py +5 -0
- fractal_server/app/routes/api/v2/task_collection.py +36 -47
- fractal_server/app/routes/api/v2/task_collection_custom.py +11 -5
- fractal_server/app/routes/api/v2/task_collection_pixi.py +34 -40
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +39 -82
- fractal_server/app/routes/api/v2/workflow_import.py +4 -3
- fractal_server/app/routes/auth/_aux_auth.py +3 -3
- fractal_server/app/routes/auth/current_user.py +45 -7
- fractal_server/app/routes/auth/oauth.py +1 -1
- fractal_server/app/routes/auth/users.py +9 -0
- fractal_server/app/routes/aux/_runner.py +2 -1
- fractal_server/app/routes/aux/validate_user_profile.py +62 -0
- fractal_server/app/routes/aux/validate_user_settings.py +12 -9
- fractal_server/app/schemas/user.py +20 -13
- fractal_server/app/schemas/user_settings.py +0 -4
- fractal_server/app/schemas/v2/__init__.py +11 -0
- fractal_server/app/schemas/v2/profile.py +72 -0
- fractal_server/app/schemas/v2/resource.py +117 -0
- fractal_server/app/security/__init__.py +6 -13
- fractal_server/app/security/signup_email.py +2 -2
- fractal_server/app/user_settings.py +2 -12
- fractal_server/config/__init__.py +23 -0
- fractal_server/config/_database.py +58 -0
- fractal_server/config/_email.py +170 -0
- fractal_server/config/_init_data.py +27 -0
- fractal_server/config/_main.py +216 -0
- fractal_server/config/_settings_config.py +7 -0
- fractal_server/images/tools.py +3 -3
- fractal_server/logger.py +3 -3
- fractal_server/main.py +14 -21
- fractal_server/migrations/versions/90f6508c6379_drop_useroauth_username.py +36 -0
- fractal_server/migrations/versions/a80ac5a352bf_resource_profile.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 +128 -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 +59 -25
- fractal_server/runner/executors/slurm_common/get_slurm_config.py +38 -54
- fractal_server/runner/executors/slurm_common/remote.py +1 -1
- fractal_server/runner/executors/slurm_common/{_slurm_config.py → slurm_config.py} +3 -254
- 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 +40 -4
- fractal_server/runner/v2/_slurm_sudo.py +41 -11
- 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 +87 -108
- 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 +4 -0
- 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_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.0a0.dist-info}/METADATA +5 -5
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0a0.dist-info}/RECORD +112 -90
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0a0.dist-info}/WHEEL +1 -1
- fractal_server/config.py +0 -906
- /fractal_server/{runner → app}/shutdown.py +0 -0
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0a0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.16.5.dist-info → fractal_server-2.17.0a0.dist-info/licenses}/LICENSE +0 -0
|
@@ -5,7 +5,6 @@ from fastapi import HTTPException
|
|
|
5
5
|
from fastapi import Response
|
|
6
6
|
from fastapi import status
|
|
7
7
|
|
|
8
|
-
from ...aux.validate_user_settings import validate_user_settings
|
|
9
8
|
from ._aux_functions_task_lifecycle import check_no_ongoing_activity
|
|
10
9
|
from ._aux_functions_task_lifecycle import check_no_related_workflowtask
|
|
11
10
|
from ._aux_functions_task_lifecycle import check_no_submitted_job
|
|
@@ -15,15 +14,16 @@ from fractal_server.app.db import get_async_db
|
|
|
15
14
|
from fractal_server.app.models import UserOAuth
|
|
16
15
|
from fractal_server.app.models.v2 import TaskGroupActivityV2
|
|
17
16
|
from fractal_server.app.routes.auth import current_active_user
|
|
17
|
+
from fractal_server.app.routes.aux.validate_user_profile import (
|
|
18
|
+
validate_user_profile,
|
|
19
|
+
)
|
|
20
|
+
from fractal_server.app.schemas.v2 import ResourceType
|
|
18
21
|
from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
|
|
19
22
|
from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
|
|
20
23
|
from fractal_server.app.schemas.v2 import TaskGroupActivityV2Read
|
|
21
24
|
from fractal_server.app.schemas.v2 import TaskGroupReadV2
|
|
22
25
|
from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
|
|
23
|
-
from fractal_server.config import get_settings
|
|
24
26
|
from fractal_server.logger import set_logger
|
|
25
|
-
from fractal_server.ssh._fabric import SSHConfig
|
|
26
|
-
from fractal_server.syringe import Inject
|
|
27
27
|
from fractal_server.tasks.v2.local import deactivate_local
|
|
28
28
|
from fractal_server.tasks.v2.local import deactivate_local_pixi
|
|
29
29
|
from fractal_server.tasks.v2.local import delete_local
|
|
@@ -56,6 +56,10 @@ async def deactivate_task_group(
|
|
|
56
56
|
"""
|
|
57
57
|
Deactivate task-group venv
|
|
58
58
|
"""
|
|
59
|
+
|
|
60
|
+
# Get validated resource and profile
|
|
61
|
+
resource, profile = await validate_user_profile(user=user, db=db)
|
|
62
|
+
|
|
59
63
|
# Check access
|
|
60
64
|
task_group = await _get_task_group_full_access(
|
|
61
65
|
task_group_id=task_group_id,
|
|
@@ -116,41 +120,23 @@ async def deactivate_task_group(
|
|
|
116
120
|
await db.commit()
|
|
117
121
|
|
|
118
122
|
# Submit background task
|
|
119
|
-
|
|
120
|
-
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
|
121
|
-
# Validate user settings (backend-specific)
|
|
122
|
-
user_settings = await validate_user_settings(
|
|
123
|
-
user=user, backend=settings.FRACTAL_RUNNER_BACKEND, db=db
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
# User appropriate FractalSSH object
|
|
127
|
-
ssh_config = SSHConfig(
|
|
128
|
-
user=user_settings.ssh_username,
|
|
129
|
-
host=user_settings.ssh_host,
|
|
130
|
-
key_path=user_settings.ssh_private_key_path,
|
|
131
|
-
)
|
|
123
|
+
if resource.type == ResourceType.SLURM_SSH:
|
|
132
124
|
if task_group.origin == TaskGroupV2OriginEnum.PIXI:
|
|
133
125
|
deactivate_function = deactivate_ssh_pixi
|
|
134
126
|
else:
|
|
135
127
|
deactivate_function = deactivate_ssh
|
|
136
|
-
background_tasks.add_task(
|
|
137
|
-
deactivate_function,
|
|
138
|
-
task_group_id=task_group.id,
|
|
139
|
-
task_group_activity_id=task_group_activity.id,
|
|
140
|
-
ssh_config=ssh_config,
|
|
141
|
-
tasks_base_dir=user_settings.ssh_tasks_dir,
|
|
142
|
-
)
|
|
143
|
-
|
|
144
128
|
else:
|
|
145
129
|
if task_group.origin == TaskGroupV2OriginEnum.PIXI:
|
|
146
130
|
deactivate_function = deactivate_local_pixi
|
|
147
131
|
else:
|
|
148
132
|
deactivate_function = deactivate_local
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
133
|
+
background_tasks.add_task(
|
|
134
|
+
deactivate_function,
|
|
135
|
+
task_group_id=task_group.id,
|
|
136
|
+
task_group_activity_id=task_group_activity.id,
|
|
137
|
+
resource=resource,
|
|
138
|
+
profile=profile,
|
|
139
|
+
)
|
|
154
140
|
|
|
155
141
|
logger.debug(
|
|
156
142
|
"Task group deactivation endpoint: start deactivate "
|
|
@@ -174,6 +160,8 @@ async def reactivate_task_group(
|
|
|
174
160
|
"""
|
|
175
161
|
Deactivate task-group venv
|
|
176
162
|
"""
|
|
163
|
+
# Get validated resource and profile
|
|
164
|
+
resource, profile = await validate_user_profile(user=user, db=db)
|
|
177
165
|
|
|
178
166
|
# Check access
|
|
179
167
|
task_group = await _get_task_group_full_access(
|
|
@@ -242,42 +230,23 @@ async def reactivate_task_group(
|
|
|
242
230
|
await db.commit()
|
|
243
231
|
|
|
244
232
|
# Submit background task
|
|
245
|
-
|
|
246
|
-
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
|
247
|
-
# Validate user settings (backend-specific)
|
|
248
|
-
user_settings = await validate_user_settings(
|
|
249
|
-
user=user, backend=settings.FRACTAL_RUNNER_BACKEND, db=db
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
# Use appropriate SSH credentials
|
|
253
|
-
ssh_config = SSHConfig(
|
|
254
|
-
user=user_settings.ssh_username,
|
|
255
|
-
host=user_settings.ssh_host,
|
|
256
|
-
key_path=user_settings.ssh_private_key_path,
|
|
257
|
-
)
|
|
258
|
-
|
|
233
|
+
if resource.type == ResourceType.SLURM_SSH:
|
|
259
234
|
if task_group.origin == TaskGroupV2OriginEnum.PIXI:
|
|
260
235
|
reactivate_function = reactivate_ssh_pixi
|
|
261
236
|
else:
|
|
262
237
|
reactivate_function = reactivate_ssh
|
|
263
|
-
background_tasks.add_task(
|
|
264
|
-
reactivate_function,
|
|
265
|
-
task_group_id=task_group.id,
|
|
266
|
-
task_group_activity_id=task_group_activity.id,
|
|
267
|
-
ssh_config=ssh_config,
|
|
268
|
-
tasks_base_dir=user_settings.ssh_tasks_dir,
|
|
269
|
-
)
|
|
270
|
-
|
|
271
238
|
else:
|
|
272
239
|
if task_group.origin == TaskGroupV2OriginEnum.PIXI:
|
|
273
240
|
reactivate_function = reactivate_local_pixi
|
|
274
241
|
else:
|
|
275
242
|
reactivate_function = reactivate_local
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
243
|
+
background_tasks.add_task(
|
|
244
|
+
reactivate_function,
|
|
245
|
+
task_group_id=task_group.id,
|
|
246
|
+
task_group_activity_id=task_group_activity.id,
|
|
247
|
+
resource=resource,
|
|
248
|
+
profile=profile,
|
|
249
|
+
)
|
|
281
250
|
logger.debug(
|
|
282
251
|
"Task group reactivation endpoint: start reactivate "
|
|
283
252
|
"and return task_group_activity"
|
|
@@ -300,6 +269,8 @@ async def delete_task_group(
|
|
|
300
269
|
"""
|
|
301
270
|
Deletion of task-group from db and file system
|
|
302
271
|
"""
|
|
272
|
+
# Get validated resource and profile
|
|
273
|
+
resource, profile = await validate_user_profile(user=user, db=db)
|
|
303
274
|
|
|
304
275
|
task_group = await _get_task_group_full_access(
|
|
305
276
|
task_group_id=task_group_id,
|
|
@@ -321,32 +292,18 @@ async def delete_task_group(
|
|
|
321
292
|
db.add(task_group_activity)
|
|
322
293
|
await db.commit()
|
|
323
294
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
# Validate user settings (backend-specific)
|
|
327
|
-
user_settings = await validate_user_settings(
|
|
328
|
-
user=user, backend=settings.FRACTAL_RUNNER_BACKEND, db=db
|
|
329
|
-
)
|
|
330
|
-
# User appropriate FractalSSH object
|
|
331
|
-
ssh_config = SSHConfig(
|
|
332
|
-
user=user_settings.ssh_username,
|
|
333
|
-
host=user_settings.ssh_host,
|
|
334
|
-
key_path=user_settings.ssh_private_key_path,
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
background_tasks.add_task(
|
|
338
|
-
delete_ssh,
|
|
339
|
-
task_group_id=task_group.id,
|
|
340
|
-
task_group_activity_id=task_group_activity.id,
|
|
341
|
-
ssh_config=ssh_config,
|
|
342
|
-
tasks_base_dir=user_settings.ssh_tasks_dir,
|
|
343
|
-
)
|
|
295
|
+
if resource.type == ResourceType.SLURM_SSH:
|
|
296
|
+
delete_function = delete_ssh
|
|
344
297
|
else:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
298
|
+
delete_function = delete_local
|
|
299
|
+
|
|
300
|
+
background_tasks.add_task(
|
|
301
|
+
delete_function,
|
|
302
|
+
task_group_id=task_group.id,
|
|
303
|
+
task_group_activity_id=task_group_activity.id,
|
|
304
|
+
resource=resource,
|
|
305
|
+
profile=profile,
|
|
306
|
+
)
|
|
350
307
|
|
|
351
308
|
response.status_code = status.HTTP_202_ACCEPTED
|
|
352
309
|
return task_group_activity
|
|
@@ -149,7 +149,7 @@ async def _get_task_by_taskimport(
|
|
|
149
149
|
|
|
150
150
|
# Filter task groups by version
|
|
151
151
|
final_matching_task_groups = list(
|
|
152
|
-
filter(lambda tg: tg.version == version,
|
|
152
|
+
filter(lambda tg: tg.version == version, matching_task_groups)
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
if len(final_matching_task_groups) < 1:
|
|
@@ -167,10 +167,11 @@ async def _get_task_by_taskimport(
|
|
|
167
167
|
else:
|
|
168
168
|
logger.info(
|
|
169
169
|
"[_get_task_by_taskimport] "
|
|
170
|
-
"Found
|
|
170
|
+
f"Found {len(final_matching_task_groups)} task groups, "
|
|
171
|
+
"after filtering by version."
|
|
171
172
|
)
|
|
172
173
|
final_task_group = await _disambiguate_task_groups(
|
|
173
|
-
matching_task_groups=
|
|
174
|
+
matching_task_groups=final_matching_task_groups,
|
|
174
175
|
user_id=user_id,
|
|
175
176
|
db=db,
|
|
176
177
|
default_group_id=default_group_id,
|
|
@@ -22,7 +22,7 @@ async def _get_single_user_with_groups(
|
|
|
22
22
|
"""
|
|
23
23
|
Enrich a user object by filling its `group_ids_names` attribute.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Args:
|
|
26
26
|
user: The current `UserOAuth` object
|
|
27
27
|
db: Async db session
|
|
28
28
|
|
|
@@ -75,7 +75,7 @@ async def _get_single_usergroup_with_user_ids(
|
|
|
75
75
|
"""
|
|
76
76
|
Get a group, and construct its `user_ids` list.
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Args:
|
|
79
79
|
group_id:
|
|
80
80
|
db:
|
|
81
81
|
|
|
@@ -98,7 +98,7 @@ async def _user_or_404(user_id: int, db: AsyncSession) -> UserOAuth:
|
|
|
98
98
|
"""
|
|
99
99
|
Get a user from db, or raise a 404 HTTP exception if missing.
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
Args:
|
|
102
102
|
user_id: ID of the user
|
|
103
103
|
db: Async db session
|
|
104
104
|
"""
|
|
@@ -9,19 +9,26 @@ from fastapi_users import schemas
|
|
|
9
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
10
|
from sqlmodel import select
|
|
11
11
|
|
|
12
|
-
from . import
|
|
13
|
-
from ...db import get_async_db
|
|
14
|
-
from ...schemas.user import UserRead
|
|
15
|
-
from ...schemas.user import UserUpdate
|
|
16
|
-
from ...schemas.user import UserUpdateStrict
|
|
17
|
-
from ..aux.validate_user_settings import verify_user_has_settings
|
|
18
|
-
from ._aux_auth import _get_single_user_with_groups
|
|
12
|
+
from fractal_server.app.db import get_async_db
|
|
19
13
|
from fractal_server.app.models import LinkUserGroup
|
|
14
|
+
from fractal_server.app.models import Profile
|
|
15
|
+
from fractal_server.app.models import Resource
|
|
20
16
|
from fractal_server.app.models import UserGroup
|
|
21
17
|
from fractal_server.app.models import UserOAuth
|
|
22
18
|
from fractal_server.app.models import UserSettings
|
|
19
|
+
from fractal_server.app.routes.auth import current_active_user
|
|
20
|
+
from fractal_server.app.routes.auth._aux_auth import (
|
|
21
|
+
_get_single_user_with_groups,
|
|
22
|
+
)
|
|
23
|
+
from fractal_server.app.routes.aux.validate_user_settings import (
|
|
24
|
+
verify_user_has_settings,
|
|
25
|
+
)
|
|
26
|
+
from fractal_server.app.schemas import UserProfileInfo
|
|
23
27
|
from fractal_server.app.schemas import UserSettingsReadStrict
|
|
24
28
|
from fractal_server.app.schemas import UserSettingsUpdateStrict
|
|
29
|
+
from fractal_server.app.schemas.user import UserRead
|
|
30
|
+
from fractal_server.app.schemas.user import UserUpdate
|
|
31
|
+
from fractal_server.app.schemas.user import UserUpdateStrict
|
|
25
32
|
from fractal_server.app.security import get_user_manager
|
|
26
33
|
from fractal_server.app.security import UserManager
|
|
27
34
|
from fractal_server.config import get_settings
|
|
@@ -110,6 +117,37 @@ async def patch_current_user_settings(
|
|
|
110
117
|
return current_user_settings
|
|
111
118
|
|
|
112
119
|
|
|
120
|
+
@router_current_user.get(
|
|
121
|
+
"/current-user/profile-info/",
|
|
122
|
+
response_model=UserProfileInfo,
|
|
123
|
+
)
|
|
124
|
+
async def get_current_user_profile_info(
|
|
125
|
+
current_user: UserOAuth = Depends(current_active_user),
|
|
126
|
+
db: AsyncSession = Depends(get_async_db),
|
|
127
|
+
) -> UserProfileInfo:
|
|
128
|
+
stm = (
|
|
129
|
+
select(Resource, Profile)
|
|
130
|
+
.join(UserOAuth)
|
|
131
|
+
.where(Resource.id == Profile.resource_id)
|
|
132
|
+
.where(Profile.id == UserOAuth.profile_id)
|
|
133
|
+
.where(UserOAuth.id == current_user.id)
|
|
134
|
+
)
|
|
135
|
+
res = await db.execute(stm)
|
|
136
|
+
db_data = res.one_or_none()
|
|
137
|
+
if db_data is None:
|
|
138
|
+
response_data = dict(has_profile=False)
|
|
139
|
+
else:
|
|
140
|
+
resource, profile = db_data
|
|
141
|
+
response_data = dict(
|
|
142
|
+
has_profile=True,
|
|
143
|
+
resource_name=resource.name,
|
|
144
|
+
profile_name=profile.name,
|
|
145
|
+
username=profile.username,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return response_data
|
|
149
|
+
|
|
150
|
+
|
|
113
151
|
@router_current_user.get(
|
|
114
152
|
"/current-user/allowed-viewer-paths/", response_model=list[str]
|
|
115
153
|
)
|
|
@@ -15,7 +15,7 @@ router_oauth = APIRouter()
|
|
|
15
15
|
# environment variables (e.g. by setting OAUTH_FOO_CLIENT_ID and
|
|
16
16
|
# OAUTH_FOO_CLIENT_SECRET), this list is empty
|
|
17
17
|
|
|
18
|
-
#
|
|
18
|
+
# Note: dependency injection should be wrapped within a function call to make
|
|
19
19
|
# it truly lazy. This function could then be called on startup of the FastAPI
|
|
20
20
|
# app (cf. fractal_server.main)
|
|
21
21
|
settings = Inject(get_settings)
|
|
@@ -24,6 +24,7 @@ from fractal_server.app.models import LinkUserGroup
|
|
|
24
24
|
from fractal_server.app.models import UserGroup
|
|
25
25
|
from fractal_server.app.models import UserOAuth
|
|
26
26
|
from fractal_server.app.models import UserSettings
|
|
27
|
+
from fractal_server.app.models.v2 import Profile
|
|
27
28
|
from fractal_server.app.routes.auth._aux_auth import _user_or_404
|
|
28
29
|
from fractal_server.app.schemas import UserSettingsRead
|
|
29
30
|
from fractal_server.app.schemas import UserSettingsUpdate
|
|
@@ -67,6 +68,14 @@ async def patch_user(
|
|
|
67
68
|
# Check that user exists
|
|
68
69
|
user_to_patch = await _user_or_404(user_id, db)
|
|
69
70
|
|
|
71
|
+
if user_update.profile_id is not None:
|
|
72
|
+
profile = await db.get(Profile, user_update.profile_id)
|
|
73
|
+
if profile is None:
|
|
74
|
+
raise HTTPException(
|
|
75
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
76
|
+
detail=f"Profile {user_update.profile_id} not found.",
|
|
77
|
+
)
|
|
78
|
+
|
|
70
79
|
# Modify user attributes
|
|
71
80
|
try:
|
|
72
81
|
user = await user_manager.update(
|
|
@@ -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 validate_profile_data
|
|
10
|
+
from fractal_server.app.schemas.v2.resource import validate_resource_data
|
|
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
|
+
validate_resource_data(
|
|
42
|
+
resource.model_dump(exclude={"id", "timestamp_created"}),
|
|
43
|
+
)
|
|
44
|
+
validate_profile_data(
|
|
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
|
+
)
|
|
@@ -5,10 +5,12 @@ from pydantic import ValidationError
|
|
|
5
5
|
from fractal_server.app.db import AsyncSession
|
|
6
6
|
from fractal_server.app.models import UserOAuth
|
|
7
7
|
from fractal_server.app.models import UserSettings
|
|
8
|
+
from fractal_server.app.schemas.v2 import ResourceType
|
|
8
9
|
from fractal_server.app.user_settings import SlurmSshUserSettings
|
|
9
10
|
from fractal_server.app.user_settings import SlurmSudoUserSettings
|
|
10
11
|
from fractal_server.logger import set_logger
|
|
11
12
|
|
|
13
|
+
|
|
12
14
|
logger = set_logger(__name__)
|
|
13
15
|
|
|
14
16
|
|
|
@@ -19,7 +21,7 @@ def verify_user_has_settings(user: UserOAuth) -> None:
|
|
|
19
21
|
NOTE: This check will become useless when we make the foreign-key column
|
|
20
22
|
required, but for the moment (as of v2.6.0) we have to keep it in place.
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
Args:
|
|
23
25
|
user: The user to be checked.
|
|
24
26
|
"""
|
|
25
27
|
if user.user_settings_id is None:
|
|
@@ -35,7 +37,7 @@ async def validate_user_settings(
|
|
|
35
37
|
"""
|
|
36
38
|
Get a UserSettings object and validate it based on a given Fractal backend.
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
Args:
|
|
39
41
|
user: The user whose settings we should validate.
|
|
40
42
|
backend: The value of `FRACTAL_RUNNER_BACKEND`
|
|
41
43
|
db: An async DB session
|
|
@@ -48,13 +50,14 @@ async def validate_user_settings(
|
|
|
48
50
|
|
|
49
51
|
user_settings = await db.get(UserSettings, user.user_settings_id)
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
match backend:
|
|
54
|
+
case ResourceType.SLURM_SSH:
|
|
55
|
+
UserSettingsValidationModel = SlurmSshUserSettings
|
|
56
|
+
case ResourceType.SLURM_SUDO:
|
|
57
|
+
UserSettingsValidationModel = SlurmSudoUserSettings
|
|
58
|
+
case _:
|
|
59
|
+
# For other backends, we don't validate anything
|
|
60
|
+
return user_settings
|
|
58
61
|
|
|
59
62
|
try:
|
|
60
63
|
UserSettingsValidationModel(**user_settings.model_dump())
|
|
@@ -7,13 +7,6 @@ from pydantic import Field
|
|
|
7
7
|
from fractal_server.types import ListUniqueNonNegativeInt
|
|
8
8
|
from fractal_server.types import NonEmptyStr
|
|
9
9
|
|
|
10
|
-
__all__ = (
|
|
11
|
-
"UserRead",
|
|
12
|
-
"UserUpdate",
|
|
13
|
-
"UserUpdateGroups",
|
|
14
|
-
"UserCreate",
|
|
15
|
-
)
|
|
16
|
-
|
|
17
10
|
|
|
18
11
|
class OAuthAccountRead(BaseModel):
|
|
19
12
|
"""
|
|
@@ -36,12 +29,14 @@ class UserRead(schemas.BaseUser[int]):
|
|
|
36
29
|
Schema for `User` read from database.
|
|
37
30
|
|
|
38
31
|
Attributes:
|
|
39
|
-
|
|
32
|
+
group_ids_names:
|
|
33
|
+
oauth_accounts:
|
|
34
|
+
profile_id:
|
|
40
35
|
"""
|
|
41
36
|
|
|
42
|
-
username: str | None = None
|
|
43
37
|
group_ids_names: list[tuple[int, str]] | None = None
|
|
44
38
|
oauth_accounts: list[OAuthAccountRead]
|
|
39
|
+
profile_id: int | None = None
|
|
45
40
|
|
|
46
41
|
|
|
47
42
|
class UserUpdate(schemas.BaseUserUpdate):
|
|
@@ -49,16 +44,21 @@ class UserUpdate(schemas.BaseUserUpdate):
|
|
|
49
44
|
Schema for `User` update.
|
|
50
45
|
|
|
51
46
|
Attributes:
|
|
52
|
-
|
|
47
|
+
password:
|
|
48
|
+
email:
|
|
49
|
+
is_active:
|
|
50
|
+
is_superuser:
|
|
51
|
+
is_verified:
|
|
52
|
+
profile_id:
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
model_config = ConfigDict(extra="forbid")
|
|
56
|
-
username: NonEmptyStr = None
|
|
57
56
|
password: NonEmptyStr = None
|
|
58
57
|
email: EmailStr = None
|
|
59
58
|
is_active: bool = None
|
|
60
59
|
is_superuser: bool = None
|
|
61
60
|
is_verified: bool = None
|
|
61
|
+
profile_id: int | None = None
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
class UserUpdateStrict(BaseModel):
|
|
@@ -76,10 +76,10 @@ class UserCreate(schemas.BaseUserCreate):
|
|
|
76
76
|
Schema for `User` creation.
|
|
77
77
|
|
|
78
78
|
Attributes:
|
|
79
|
-
|
|
79
|
+
profile_id:
|
|
80
80
|
"""
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
profile_id: int | None = None
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
class UserUpdateGroups(BaseModel):
|
|
@@ -91,3 +91,10 @@ class UserUpdateGroups(BaseModel):
|
|
|
91
91
|
model_config = ConfigDict(extra="forbid")
|
|
92
92
|
|
|
93
93
|
group_ids: ListUniqueNonNegativeInt = Field(min_length=1)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class UserProfileInfo(BaseModel):
|
|
97
|
+
has_profile: bool
|
|
98
|
+
resource_name: str | None = None
|
|
99
|
+
profile_name: str | None = None
|
|
100
|
+
username: str | None = None
|
|
@@ -24,8 +24,6 @@ class UserSettingsRead(BaseModel):
|
|
|
24
24
|
ssh_host: str | None = None
|
|
25
25
|
ssh_username: str | None = None
|
|
26
26
|
ssh_private_key_path: str | None = None
|
|
27
|
-
ssh_tasks_dir: str | None = None
|
|
28
|
-
ssh_jobs_dir: str | None = None
|
|
29
27
|
slurm_user: str | None = None
|
|
30
28
|
slurm_accounts: list[str]
|
|
31
29
|
project_dir: str | None = None
|
|
@@ -48,8 +46,6 @@ class UserSettingsUpdate(BaseModel):
|
|
|
48
46
|
ssh_host: NonEmptyStr | None = None
|
|
49
47
|
ssh_username: NonEmptyStr | None = None
|
|
50
48
|
ssh_private_key_path: AbsolutePathStr | None = None
|
|
51
|
-
ssh_tasks_dir: AbsolutePathStr | None = None
|
|
52
|
-
ssh_jobs_dir: AbsolutePathStr | None = None
|
|
53
49
|
slurm_user: NonEmptyStr | None = None
|
|
54
50
|
slurm_accounts: ListUniqueNonEmptyString | None = None
|
|
55
51
|
project_dir: AbsolutePathStr | None = None
|
|
@@ -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
|
|
@@ -0,0 +1,72 @@
|
|
|
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 validate_profile_data(_data: ProfileCreate):
|
|
69
|
+
"""
|
|
70
|
+
We use `@validate_call` because `ProfileCreate` is a `Union` type and it
|
|
71
|
+
cannot be instantiated directly.
|
|
72
|
+
"""
|