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.
Files changed (113) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +129 -22
  3. fractal_server/app/db/__init__.py +9 -11
  4. fractal_server/app/models/security.py +7 -3
  5. fractal_server/app/models/user_settings.py +0 -4
  6. fractal_server/app/models/v2/__init__.py +4 -0
  7. fractal_server/app/models/v2/job.py +3 -4
  8. fractal_server/app/models/v2/profile.py +16 -0
  9. fractal_server/app/models/v2/project.py +3 -0
  10. fractal_server/app/models/v2/resource.py +130 -0
  11. fractal_server/app/models/v2/task_group.py +3 -0
  12. fractal_server/app/routes/admin/v2/__init__.py +4 -0
  13. fractal_server/app/routes/admin/v2/_aux_functions.py +55 -0
  14. fractal_server/app/routes/admin/v2/profile.py +86 -0
  15. fractal_server/app/routes/admin/v2/resource.py +229 -0
  16. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +48 -82
  17. fractal_server/app/routes/api/__init__.py +26 -7
  18. fractal_server/app/routes/api/v2/_aux_functions.py +27 -1
  19. fractal_server/app/routes/api/v2/_aux_functions_history.py +2 -2
  20. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
  21. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +7 -7
  22. fractal_server/app/routes/api/v2/project.py +5 -1
  23. fractal_server/app/routes/api/v2/submit.py +32 -24
  24. fractal_server/app/routes/api/v2/task.py +5 -0
  25. fractal_server/app/routes/api/v2/task_collection.py +36 -47
  26. fractal_server/app/routes/api/v2/task_collection_custom.py +11 -5
  27. fractal_server/app/routes/api/v2/task_collection_pixi.py +34 -40
  28. fractal_server/app/routes/api/v2/task_group_lifecycle.py +39 -82
  29. fractal_server/app/routes/api/v2/workflow_import.py +4 -3
  30. fractal_server/app/routes/auth/_aux_auth.py +3 -3
  31. fractal_server/app/routes/auth/current_user.py +45 -7
  32. fractal_server/app/routes/auth/oauth.py +1 -1
  33. fractal_server/app/routes/auth/users.py +9 -0
  34. fractal_server/app/routes/aux/_runner.py +2 -1
  35. fractal_server/app/routes/aux/validate_user_profile.py +62 -0
  36. fractal_server/app/routes/aux/validate_user_settings.py +12 -9
  37. fractal_server/app/schemas/user.py +20 -13
  38. fractal_server/app/schemas/user_settings.py +0 -4
  39. fractal_server/app/schemas/v2/__init__.py +11 -0
  40. fractal_server/app/schemas/v2/profile.py +72 -0
  41. fractal_server/app/schemas/v2/resource.py +117 -0
  42. fractal_server/app/security/__init__.py +6 -13
  43. fractal_server/app/security/signup_email.py +2 -2
  44. fractal_server/app/user_settings.py +2 -12
  45. fractal_server/config/__init__.py +23 -0
  46. fractal_server/config/_database.py +58 -0
  47. fractal_server/config/_email.py +170 -0
  48. fractal_server/config/_init_data.py +27 -0
  49. fractal_server/config/_main.py +216 -0
  50. fractal_server/config/_settings_config.py +7 -0
  51. fractal_server/images/tools.py +3 -3
  52. fractal_server/logger.py +3 -3
  53. fractal_server/main.py +14 -21
  54. fractal_server/migrations/versions/90f6508c6379_drop_useroauth_username.py +36 -0
  55. fractal_server/migrations/versions/a80ac5a352bf_resource_profile.py +195 -0
  56. fractal_server/runner/config/__init__.py +2 -0
  57. fractal_server/runner/config/_local.py +21 -0
  58. fractal_server/runner/config/_slurm.py +128 -0
  59. fractal_server/runner/config/slurm_mem_to_MB.py +63 -0
  60. fractal_server/runner/exceptions.py +4 -0
  61. fractal_server/runner/executors/base_runner.py +17 -7
  62. fractal_server/runner/executors/local/get_local_config.py +21 -86
  63. fractal_server/runner/executors/local/runner.py +48 -5
  64. fractal_server/runner/executors/slurm_common/_batching.py +2 -2
  65. fractal_server/runner/executors/slurm_common/base_slurm_runner.py +59 -25
  66. fractal_server/runner/executors/slurm_common/get_slurm_config.py +38 -54
  67. fractal_server/runner/executors/slurm_common/remote.py +1 -1
  68. fractal_server/runner/executors/slurm_common/{_slurm_config.py → slurm_config.py} +3 -254
  69. fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -1
  70. fractal_server/runner/executors/slurm_ssh/runner.py +12 -14
  71. fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +2 -2
  72. fractal_server/runner/executors/slurm_sudo/runner.py +12 -12
  73. fractal_server/runner/v2/_local.py +36 -21
  74. fractal_server/runner/v2/_slurm_ssh.py +40 -4
  75. fractal_server/runner/v2/_slurm_sudo.py +41 -11
  76. fractal_server/runner/v2/db_tools.py +1 -1
  77. fractal_server/runner/v2/runner.py +3 -11
  78. fractal_server/runner/v2/runner_functions.py +42 -28
  79. fractal_server/runner/v2/submit_workflow.py +87 -108
  80. fractal_server/runner/versions.py +8 -3
  81. fractal_server/ssh/_fabric.py +6 -6
  82. fractal_server/tasks/config/__init__.py +3 -0
  83. fractal_server/tasks/config/_pixi.py +127 -0
  84. fractal_server/tasks/config/_python.py +51 -0
  85. fractal_server/tasks/v2/local/_utils.py +7 -7
  86. fractal_server/tasks/v2/local/collect.py +13 -5
  87. fractal_server/tasks/v2/local/collect_pixi.py +26 -10
  88. fractal_server/tasks/v2/local/deactivate.py +7 -1
  89. fractal_server/tasks/v2/local/deactivate_pixi.py +5 -1
  90. fractal_server/tasks/v2/local/delete.py +4 -0
  91. fractal_server/tasks/v2/local/reactivate.py +13 -5
  92. fractal_server/tasks/v2/local/reactivate_pixi.py +27 -9
  93. fractal_server/tasks/v2/ssh/_pixi_slurm_ssh.py +11 -10
  94. fractal_server/tasks/v2/ssh/_utils.py +6 -7
  95. fractal_server/tasks/v2/ssh/collect.py +19 -12
  96. fractal_server/tasks/v2/ssh/collect_pixi.py +34 -16
  97. fractal_server/tasks/v2/ssh/deactivate.py +12 -8
  98. fractal_server/tasks/v2/ssh/deactivate_pixi.py +14 -10
  99. fractal_server/tasks/v2/ssh/delete.py +12 -9
  100. fractal_server/tasks/v2/ssh/reactivate.py +18 -12
  101. fractal_server/tasks/v2/ssh/reactivate_pixi.py +36 -17
  102. fractal_server/tasks/v2/templates/4_pip_show.sh +4 -6
  103. fractal_server/tasks/v2/utils_database.py +2 -2
  104. fractal_server/tasks/v2/utils_python_interpreter.py +8 -16
  105. fractal_server/tasks/v2/utils_templates.py +7 -10
  106. fractal_server/utils.py +1 -1
  107. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0a0.dist-info}/METADATA +5 -5
  108. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0a0.dist-info}/RECORD +112 -90
  109. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0a0.dist-info}/WHEEL +1 -1
  110. fractal_server/config.py +0 -906
  111. /fractal_server/{runner → app}/shutdown.py +0 -0
  112. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0a0.dist-info}/entry_points.txt +0 -0
  113. {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
- settings = Inject(get_settings)
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
- background_tasks.add_task(
150
- deactivate_function,
151
- task_group_id=task_group.id,
152
- task_group_activity_id=task_group_activity.id,
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
- settings = Inject(get_settings)
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
- background_tasks.add_task(
277
- reactivate_function,
278
- task_group_id=task_group.id,
279
- task_group_activity_id=task_group_activity.id,
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
- settings = Inject(get_settings)
325
- if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
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
- background_tasks.add_task(
346
- delete_local,
347
- task_group_id=task_group.id,
348
- task_group_activity_id=task_group_activity.id,
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, task_groups_list)
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 many task groups, after filtering by version."
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=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
- Arguments:
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
- Arguments:
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
- Arguments:
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 current_active_user
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
- # FIXME:Dependency injection should be wrapped within a function call to make
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 ["slurm", "slurm_ssh"]:
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
- Arguments:
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
- Arguments:
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
- if backend == "slurm_ssh":
52
- UserSettingsValidationModel = SlurmSshUserSettings
53
- elif backend == "slurm":
54
- UserSettingsValidationModel = SlurmSudoUserSettings
55
- else:
56
- # For other backends, we don't validate anything
57
- return user_settings
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
- username:
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
- username:
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
- username:
79
+ profile_id:
80
80
  """
81
81
 
82
- username: NonEmptyStr = None
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
+ """