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.
Files changed (143) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +178 -52
  3. fractal_server/app/db/__init__.py +9 -11
  4. fractal_server/app/models/security.py +30 -22
  5. fractal_server/app/models/user_settings.py +5 -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 +5 -0
  10. fractal_server/app/models/v2/resource.py +130 -0
  11. fractal_server/app/models/v2/task_group.py +4 -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/accounting.py +3 -3
  15. fractal_server/app/routes/admin/v2/impersonate.py +2 -2
  16. fractal_server/app/routes/admin/v2/job.py +51 -15
  17. fractal_server/app/routes/admin/v2/profile.py +100 -0
  18. fractal_server/app/routes/admin/v2/project.py +2 -2
  19. fractal_server/app/routes/admin/v2/resource.py +222 -0
  20. fractal_server/app/routes/admin/v2/task.py +59 -32
  21. fractal_server/app/routes/admin/v2/task_group.py +17 -12
  22. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +52 -86
  23. fractal_server/app/routes/api/__init__.py +45 -8
  24. fractal_server/app/routes/api/v2/_aux_functions.py +17 -1
  25. fractal_server/app/routes/api/v2/_aux_functions_history.py +2 -2
  26. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
  27. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +55 -19
  28. fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +21 -17
  29. fractal_server/app/routes/api/v2/dataset.py +10 -19
  30. fractal_server/app/routes/api/v2/history.py +8 -8
  31. fractal_server/app/routes/api/v2/images.py +5 -5
  32. fractal_server/app/routes/api/v2/job.py +8 -8
  33. fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
  34. fractal_server/app/routes/api/v2/project.py +15 -7
  35. fractal_server/app/routes/api/v2/status_legacy.py +2 -2
  36. fractal_server/app/routes/api/v2/submit.py +49 -42
  37. fractal_server/app/routes/api/v2/task.py +26 -8
  38. fractal_server/app/routes/api/v2/task_collection.py +39 -50
  39. fractal_server/app/routes/api/v2/task_collection_custom.py +10 -6
  40. fractal_server/app/routes/api/v2/task_collection_pixi.py +34 -42
  41. fractal_server/app/routes/api/v2/task_group.py +19 -9
  42. fractal_server/app/routes/api/v2/task_group_lifecycle.py +43 -86
  43. fractal_server/app/routes/api/v2/task_version_update.py +3 -3
  44. fractal_server/app/routes/api/v2/workflow.py +9 -9
  45. fractal_server/app/routes/api/v2/workflow_import.py +29 -16
  46. fractal_server/app/routes/api/v2/workflowtask.py +5 -5
  47. fractal_server/app/routes/auth/__init__.py +34 -5
  48. fractal_server/app/routes/auth/_aux_auth.py +39 -20
  49. fractal_server/app/routes/auth/current_user.py +56 -67
  50. fractal_server/app/routes/auth/group.py +29 -46
  51. fractal_server/app/routes/auth/oauth.py +55 -38
  52. fractal_server/app/routes/auth/register.py +2 -2
  53. fractal_server/app/routes/auth/router.py +4 -2
  54. fractal_server/app/routes/auth/users.py +29 -53
  55. fractal_server/app/routes/aux/_runner.py +2 -1
  56. fractal_server/app/routes/aux/validate_user_profile.py +62 -0
  57. fractal_server/app/schemas/__init__.py +0 -1
  58. fractal_server/app/schemas/user.py +43 -13
  59. fractal_server/app/schemas/user_group.py +2 -1
  60. fractal_server/app/schemas/v2/__init__.py +12 -0
  61. fractal_server/app/schemas/v2/profile.py +78 -0
  62. fractal_server/app/schemas/v2/resource.py +137 -0
  63. fractal_server/app/schemas/v2/task_collection.py +11 -3
  64. fractal_server/app/schemas/v2/task_group.py +5 -0
  65. fractal_server/app/security/__init__.py +174 -75
  66. fractal_server/app/security/signup_email.py +52 -34
  67. fractal_server/config/__init__.py +27 -0
  68. fractal_server/config/_data.py +68 -0
  69. fractal_server/config/_database.py +59 -0
  70. fractal_server/config/_email.py +133 -0
  71. fractal_server/config/_main.py +78 -0
  72. fractal_server/config/_oauth.py +69 -0
  73. fractal_server/config/_settings_config.py +7 -0
  74. fractal_server/data_migrations/2_17_0.py +339 -0
  75. fractal_server/images/tools.py +3 -3
  76. fractal_server/logger.py +3 -3
  77. fractal_server/main.py +17 -23
  78. fractal_server/migrations/naming_convention.py +1 -1
  79. fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py +195 -0
  80. fractal_server/runner/config/__init__.py +2 -0
  81. fractal_server/runner/config/_local.py +21 -0
  82. fractal_server/runner/config/_slurm.py +129 -0
  83. fractal_server/runner/config/slurm_mem_to_MB.py +63 -0
  84. fractal_server/runner/exceptions.py +4 -0
  85. fractal_server/runner/executors/base_runner.py +17 -7
  86. fractal_server/runner/executors/local/get_local_config.py +21 -86
  87. fractal_server/runner/executors/local/runner.py +48 -5
  88. fractal_server/runner/executors/slurm_common/_batching.py +2 -2
  89. fractal_server/runner/executors/slurm_common/base_slurm_runner.py +60 -26
  90. fractal_server/runner/executors/slurm_common/get_slurm_config.py +39 -55
  91. fractal_server/runner/executors/slurm_common/remote.py +1 -1
  92. fractal_server/runner/executors/slurm_common/slurm_config.py +214 -0
  93. fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -1
  94. fractal_server/runner/executors/slurm_ssh/runner.py +12 -14
  95. fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +2 -2
  96. fractal_server/runner/executors/slurm_sudo/runner.py +12 -12
  97. fractal_server/runner/v2/_local.py +36 -21
  98. fractal_server/runner/v2/_slurm_ssh.py +41 -4
  99. fractal_server/runner/v2/_slurm_sudo.py +42 -12
  100. fractal_server/runner/v2/db_tools.py +1 -1
  101. fractal_server/runner/v2/runner.py +3 -11
  102. fractal_server/runner/v2/runner_functions.py +42 -28
  103. fractal_server/runner/v2/submit_workflow.py +88 -109
  104. fractal_server/runner/versions.py +8 -3
  105. fractal_server/ssh/_fabric.py +6 -6
  106. fractal_server/tasks/config/__init__.py +3 -0
  107. fractal_server/tasks/config/_pixi.py +127 -0
  108. fractal_server/tasks/config/_python.py +51 -0
  109. fractal_server/tasks/v2/local/_utils.py +7 -7
  110. fractal_server/tasks/v2/local/collect.py +13 -5
  111. fractal_server/tasks/v2/local/collect_pixi.py +26 -10
  112. fractal_server/tasks/v2/local/deactivate.py +7 -1
  113. fractal_server/tasks/v2/local/deactivate_pixi.py +5 -1
  114. fractal_server/tasks/v2/local/delete.py +5 -1
  115. fractal_server/tasks/v2/local/reactivate.py +13 -5
  116. fractal_server/tasks/v2/local/reactivate_pixi.py +27 -9
  117. fractal_server/tasks/v2/ssh/_pixi_slurm_ssh.py +11 -10
  118. fractal_server/tasks/v2/ssh/_utils.py +6 -7
  119. fractal_server/tasks/v2/ssh/collect.py +19 -12
  120. fractal_server/tasks/v2/ssh/collect_pixi.py +34 -16
  121. fractal_server/tasks/v2/ssh/deactivate.py +12 -8
  122. fractal_server/tasks/v2/ssh/deactivate_pixi.py +14 -10
  123. fractal_server/tasks/v2/ssh/delete.py +12 -9
  124. fractal_server/tasks/v2/ssh/reactivate.py +18 -12
  125. fractal_server/tasks/v2/ssh/reactivate_pixi.py +36 -17
  126. fractal_server/tasks/v2/templates/4_pip_show.sh +4 -6
  127. fractal_server/tasks/v2/utils_database.py +2 -2
  128. fractal_server/tasks/v2/utils_pixi.py +3 -0
  129. fractal_server/tasks/v2/utils_python_interpreter.py +8 -16
  130. fractal_server/tasks/v2/utils_templates.py +7 -10
  131. fractal_server/utils.py +1 -1
  132. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/METADATA +8 -10
  133. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/RECORD +137 -118
  134. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/WHEEL +1 -1
  135. fractal_server/app/routes/aux/validate_user_settings.py +0 -73
  136. fractal_server/app/schemas/user_settings.py +0 -67
  137. fractal_server/app/user_settings.py +0 -42
  138. fractal_server/config.py +0 -906
  139. fractal_server/data_migrations/2_14_10.py +0 -48
  140. fractal_server/runner/executors/slurm_common/_slurm_config.py +0 -471
  141. /fractal_server/{runner → app}/shutdown.py +0 -0
  142. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info}/entry_points.txt +0 -0
  143. {fractal_server-2.16.5.dist-info → fractal_server-2.17.0.dist-info/licenses}/LICENSE +0 -0
@@ -10,25 +10,25 @@ from fastapi import Request
10
10
  from fastapi import status
11
11
  from sqlmodel import select
12
12
 
13
+ from ...aux.validate_user_profile import validate_user_profile
13
14
  from ._aux_functions import _get_dataset_check_owner
14
15
  from ._aux_functions import _get_workflow_check_owner
15
16
  from ._aux_functions import clean_app_job_list_v2
16
17
  from ._aux_functions_tasks import _check_type_filters_compatibility
17
18
  from fractal_server.app.db import AsyncSession
18
19
  from fractal_server.app.db import get_async_db
20
+ from fractal_server.app.models import Profile
19
21
  from fractal_server.app.models import TaskGroupV2
20
22
  from fractal_server.app.models import UserOAuth
21
23
  from fractal_server.app.models.v2 import JobV2
22
24
  from fractal_server.app.routes.api.v2._aux_functions_tasks import (
23
25
  _get_task_read_access,
24
26
  )
25
- from fractal_server.app.routes.auth import current_active_verified_user
26
- from fractal_server.app.routes.aux.validate_user_settings import (
27
- validate_user_settings,
28
- )
27
+ from fractal_server.app.routes.auth import current_user_act_ver_prof
29
28
  from fractal_server.app.schemas.v2 import JobCreateV2
30
29
  from fractal_server.app.schemas.v2 import JobReadV2
31
30
  from fractal_server.app.schemas.v2 import JobStatusTypeV2
31
+ from fractal_server.app.schemas.v2 import ResourceType
32
32
  from fractal_server.config import get_settings
33
33
  from fractal_server.logger import set_logger
34
34
  from fractal_server.runner.set_start_and_last_task_index import (
@@ -37,7 +37,7 @@ from fractal_server.runner.set_start_and_last_task_index import (
37
37
  from fractal_server.runner.v2.submit_workflow import submit_workflow
38
38
  from fractal_server.syringe import Inject
39
39
 
40
-
40
+ FRACTAL_CACHE_DIR = ".fractal_cache"
41
41
  router = APIRouter()
42
42
  logger = set_logger(__name__)
43
43
 
@@ -54,11 +54,14 @@ async def apply_workflow(
54
54
  job_create: JobCreateV2,
55
55
  background_tasks: BackgroundTasks,
56
56
  request: Request,
57
- user: UserOAuth = Depends(current_active_verified_user),
57
+ user: UserOAuth = Depends(current_user_act_ver_prof),
58
58
  db: AsyncSession = Depends(get_async_db),
59
59
  ) -> JobReadV2 | None:
60
60
  # Remove non-submitted V2 jobs from the app state when the list grows
61
61
  # beyond a threshold
62
+ # NOTE: this may lead to a race condition on `app.state.jobsV2` if two
63
+ # requests take place at the same time and `clean_app_job_list_v2` is
64
+ # somewhat slow.
62
65
  settings = Inject(get_settings)
63
66
  if (
64
67
  len(request.app.state.jobsV2)
@@ -78,6 +81,17 @@ async def apply_workflow(
78
81
  project = output["project"]
79
82
  dataset = output["dataset"]
80
83
 
84
+ # Verify that user's resource matches with project resource
85
+ res = await db.execute(
86
+ select(Profile.resource_id).where(Profile.id == user.profile_id)
87
+ )
88
+ user_resource_id = res.scalar_one()
89
+ if project.resource_id != user_resource_id:
90
+ raise HTTPException(
91
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
92
+ detail="Project resource does not match with user's resource",
93
+ )
94
+
81
95
  workflow = await _get_workflow_check_owner(
82
96
  project_id=project_id, workflow_id=workflow_id, user_id=user.id, db=db
83
97
  )
@@ -122,11 +136,12 @@ async def apply_workflow(
122
136
  )
123
137
  used_task_group_ids.add(task.taskgroupv2_id)
124
138
 
125
- # Validate user settings
126
- FRACTAL_RUNNER_BACKEND = settings.FRACTAL_RUNNER_BACKEND
127
- user_settings = await validate_user_settings(
128
- user=user, backend=FRACTAL_RUNNER_BACKEND, db=db
139
+ # Get validated resource and profile
140
+ resource, profile = await validate_user_profile(
141
+ user=user,
142
+ db=db,
129
143
  )
144
+
130
145
  # Check that no other job with the same dataset_id is SUBMITTED
131
146
  stm = (
132
147
  select(JobV2)
@@ -144,7 +159,7 @@ async def apply_workflow(
144
159
  )
145
160
 
146
161
  if job_create.slurm_account is not None:
147
- if job_create.slurm_account not in user_settings.slurm_accounts:
162
+ if job_create.slurm_account not in user.slurm_accounts:
148
163
  raise HTTPException(
149
164
  status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
150
165
  detail=(
@@ -153,15 +168,15 @@ async def apply_workflow(
153
168
  ),
154
169
  )
155
170
  else:
156
- if len(user_settings.slurm_accounts) > 0:
157
- job_create.slurm_account = user_settings.slurm_accounts[0]
171
+ if len(user.slurm_accounts) > 0:
172
+ job_create.slurm_account = user.slurm_accounts[0]
158
173
 
159
174
  # User appropriate FractalSSH object
160
- if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
175
+ if resource.type == ResourceType.SLURM_SSH:
161
176
  ssh_config = dict(
162
- user=user_settings.ssh_username,
163
- host=user_settings.ssh_host,
164
- key_path=user_settings.ssh_private_key_path,
177
+ user=profile.username,
178
+ host=resource.host,
179
+ key_path=profile.ssh_key_path,
165
180
  )
166
181
  fractal_ssh_list = request.app.state.fractal_ssh_list
167
182
  try:
@@ -184,16 +199,14 @@ async def apply_workflow(
184
199
  dataset_id=dataset_id,
185
200
  workflow_id=workflow_id,
186
201
  user_email=user.email,
187
- # The 'filters' field is not supported any more but still exists as a
188
- # database column, therefore we manually exclude it from dumps.
189
202
  dataset_dump=json.loads(
190
- dataset.model_dump_json(exclude={"images", "history", "filters"})
203
+ dataset.model_dump_json(exclude={"images", "history"})
191
204
  ),
192
205
  workflow_dump=json.loads(
193
206
  workflow.model_dump_json(exclude={"task_list"})
194
207
  ),
195
208
  project_dump=json.loads(
196
- project.model_dump_json(exclude={"user_list"})
209
+ project.model_dump_json(exclude={"user_list", "resource_id"})
197
210
  ),
198
211
  **job_create.model_dump(),
199
212
  )
@@ -214,26 +227,23 @@ async def apply_workflow(
214
227
 
215
228
  # Define server-side job directory
216
229
  timestamp_string = job.start_timestamp.strftime("%Y%m%d_%H%M%S")
217
- WORKFLOW_DIR_LOCAL = settings.FRACTAL_RUNNER_WORKING_BASE_DIR / (
230
+ WORKFLOW_DIR_LOCAL = Path(resource.jobs_local_dir) / (
218
231
  f"proj_v2_{project_id:07d}_wf_{workflow_id:07d}_job_{job.id:07d}"
219
232
  f"_{timestamp_string}"
220
233
  )
221
234
 
222
- cache_dir = (
223
- Path(user_settings.project_dir) / ".fractal_cache"
224
- if user_settings.project_dir is not None
225
- else None
226
- )
227
-
228
235
  # Define user-side job directory
229
- if FRACTAL_RUNNER_BACKEND == "local":
230
- WORKFLOW_DIR_REMOTE = WORKFLOW_DIR_LOCAL
231
- elif FRACTAL_RUNNER_BACKEND == "slurm":
232
- WORKFLOW_DIR_REMOTE = cache_dir / WORKFLOW_DIR_LOCAL.name
233
- elif FRACTAL_RUNNER_BACKEND == "slurm_ssh":
234
- WORKFLOW_DIR_REMOTE = (
235
- Path(user_settings.ssh_jobs_dir) / WORKFLOW_DIR_LOCAL.name
236
- )
236
+ cache_dir = Path(user.project_dir, FRACTAL_CACHE_DIR)
237
+ match resource.type:
238
+ case ResourceType.LOCAL:
239
+ WORKFLOW_DIR_REMOTE = WORKFLOW_DIR_LOCAL
240
+ case ResourceType.SLURM_SUDO:
241
+ WORKFLOW_DIR_REMOTE = cache_dir / WORKFLOW_DIR_LOCAL.name
242
+ case ResourceType.SLURM_SSH:
243
+ WORKFLOW_DIR_REMOTE = Path(
244
+ profile.jobs_remote_dir,
245
+ WORKFLOW_DIR_LOCAL.name,
246
+ )
237
247
 
238
248
  # Update job folders in the db
239
249
  job.working_dir = WORKFLOW_DIR_LOCAL.as_posix()
@@ -241,20 +251,17 @@ async def apply_workflow(
241
251
  await db.merge(job)
242
252
  await db.commit()
243
253
 
244
- # Expunge user settings from db, to use in background task
245
- db.expunge(user_settings)
246
-
247
254
  background_tasks.add_task(
248
255
  submit_workflow,
249
256
  workflow_id=workflow.id,
250
257
  dataset_id=dataset.id,
251
258
  job_id=job.id,
252
259
  user_id=user.id,
253
- user_settings=user_settings,
254
260
  worker_init=job.worker_init,
255
- slurm_user=user_settings.slurm_user,
256
- user_cache_dir=cache_dir.as_posix() if cache_dir else None,
261
+ user_cache_dir=cache_dir.as_posix(),
257
262
  fractal_ssh=fractal_ssh,
263
+ resource=resource,
264
+ profile=profile,
258
265
  )
259
266
  request.app.state.jobsV2.append(job.id)
260
267
  logger.info(
@@ -9,6 +9,8 @@ from sqlmodel import func
9
9
  from sqlmodel import or_
10
10
  from sqlmodel import select
11
11
 
12
+ from ...aux.validate_user_profile import validate_user_profile
13
+ from ._aux_functions import _get_user_resource_id
12
14
  from ._aux_functions_tasks import _get_task_full_access
13
15
  from ._aux_functions_tasks import _get_task_read_access
14
16
  from ._aux_functions_tasks import _get_valid_user_group_id
@@ -20,8 +22,7 @@ from fractal_server.app.models import LinkUserGroup
20
22
  from fractal_server.app.models import UserOAuth
21
23
  from fractal_server.app.models.v2 import TaskGroupV2
22
24
  from fractal_server.app.models.v2 import TaskV2
23
- from fractal_server.app.routes.auth import current_active_user
24
- from fractal_server.app.routes.auth import current_active_verified_user
25
+ from fractal_server.app.routes.auth import current_user_act_ver_prof
25
26
  from fractal_server.app.schemas.v2 import TaskCreateV2
26
27
  from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
27
28
  from fractal_server.app.schemas.v2 import TaskReadV2
@@ -40,16 +41,20 @@ async def get_list_task(
40
41
  category: str | None = None,
41
42
  modality: str | None = None,
42
43
  author: str | None = None,
43
- user: UserOAuth = Depends(current_active_user),
44
+ user: UserOAuth = Depends(current_user_act_ver_prof),
44
45
  db: AsyncSession = Depends(get_async_db),
45
46
  ) -> list[TaskReadV2]:
46
47
  """
47
48
  Get list of available tasks
48
49
  """
50
+
51
+ user_resource_id = await _get_user_resource_id(user_id=user.id, db=db)
52
+
49
53
  stm = (
50
54
  select(TaskV2)
51
55
  .join(TaskGroupV2)
52
56
  .where(TaskGroupV2.id == TaskV2.taskgroupv2_id)
57
+ .where(TaskGroupV2.resource_id == user_resource_id)
53
58
  .where(
54
59
  or_(
55
60
  TaskGroupV2.user_id == user.id,
@@ -68,6 +73,7 @@ async def get_list_task(
68
73
  if author is not None:
69
74
  stm = stm.where(TaskV2.authors.icontains(author))
70
75
 
76
+ stm = stm.order_by(TaskV2.id)
71
77
  res = await db.execute(stm)
72
78
  task_list = list(res.scalars().all())
73
79
  await db.close()
@@ -82,7 +88,7 @@ async def get_list_task(
82
88
  @router.get("/{task_id}/", response_model=TaskReadV2)
83
89
  async def get_task(
84
90
  task_id: int,
85
- user: UserOAuth = Depends(current_active_user),
91
+ user: UserOAuth = Depends(current_user_act_ver_prof),
86
92
  db: AsyncSession = Depends(get_async_db),
87
93
  ) -> TaskReadV2:
88
94
  """
@@ -96,7 +102,7 @@ async def get_task(
96
102
  async def patch_task(
97
103
  task_id: int,
98
104
  task_update: TaskUpdateV2,
99
- user: UserOAuth = Depends(current_active_verified_user),
105
+ user: UserOAuth = Depends(current_user_act_ver_prof),
100
106
  db: AsyncSession = Depends(get_async_db),
101
107
  ) -> TaskReadV2 | None:
102
108
  """
@@ -137,13 +143,20 @@ async def create_task(
137
143
  task: TaskCreateV2,
138
144
  user_group_id: int | None = None,
139
145
  private: bool = False,
140
- user: UserOAuth = Depends(current_active_verified_user),
146
+ user: UserOAuth = Depends(current_user_act_ver_prof),
141
147
  db: AsyncSession = Depends(get_async_db),
142
148
  ) -> TaskReadV2 | None:
143
149
  """
144
150
  Create a new task
145
151
  """
146
152
 
153
+ # Get validated resource and profile
154
+ resource, profile = await validate_user_profile(
155
+ user=user,
156
+ db=db,
157
+ )
158
+ resource_id = resource.id
159
+
147
160
  # Validate query parameters related to user-group ownership
148
161
  user_group_id = await _get_valid_user_group_id(
149
162
  user_group_id=user_group_id,
@@ -179,7 +192,11 @@ async def create_task(
179
192
  db_task = TaskV2(**task.model_dump(exclude_unset=True))
180
193
  pkg_name = db_task.name
181
194
  await _verify_non_duplication_user_constraint(
182
- db=db, pkg_name=pkg_name, user_id=user.id, version=db_task.version
195
+ db=db,
196
+ pkg_name=pkg_name,
197
+ user_id=user.id,
198
+ version=db_task.version,
199
+ user_resource_id=resource_id,
183
200
  )
184
201
  await _verify_non_duplication_group_constraint(
185
202
  db=db,
@@ -190,6 +207,7 @@ async def create_task(
190
207
  db_task_group = TaskGroupV2(
191
208
  user_id=user.id,
192
209
  user_group_id=user_group_id,
210
+ resource_id=resource_id,
193
211
  active=True,
194
212
  task_list=[db_task],
195
213
  origin=TaskGroupV2OriginEnum.OTHER,
@@ -207,7 +225,7 @@ async def create_task(
207
225
  @router.delete("/{task_id}/", status_code=204)
208
226
  async def delete_task(
209
227
  task_id: int,
210
- user: UserOAuth = Depends(current_active_user),
228
+ user: UserOAuth = Depends(current_user_act_ver_prof),
211
229
  db: AsyncSession = Depends(get_async_db),
212
230
  ) -> Response:
213
231
  """
@@ -14,10 +14,8 @@ from pydantic import BaseModel
14
14
  from pydantic import model_validator
15
15
  from pydantic import ValidationError
16
16
 
17
- from .....config import get_settings
18
17
  from .....logger import reset_logger_handlers
19
18
  from .....logger import set_logger
20
- from .....syringe import Inject
21
19
  from ....db import AsyncSession
22
20
  from ....db import get_async_db
23
21
  from ....models.v2 import TaskGroupV2
@@ -26,7 +24,7 @@ from ....schemas.v2 import TaskCollectPipV2
26
24
  from ....schemas.v2 import TaskGroupActivityStatusV2
27
25
  from ....schemas.v2 import TaskGroupActivityV2Read
28
26
  from ....schemas.v2 import TaskGroupCreateV2Strict
29
- from ...aux.validate_user_settings import validate_user_settings
27
+ from ...aux.validate_user_profile import validate_user_profile
30
28
  from ._aux_functions_task_lifecycle import get_package_version_from_pypi
31
29
  from ._aux_functions_tasks import _get_valid_user_group_id
32
30
  from ._aux_functions_tasks import _verify_non_duplication_group_constraint
@@ -34,12 +32,12 @@ from ._aux_functions_tasks import _verify_non_duplication_group_path
34
32
  from ._aux_functions_tasks import _verify_non_duplication_user_constraint
35
33
  from fractal_server.app.models import UserOAuth
36
34
  from fractal_server.app.models.v2 import TaskGroupActivityV2
37
- from fractal_server.app.routes.auth import current_active_verified_user
35
+ from fractal_server.app.routes.auth import current_user_act_ver_prof
36
+ from fractal_server.app.schemas.v2 import ResourceType
38
37
  from fractal_server.app.schemas.v2 import (
39
38
  TaskGroupActivityActionV2,
40
39
  )
41
40
  from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
42
- from fractal_server.ssh._fabric import SSHConfig
43
41
  from fractal_server.tasks.v2.local.collect import (
44
42
  collect_local,
45
43
  )
@@ -47,7 +45,7 @@ from fractal_server.tasks.v2.ssh import collect_ssh
47
45
  from fractal_server.tasks.v2.utils_package_names import _parse_wheel_filename
48
46
  from fractal_server.tasks.v2.utils_package_names import normalize_package_name
49
47
  from fractal_server.tasks.v2.utils_python_interpreter import (
50
- get_python_interpreter_v2,
48
+ get_python_interpreter,
51
49
  )
52
50
 
53
51
 
@@ -162,14 +160,19 @@ async def collect_tasks_pip(
162
160
  request_data: CollectionRequestData = Depends(parse_request_data),
163
161
  private: bool = False,
164
162
  user_group_id: int | None = None,
165
- user: UserOAuth = Depends(current_active_verified_user),
163
+ user: UserOAuth = Depends(current_user_act_ver_prof),
166
164
  db: AsyncSession = Depends(get_async_db),
167
165
  ) -> TaskGroupActivityV2Read:
168
166
  """
169
167
  Task-collection endpoint
170
168
  """
171
- # Get settings
172
- settings = Inject(get_settings)
169
+
170
+ # Get validated resource and profile
171
+ resource, profile = await validate_user_profile(
172
+ user=user,
173
+ db=db,
174
+ )
175
+ resource_id = resource.id
173
176
 
174
177
  # Get some validated request data
175
178
  task_collect = request_data.task_collect
@@ -177,26 +180,28 @@ async def collect_tasks_pip(
177
180
  # Initialize task-group attributes
178
181
  task_group_attrs = dict(
179
182
  user_id=user.id,
183
+ resource_id=resource_id,
180
184
  origin=request_data.origin,
181
185
  )
182
186
 
183
187
  # Set/check python version
184
188
  if task_collect.python_version is None:
185
- task_group_attrs[
186
- "python_version"
187
- ] = settings.FRACTAL_TASKS_PYTHON_DEFAULT_VERSION
189
+ task_group_attrs["python_version"] = resource.tasks_python_config[
190
+ "default_version"
191
+ ]
188
192
  else:
189
193
  task_group_attrs["python_version"] = task_collect.python_version
190
194
  try:
191
- get_python_interpreter_v2(
192
- python_version=task_group_attrs["python_version"]
195
+ get_python_interpreter(
196
+ python_version=task_group_attrs["python_version"],
197
+ resource=resource,
193
198
  )
194
199
  except ValueError:
195
200
  raise HTTPException(
196
201
  status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
197
202
  detail=(
198
- f"Python version {task_group_attrs['python_version']} is "
199
- "not available for Fractal task collection."
203
+ f"Python version {task_group_attrs['python_version']} "
204
+ "is not available on this Fractal instance."
200
205
  ),
201
206
  )
202
207
 
@@ -259,16 +264,11 @@ async def collect_tasks_pip(
259
264
  # Set user_group_id
260
265
  task_group_attrs["user_group_id"] = user_group_id
261
266
 
262
- # Validate user settings (backend-specific)
263
- user_settings = await validate_user_settings(
264
- user=user, backend=settings.FRACTAL_RUNNER_BACKEND, db=db
265
- )
266
-
267
267
  # Set path and venv_path
268
- if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
269
- base_tasks_path = user_settings.ssh_tasks_dir
268
+ if resource.type == ResourceType.SLURM_SSH:
269
+ base_tasks_path = profile.tasks_remote_dir
270
270
  else:
271
- base_tasks_path = settings.FRACTAL_TASKS_DIR.as_posix()
271
+ base_tasks_path = resource.tasks_local_dir
272
272
  task_group_path = (
273
273
  Path(base_tasks_path)
274
274
  / str(user.id)
@@ -294,6 +294,7 @@ async def collect_tasks_pip(
294
294
  user_id=user.id,
295
295
  pkg_name=task_group_attrs["pkg_name"],
296
296
  version=task_group_attrs["version"],
297
+ user_resource_id=resource_id,
297
298
  db=db,
298
299
  )
299
300
  await _verify_non_duplication_group_constraint(
@@ -304,12 +305,13 @@ async def collect_tasks_pip(
304
305
  )
305
306
  await _verify_non_duplication_group_path(
306
307
  path=task_group_attrs["path"],
308
+ resource_id=resource_id,
307
309
  db=db,
308
310
  )
309
311
 
310
312
  # On-disk checks
311
313
 
312
- if settings.FRACTAL_RUNNER_BACKEND != "slurm_ssh":
314
+ if resource.type != ResourceType.SLURM_SSH:
313
315
  # Verify that folder does not exist (for local collection)
314
316
  if Path(task_group_path).exists():
315
317
  raise HTTPException(
@@ -340,33 +342,20 @@ async def collect_tasks_pip(
340
342
 
341
343
  # END of SSH/non-SSH common part
342
344
 
343
- if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
344
- # SSH task collection
345
- # Use appropriate FractalSSH object
346
- ssh_config = SSHConfig(
347
- user=user_settings.ssh_username,
348
- host=user_settings.ssh_host,
349
- key_path=user_settings.ssh_private_key_path,
350
- )
351
-
352
- background_tasks.add_task(
353
- collect_ssh,
354
- task_group_id=task_group.id,
355
- task_group_activity_id=task_group_activity.id,
356
- ssh_config=ssh_config,
357
- tasks_base_dir=user_settings.ssh_tasks_dir,
358
- wheel_file=wheel_file,
359
- )
360
-
345
+ if resource.type == ResourceType.SLURM_SSH:
346
+ collect_function = collect_ssh
361
347
  else:
362
- # Local task collection
348
+ collect_function = collect_local
349
+
350
+ background_tasks.add_task(
351
+ collect_function,
352
+ task_group_id=task_group.id,
353
+ task_group_activity_id=task_group_activity.id,
354
+ wheel_file=wheel_file,
355
+ resource=resource,
356
+ profile=profile,
357
+ )
363
358
 
364
- background_tasks.add_task(
365
- collect_local,
366
- task_group_id=task_group.id,
367
- task_group_activity_id=task_group_activity.id,
368
- wheel_file=wheel_file,
369
- )
370
359
  logger.debug(
371
360
  "Task-collection endpoint: start background collection "
372
361
  "and return task_group_activity"
@@ -9,22 +9,22 @@ from fastapi import HTTPException
9
9
  from fastapi import status
10
10
  from sqlalchemy.ext.asyncio import AsyncSession
11
11
 
12
+ from ...aux.validate_user_profile import validate_user_profile
12
13
  from ._aux_functions_tasks import _get_valid_user_group_id
13
14
  from ._aux_functions_tasks import _verify_non_duplication_group_constraint
14
15
  from ._aux_functions_tasks import _verify_non_duplication_user_constraint
15
16
  from fractal_server.app.db import get_async_db
16
17
  from fractal_server.app.models import UserOAuth
17
18
  from fractal_server.app.models.v2 import TaskGroupV2
18
- from fractal_server.app.routes.auth import current_active_verified_user
19
+ from fractal_server.app.routes.auth import current_user_act_ver_prof
20
+ from fractal_server.app.schemas.v2 import ResourceType
19
21
  from fractal_server.app.schemas.v2 import TaskCollectCustomV2
20
22
  from fractal_server.app.schemas.v2 import TaskCreateV2
21
23
  from fractal_server.app.schemas.v2 import TaskGroupCreateV2
22
24
  from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
23
25
  from fractal_server.app.schemas.v2 import TaskReadV2
24
- from fractal_server.config import get_settings
25
26
  from fractal_server.logger import set_logger
26
27
  from fractal_server.string_tools import validate_cmd
27
- from fractal_server.syringe import Inject
28
28
  from fractal_server.tasks.v2.utils_background import (
29
29
  prepare_tasks_metadata,
30
30
  )
@@ -44,10 +44,12 @@ async def collect_task_custom(
44
44
  task_collect: TaskCollectCustomV2,
45
45
  private: bool = False,
46
46
  user_group_id: int | None = None,
47
- user: UserOAuth = Depends(current_active_verified_user),
47
+ user: UserOAuth = Depends(current_user_act_ver_prof),
48
48
  db: AsyncSession = Depends(get_async_db),
49
49
  ) -> list[TaskReadV2]:
50
- settings = Inject(get_settings)
50
+ # Get validated resource and profile
51
+ resource, profile = await validate_user_profile(user=user, db=db)
52
+ resource_id = resource.id
51
53
 
52
54
  # Validate query parameters related to user-group ownership
53
55
  user_group_id = await _get_valid_user_group_id(
@@ -57,7 +59,7 @@ async def collect_task_custom(
57
59
  db=db,
58
60
  )
59
61
 
60
- if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
62
+ if resource.type == ResourceType.SLURM_SSH:
61
63
  if task_collect.package_root is None:
62
64
  raise HTTPException(
63
65
  status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
@@ -150,6 +152,7 @@ async def collect_task_custom(
150
152
  user_id=user.id,
151
153
  user_group_id=user_group_id,
152
154
  version=task_collect.version,
155
+ resource_id=resource_id,
153
156
  )
154
157
  TaskGroupCreateV2(**task_group_attrs)
155
158
 
@@ -158,6 +161,7 @@ async def collect_task_custom(
158
161
  user_id=user.id,
159
162
  pkg_name=task_group_attrs["pkg_name"],
160
163
  version=task_group_attrs["version"],
164
+ user_resource_id=resource_id,
161
165
  db=db,
162
166
  )
163
167
  await _verify_non_duplication_group_constraint(