fractal-server 2.17.0a6__py3-none-any.whl → 2.17.0a8__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 (28) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/security.py +7 -1
  3. fractal_server/app/models/user_settings.py +4 -0
  4. fractal_server/app/models/v2/profile.py +1 -1
  5. fractal_server/app/models/v2/project.py +3 -1
  6. fractal_server/app/models/v2/resource.py +2 -2
  7. fractal_server/app/models/v2/task_group.py +1 -1
  8. fractal_server/app/routes/admin/v2/resource.py +9 -52
  9. fractal_server/app/routes/api/v2/_aux_functions.py +6 -16
  10. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +36 -8
  11. fractal_server/app/routes/api/v2/project.py +8 -4
  12. fractal_server/app/routes/api/v2/submit.py +3 -0
  13. fractal_server/app/routes/api/v2/task.py +18 -5
  14. fractal_server/app/routes/api/v2/task_collection.py +3 -5
  15. fractal_server/app/routes/api/v2/task_collection_custom.py +2 -5
  16. fractal_server/app/routes/api/v2/task_collection_pixi.py +2 -5
  17. fractal_server/app/routes/auth/oauth.py +16 -6
  18. fractal_server/config/_main.py +1 -1
  19. fractal_server/migrations/naming_convention.py +1 -1
  20. fractal_server/migrations/versions/{a80ac5a352bf_resource_profile.py → 83bc2ad3ffcc_2_17_0.py} +31 -31
  21. {fractal_server-2.17.0a6.dist-info → fractal_server-2.17.0a8.dist-info}/METADATA +1 -1
  22. {fractal_server-2.17.0a6.dist-info → fractal_server-2.17.0a8.dist-info}/RECORD +25 -28
  23. fractal_server/data_migrations/2_14_10.py +0 -48
  24. fractal_server/migrations/versions/90f6508c6379_drop_useroauth_username.py +0 -36
  25. fractal_server/migrations/versions/f65ee53991e3_user_settings_related.py +0 -67
  26. {fractal_server-2.17.0a6.dist-info → fractal_server-2.17.0a8.dist-info}/WHEEL +0 -0
  27. {fractal_server-2.17.0a6.dist-info → fractal_server-2.17.0a8.dist-info}/entry_points.txt +0 -0
  28. {fractal_server-2.17.0a6.dist-info → fractal_server-2.17.0a8.dist-info}/licenses/LICENSE +0 -0
@@ -1 +1 @@
1
- __VERSION__ = "2.17.0a6"
1
+ __VERSION__ = "2.17.0a8"
@@ -92,7 +92,7 @@ class UserOAuth(SQLModel, table=True):
92
92
  profile_id: int | None = Field(
93
93
  foreign_key="profile.id",
94
94
  default=None,
95
- ondelete="SET NULL",
95
+ ondelete="RESTRICT",
96
96
  )
97
97
 
98
98
  # TODO-2.17.1: update to `project_dir: str`
@@ -107,6 +107,12 @@ class UserOAuth(SQLModel, table=True):
107
107
  sa_column=Column(ARRAY(String), server_default="{}"),
108
108
  )
109
109
 
110
+ # TODO-2.17.1: remove
111
+ user_settings_id: int | None = Field(
112
+ foreign_key="user_settings.id",
113
+ default=None,
114
+ )
115
+
110
116
 
111
117
  class UserGroup(SQLModel, table=True):
112
118
  """
@@ -29,5 +29,9 @@ class UserSettings(SQLModel, table=True):
29
29
  ssh_host: str | None = None
30
30
  ssh_username: str | None = None
31
31
  ssh_private_key_path: str | None = None
32
+
32
33
  slurm_user: str | None = None
33
34
  project_dir: str | None = None
35
+
36
+ ssh_tasks_dir: str | None = None
37
+ ssh_jobs_dir: str | None = None
@@ -4,7 +4,7 @@ from sqlmodel import SQLModel
4
4
 
5
5
  class Profile(SQLModel, table=True):
6
6
  id: int | None = Field(default=None, primary_key=True)
7
- resource_id: int = Field(foreign_key="resource.id", ondelete="CASCADE")
7
+ resource_id: int = Field(foreign_key="resource.id", ondelete="RESTRICT")
8
8
  resource_type: str
9
9
 
10
10
  name: str = Field(unique=True)
@@ -14,8 +14,10 @@ from fractal_server.utils import get_timestamp
14
14
  class ProjectV2(SQLModel, table=True):
15
15
  id: int | None = Field(default=None, primary_key=True)
16
16
  name: str
17
+
18
+ # TODO-2.17.1: make `resource_id` not nullable
17
19
  resource_id: int | None = Field(
18
- foreign_key="resource.id", default=None, ondelete="SET NULL"
20
+ foreign_key="resource.id", default=None, ondelete="RESTRICT"
19
21
  )
20
22
  timestamp_created: datetime = Field(
21
23
  default_factory=get_timestamp,
@@ -120,11 +120,11 @@ class Resource(SQLModel, table=True):
120
120
  # `type` column must be one of "local", "slurm_sudo" or "slurm_ssh"
121
121
  CheckConstraint(
122
122
  "type IN ('local', 'slurm_sudo', 'slurm_ssh')",
123
- name="ck_resource_correct_type",
123
+ name="correct_type",
124
124
  ),
125
125
  # If `type` is not "local", `jobs_slurm_python_worker` must be set
126
126
  CheckConstraint(
127
127
  "(type = 'local') OR (jobs_slurm_python_worker IS NOT NULL)",
128
- name="ck_resource_jobs_slurm_python_worker_set",
128
+ name="jobs_slurm_python_worker_set",
129
129
  ),
130
130
  )
@@ -44,7 +44,7 @@ class TaskGroupV2(SQLModel, table=True):
44
44
  )
45
45
  # TODO-2.17.1: make `resource_id` not nullable
46
46
  resource_id: int | None = Field(
47
- foreign_key="resource.id", default=None, ondelete="SET NULL"
47
+ foreign_key="resource.id", default=None, ondelete="RESTRICT"
48
48
  )
49
49
 
50
50
  origin: str
@@ -3,7 +3,7 @@ from fastapi import Depends
3
3
  from fastapi import HTTPException
4
4
  from fastapi import Response
5
5
  from fastapi import status
6
- from sqlmodel import func
6
+ from sqlalchemy.exc import IntegrityError
7
7
  from sqlmodel import select
8
8
 
9
9
  from ._aux_functions import _check_resource_name
@@ -13,9 +13,7 @@ from fractal_server.app.db import AsyncSession
13
13
  from fractal_server.app.db import get_async_db
14
14
  from fractal_server.app.models import UserOAuth
15
15
  from fractal_server.app.models.v2 import Profile
16
- from fractal_server.app.models.v2 import ProjectV2
17
16
  from fractal_server.app.models.v2 import Resource
18
- from fractal_server.app.models.v2 import TaskGroupV2
19
17
  from fractal_server.app.routes.auth import current_superuser_act
20
18
  from fractal_server.app.schemas.v2 import ProfileCreate
21
19
  from fractal_server.app.schemas.v2 import ProfileRead
@@ -151,61 +149,20 @@ async def delete_resource(
151
149
  Delete single `Resource`.
152
150
  """
153
151
  resource = await _get_resource_or_404(resource_id=resource_id, db=db)
154
-
155
- # Fail if at least one Profile is associated with the Resource.
156
- res = await db.execute(
157
- select(func.count(Profile.id)).where(
158
- Profile.resource_id == resource_id
159
- )
160
- )
161
- associated_profile_count = res.scalar()
162
- if associated_profile_count > 0:
163
- raise HTTPException(
164
- status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
165
- detail=(
166
- f"Cannot delete Resource {resource_id} because it's associated"
167
- f" with {associated_profile_count} Profiles."
168
- ),
169
- )
170
-
171
- # Fail if at least one Project is associated with the Resource.
172
- res = await db.execute(
173
- select(func.count(ProjectV2.id)).where(
174
- ProjectV2.resource_id == resource_id
175
- )
176
- )
177
- associated_project_count = res.scalar()
178
- if associated_project_count > 0:
152
+ try:
153
+ await db.delete(resource)
154
+ await db.commit()
155
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
156
+ except IntegrityError as e:
157
+ await db.rollback()
179
158
  raise HTTPException(
180
159
  status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
181
160
  detail=(
182
- f"Cannot delete Resource {resource_id} because it's associated"
183
- f" with {associated_project_count} Projects."
161
+ "IntegrityError for resource deletion. "
162
+ f"Original error:\n{str(e)}"
184
163
  ),
185
164
  )
186
165
 
187
- # Fail if at least one TaskGroupV2 is associated with the Resource.
188
- res = await db.execute(
189
- select(func.count(TaskGroupV2.id)).where(
190
- TaskGroupV2.resource_id == resource_id
191
- )
192
- )
193
- associated_taskgroup_count = res.scalar()
194
- if associated_taskgroup_count > 0:
195
- raise HTTPException(
196
- status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
197
- detail=(
198
- f"Cannot delete Resource {resource_id} because it's associated"
199
- f" with {associated_taskgroup_count} TaskGroupV2."
200
- ),
201
- )
202
-
203
- # Delete
204
- await db.delete(resource)
205
- await db.commit()
206
-
207
- return Response(status_code=status.HTTP_204_NO_CONTENT)
208
-
209
166
 
210
167
  @router.get(
211
168
  "/{resource_id}/profile/",
@@ -543,24 +543,14 @@ async def _get_submitted_job_or_none(
543
543
  )
544
544
 
545
545
 
546
- async def _get_resource_and_profile_ids(
547
- *,
548
- user_id: int,
549
- db: AsyncSession,
550
- ) -> tuple[int, int] | tuple[None, None]:
551
- """
552
- Get `(resource_id, profile_id)` pair for a given user, or `(None,None)`.
553
- """
554
- stm = (
555
- select(Resource.id, Profile.id)
546
+ async def _get_user_resource_id(user_id: int, db: AsyncSession) -> int | None:
547
+ res = await db.execute(
548
+ select(Resource.id)
549
+ .join(Profile)
556
550
  .join(UserOAuth)
557
551
  .where(Resource.id == Profile.resource_id)
558
552
  .where(Profile.id == UserOAuth.profile_id)
559
553
  .where(UserOAuth.id == user_id)
560
554
  )
561
- res = await db.execute(stm)
562
- data = res.one_or_none()
563
- if data is None:
564
- return (None, None)
565
- else:
566
- return tuple(data)
555
+ resource_id = res.scalar_one_or_none()
556
+ return resource_id
@@ -12,10 +12,14 @@ from fractal_server.app.db import AsyncSession
12
12
  from fractal_server.app.models import LinkUserGroup
13
13
  from fractal_server.app.models import UserGroup
14
14
  from fractal_server.app.models import UserOAuth
15
+ from fractal_server.app.models.v2 import Profile
15
16
  from fractal_server.app.models.v2 import TaskGroupActivityV2
16
17
  from fractal_server.app.models.v2 import TaskGroupV2
17
18
  from fractal_server.app.models.v2 import TaskV2
18
19
  from fractal_server.app.models.v2 import WorkflowTaskV2
20
+ from fractal_server.app.routes.api.v2._aux_functions import (
21
+ _get_user_resource_id,
22
+ )
19
23
  from fractal_server.app.routes.auth._aux_auth import _get_default_usergroup_id
20
24
  from fractal_server.app.routes.auth._aux_auth import (
21
25
  _verify_user_belongs_to_group,
@@ -80,11 +84,16 @@ async def _get_task_group_read_access(
80
84
  else:
81
85
  stm = (
82
86
  select(LinkUserGroup)
87
+ .join(UserOAuth)
88
+ .join(Profile)
83
89
  .where(LinkUserGroup.group_id == task_group.user_group_id)
84
90
  .where(LinkUserGroup.user_id == user_id)
91
+ .where(UserOAuth.id == user_id)
92
+ .where(Profile.id == UserOAuth.profile_id)
93
+ .where(TaskGroupV2.resource_id == Profile.resource_id)
85
94
  )
86
95
  res = await db.execute(stm)
87
- link = res.scalar_one_or_none()
96
+ link = res.unique().scalars().one_or_none()
88
97
  if link is None:
89
98
  raise forbidden_exception
90
99
  else:
@@ -153,9 +162,17 @@ async def _get_task_full_access(
153
162
  db: An asynchronous db session.
154
163
  """
155
164
  task = await _get_task_or_404(task_id=task_id, db=db)
156
- await _get_task_group_full_access(
165
+ task_group = await _get_task_group_full_access(
157
166
  task_group_id=task.taskgroupv2_id, user_id=user_id, db=db
158
167
  )
168
+
169
+ resource_id = await _get_user_resource_id(user_id=user_id, db=db)
170
+ if resource_id is None or resource_id != task_group.resource_id:
171
+ raise HTTPException(
172
+ status_code=status.HTTP_403_FORBIDDEN,
173
+ detail=f"User {user_id} has no access to TaskGroup's Resource.",
174
+ )
175
+
159
176
  return task
160
177
 
161
178
 
@@ -179,12 +196,20 @@ async def _get_task_read_access(
179
196
  task_group = await _get_task_group_read_access(
180
197
  task_group_id=task.taskgroupv2_id, user_id=user_id, db=db
181
198
  )
182
- if require_active:
183
- if not task_group.active:
184
- raise HTTPException(
185
- status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
186
- detail=f"Error: task {task_id} ({task.name}) is not active.",
187
- )
199
+
200
+ resource_id = await _get_user_resource_id(user_id=user_id, db=db)
201
+ if resource_id is None or resource_id != task_group.resource_id:
202
+ raise HTTPException(
203
+ status_code=status.HTTP_403_FORBIDDEN,
204
+ detail=f"User {user_id} has no access to TaskGroup's Resource.",
205
+ )
206
+
207
+ if require_active and not task_group.active:
208
+ raise HTTPException(
209
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
210
+ detail=f"Error: task {task_id} ({task.name}) is not active.",
211
+ )
212
+
188
213
  return task
189
214
 
190
215
 
@@ -255,16 +280,19 @@ async def _get_collection_task_group_activity_status_message(
255
280
 
256
281
 
257
282
  async def _verify_non_duplication_user_constraint(
283
+ *,
258
284
  db: AsyncSession,
259
285
  user_id: int,
260
286
  pkg_name: str,
261
287
  version: str | None,
288
+ user_resource_id: int,
262
289
  ):
263
290
  stm = (
264
291
  select(TaskGroupV2)
265
292
  .where(TaskGroupV2.user_id == user_id)
266
293
  .where(TaskGroupV2.pkg_name == pkg_name)
267
294
  .where(TaskGroupV2.version == version)
295
+ .where(TaskGroupV2.resource_id == user_resource_id)
268
296
  )
269
297
  res = await db.execute(stm)
270
298
  duplicate = res.scalars().all()
@@ -15,9 +15,9 @@ from ....models.v2 import ProjectV2
15
15
  from ....schemas.v2 import ProjectCreateV2
16
16
  from ....schemas.v2 import ProjectReadV2
17
17
  from ....schemas.v2 import ProjectUpdateV2
18
+ from ...aux.validate_user_profile import validate_user_profile
18
19
  from ._aux_functions import _check_project_exists
19
20
  from ._aux_functions import _get_project_check_owner
20
- from ._aux_functions import _get_resource_and_profile_ids
21
21
  from ._aux_functions import _get_submitted_jobs_statement
22
22
  from fractal_server.app.models import UserOAuth
23
23
  from fractal_server.app.routes.auth import current_user_act_ver_prof
@@ -54,13 +54,17 @@ async def create_project(
54
54
  Create new project
55
55
  """
56
56
 
57
+ # Get validated resource and profile
58
+ resource, profile = await validate_user_profile(
59
+ user=user,
60
+ db=db,
61
+ )
62
+ resource_id = resource.id
63
+
57
64
  # Check that there is no project with the same user and name
58
65
  await _check_project_exists(
59
66
  project_name=project.name, user_id=user.id, db=db
60
67
  )
61
- resource_id, _ = await _get_resource_and_profile_ids(
62
- user_id=user.id, db=db
63
- )
64
68
 
65
69
  db_project = ProjectV2(**project.model_dump(), resource_id=resource_id)
66
70
  db_project.user_list.append(user)
@@ -59,6 +59,9 @@ async def apply_workflow(
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)
@@ -9,7 +9,8 @@ from sqlmodel import func
9
9
  from sqlmodel import or_
10
10
  from sqlmodel import select
11
11
 
12
- from ._aux_functions import _get_resource_and_profile_ids
12
+ from ...aux.validate_user_profile import validate_user_profile
13
+ from ._aux_functions import _get_user_resource_id
13
14
  from ._aux_functions_tasks import _get_task_full_access
14
15
  from ._aux_functions_tasks import _get_task_read_access
15
16
  from ._aux_functions_tasks import _get_valid_user_group_id
@@ -46,10 +47,14 @@ async def get_list_task(
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,
@@ -144,6 +149,13 @@ async def create_task(
144
149
  Create a new task
145
150
  """
146
151
 
152
+ # Get validated resource and profile
153
+ resource, profile = await validate_user_profile(
154
+ user=user,
155
+ db=db,
156
+ )
157
+ resource_id = resource.id
158
+
147
159
  # Validate query parameters related to user-group ownership
148
160
  user_group_id = await _get_valid_user_group_id(
149
161
  user_group_id=user_group_id,
@@ -179,7 +191,11 @@ async def create_task(
179
191
  db_task = TaskV2(**task.model_dump(exclude_unset=True))
180
192
  pkg_name = db_task.name
181
193
  await _verify_non_duplication_user_constraint(
182
- db=db, pkg_name=pkg_name, user_id=user.id, version=db_task.version
194
+ db=db,
195
+ pkg_name=pkg_name,
196
+ user_id=user.id,
197
+ version=db_task.version,
198
+ user_resource_id=resource_id,
183
199
  )
184
200
  await _verify_non_duplication_group_constraint(
185
201
  db=db,
@@ -187,9 +203,6 @@ async def create_task(
187
203
  user_group_id=user_group_id,
188
204
  version=db_task.version,
189
205
  )
190
- resource_id, _ = await _get_resource_and_profile_ids(
191
- user_id=user.id, db=db
192
- )
193
206
  db_task_group = TaskGroupV2(
194
207
  user_id=user.id,
195
208
  user_group_id=user_group_id,
@@ -25,7 +25,6 @@ from ....schemas.v2 import TaskGroupActivityStatusV2
25
25
  from ....schemas.v2 import TaskGroupActivityV2Read
26
26
  from ....schemas.v2 import TaskGroupCreateV2Strict
27
27
  from ...aux.validate_user_profile import validate_user_profile
28
- from ._aux_functions import _get_resource_and_profile_ids
29
28
  from ._aux_functions_task_lifecycle import get_package_version_from_pypi
30
29
  from ._aux_functions_tasks import _get_valid_user_group_id
31
30
  from ._aux_functions_tasks import _verify_non_duplication_group_constraint
@@ -173,13 +172,11 @@ async def collect_tasks_pip(
173
172
  user=user,
174
173
  db=db,
175
174
  )
175
+ resource_id = resource.id
176
+
176
177
  # Get some validated request data
177
178
  task_collect = request_data.task_collect
178
179
 
179
- resource_id, _ = await _get_resource_and_profile_ids(
180
- user_id=user.id, db=db
181
- )
182
-
183
180
  # Initialize task-group attributes
184
181
  task_group_attrs = dict(
185
182
  user_id=user.id,
@@ -297,6 +294,7 @@ async def collect_tasks_pip(
297
294
  user_id=user.id,
298
295
  pkg_name=task_group_attrs["pkg_name"],
299
296
  version=task_group_attrs["version"],
297
+ user_resource_id=resource_id,
300
298
  db=db,
301
299
  )
302
300
  await _verify_non_duplication_group_constraint(
@@ -10,7 +10,6 @@ from fastapi import status
10
10
  from sqlalchemy.ext.asyncio import AsyncSession
11
11
 
12
12
  from ...aux.validate_user_profile import validate_user_profile
13
- from ._aux_functions import _get_resource_and_profile_ids
14
13
  from ._aux_functions_tasks import _get_valid_user_group_id
15
14
  from ._aux_functions_tasks import _verify_non_duplication_group_constraint
16
15
  from ._aux_functions_tasks import _verify_non_duplication_user_constraint
@@ -50,6 +49,7 @@ async def collect_task_custom(
50
49
  ) -> list[TaskReadV2]:
51
50
  # Get validated resource and profile
52
51
  resource, profile = await validate_user_profile(user=user, db=db)
52
+ resource_id = resource.id
53
53
 
54
54
  # Validate query parameters related to user-group ownership
55
55
  user_group_id = await _get_valid_user_group_id(
@@ -145,10 +145,6 @@ async def collect_task_custom(
145
145
  package_version=task_collect.version,
146
146
  )
147
147
 
148
- resource_id, _ = await _get_resource_and_profile_ids(
149
- user_id=user.id, db=db
150
- )
151
-
152
148
  # Prepare task-group attributes
153
149
  task_group_attrs = dict(
154
150
  origin=TaskGroupV2OriginEnum.OTHER,
@@ -165,6 +161,7 @@ async def collect_task_custom(
165
161
  user_id=user.id,
166
162
  pkg_name=task_group_attrs["pkg_name"],
167
163
  version=task_group_attrs["version"],
164
+ user_resource_id=resource_id,
168
165
  db=db,
169
166
  )
170
167
  await _verify_non_duplication_group_constraint(
@@ -10,7 +10,6 @@ from fastapi import Response
10
10
  from fastapi import status
11
11
  from fastapi import UploadFile
12
12
 
13
- from ._aux_functions import _get_resource_and_profile_ids
14
13
  from fractal_server.app.db import AsyncSession
15
14
  from fractal_server.app.db import get_async_db
16
15
  from fractal_server.app.models import UserOAuth
@@ -90,6 +89,7 @@ async def collect_task_pixi(
90
89
  ) -> TaskGroupActivityV2Read:
91
90
  # Get validated resource and profile
92
91
  resource, profile = await validate_user_profile(user=user, db=db)
92
+ resource_id = resource.id
93
93
 
94
94
  # Check if Pixi is available
95
95
  if not resource.tasks_pixi_config:
@@ -133,10 +133,6 @@ async def collect_task_pixi(
133
133
  Path(base_tasks_path) / str(user.id) / pkg_name / version
134
134
  ).as_posix()
135
135
 
136
- resource_id, _ = await _get_resource_and_profile_ids(
137
- user_id=user.id, db=db
138
- )
139
-
140
136
  task_group_attrs = dict(
141
137
  user_id=user.id,
142
138
  user_group_id=user_group_id,
@@ -152,6 +148,7 @@ async def collect_task_pixi(
152
148
  user_id=user.id,
153
149
  pkg_name=task_group_attrs["pkg_name"],
154
150
  version=task_group_attrs["version"],
151
+ user_resource_id=resource_id,
155
152
  db=db,
156
153
  )
157
154
  await _verify_non_duplication_group_constraint(
@@ -2,6 +2,7 @@ from fastapi import APIRouter
2
2
  from httpx_oauth.clients.github import GitHubOAuth2
3
3
  from httpx_oauth.clients.google import GoogleOAuth2
4
4
  from httpx_oauth.clients.openid import OpenID
5
+ from httpx_oauth.clients.openid import OpenIDConfigurationError
5
6
 
6
7
  from . import cookie_backend
7
8
  from . import fastapi_users
@@ -26,11 +27,21 @@ def _create_client_google(cfg: OAuthSettings) -> GoogleOAuth2:
26
27
 
27
28
 
28
29
  def _create_client_oidc(cfg: OAuthSettings) -> OpenID:
29
- return OpenID(
30
- client_id=cfg.OAUTH_CLIENT_ID.get_secret_value(),
31
- client_secret=cfg.OAUTH_CLIENT_SECRET.get_secret_value(),
32
- openid_configuration_endpoint=cfg.OAUTH_OIDC_CONFIG_ENDPOINT.get_secret_value(), # noqa
33
- )
30
+ try:
31
+ open_id = OpenID(
32
+ client_id=cfg.OAUTH_CLIENT_ID.get_secret_value(),
33
+ client_secret=cfg.OAUTH_CLIENT_SECRET.get_secret_value(),
34
+ openid_configuration_endpoint=cfg.OAUTH_OIDC_CONFIG_ENDPOINT.get_secret_value(), # noqa
35
+ )
36
+ except OpenIDConfigurationError as e:
37
+ OAUTH_OIDC_CONFIG_ENDPOINT = (
38
+ cfg.OAUTH_OIDC_CONFIG_ENDPOINT.get_secret_value()
39
+ )
40
+ raise RuntimeError(
41
+ f"Cannot initialize OpenID client. Original error: '{e}'. "
42
+ f"Hint: is {OAUTH_OIDC_CONFIG_ENDPOINT=} reachable?"
43
+ )
44
+ return open_id
34
45
 
35
46
 
36
47
  def get_oauth_router() -> APIRouter | None:
@@ -44,7 +55,6 @@ def get_oauth_router() -> APIRouter | None:
44
55
  return None
45
56
 
46
57
  client_name = oauth_settings.OAUTH_CLIENT_NAME
47
-
48
58
  if client_name == "google":
49
59
  client = _create_client_google(oauth_settings)
50
60
  elif client_name == "github":
@@ -51,7 +51,7 @@ class Settings(BaseSettings):
51
51
  Only logs of with this level (or higher) will appear in the console logs.
52
52
  """
53
53
 
54
- FRACTAL_API_MAX_JOB_LIST_LENGTH: int = 50
54
+ FRACTAL_API_MAX_JOB_LIST_LENGTH: int = 25
55
55
  """
56
56
  Number of ids that can be stored in the `jobsV2` attribute of
57
57
  `app.state`.
@@ -1,7 +1,7 @@
1
1
  NAMING_CONVENTION = {
2
2
  "ix": "ix_%(column_0_label)s",
3
3
  "uq": "uq_%(table_name)s_%(column_0_name)s",
4
- "ck": "ck_%(table_name)s_`%(constraint_name)s`",
4
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
5
5
  "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
6
6
  "pk": "pk_%(table_name)s",
7
7
  }
@@ -1,8 +1,8 @@
1
- """resource-profile
1
+ """2.17.0
2
2
 
3
- Revision ID: a80ac5a352bf
3
+ Revision ID: 83bc2ad3ffcc
4
4
  Revises: 981d588fe248
5
- Create Date: 2025-10-15 15:53:34.823398
5
+ Create Date: 2025-10-30 14:16:53.639006
6
6
 
7
7
  """
8
8
  import sqlalchemy as sa
@@ -11,7 +11,7 @@ from alembic import op
11
11
  from sqlalchemy.dialects import postgresql
12
12
 
13
13
  # revision identifiers, used by Alembic.
14
- revision = "a80ac5a352bf"
14
+ revision = "83bc2ad3ffcc"
15
15
  down_revision = "981d588fe248"
16
16
  branch_labels = None
17
17
  depends_on = None
@@ -64,13 +64,11 @@ def upgrade() -> None:
64
64
  ),
65
65
  sa.CheckConstraint(
66
66
  "(type = 'local') OR (jobs_slurm_python_worker IS NOT NULL)",
67
- name=op.f(
68
- "ck_resource_`ck_resource_jobs_slurm_python_worker_set`"
69
- ),
67
+ name=op.f("ck_resource_jobs_slurm_python_worker_set"),
70
68
  ),
71
69
  sa.CheckConstraint(
72
70
  "type IN ('local', 'slurm_sudo', 'slurm_ssh')",
73
- name=op.f("ck_resource_`ck_resource_correct_type`"),
71
+ name=op.f("ck_resource_correct_type"),
74
72
  ),
75
73
  sa.PrimaryKeyConstraint("id", name=op.f("pk_resource")),
76
74
  sa.UniqueConstraint("name", name=op.f("uq_resource_name")),
@@ -103,7 +101,7 @@ def upgrade() -> None:
103
101
  ["resource_id"],
104
102
  ["resource.id"],
105
103
  name=op.f("fk_profile_resource_id_resource"),
106
- ondelete="CASCADE",
104
+ ondelete="RESTRICT",
107
105
  ),
108
106
  sa.PrimaryKeyConstraint("id", name=op.f("pk_profile")),
109
107
  sa.UniqueConstraint("name", name=op.f("uq_profile_name")),
@@ -117,7 +115,7 @@ def upgrade() -> None:
117
115
  "resource",
118
116
  ["resource_id"],
119
117
  ["id"],
120
- ondelete="SET NULL",
118
+ ondelete="RESTRICT",
121
119
  )
122
120
 
123
121
  with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
@@ -129,52 +127,54 @@ def upgrade() -> None:
129
127
  "resource",
130
128
  ["resource_id"],
131
129
  ["id"],
132
- ondelete="SET NULL",
130
+ ondelete="RESTRICT",
133
131
  )
134
132
 
135
133
  with op.batch_alter_table("user_oauth", schema=None) as batch_op:
136
134
  batch_op.add_column(
137
135
  sa.Column("profile_id", sa.Integer(), nullable=True)
138
136
  )
137
+ batch_op.add_column(
138
+ sa.Column(
139
+ "project_dir",
140
+ sa.String(),
141
+ server_default="/PLACEHOLDER",
142
+ nullable=False,
143
+ )
144
+ )
145
+ batch_op.add_column(
146
+ sa.Column(
147
+ "slurm_accounts",
148
+ postgresql.ARRAY(sa.String()),
149
+ server_default="{}",
150
+ nullable=True,
151
+ )
152
+ )
139
153
  batch_op.create_foreign_key(
140
154
  batch_op.f("fk_user_oauth_profile_id_profile"),
141
155
  "profile",
142
156
  ["profile_id"],
143
157
  ["id"],
144
- ondelete="SET NULL",
158
+ ondelete="RESTRICT",
145
159
  )
146
-
147
- with op.batch_alter_table("user_settings", schema=None) as batch_op:
148
- batch_op.drop_column("ssh_jobs_dir")
149
- batch_op.drop_column("ssh_tasks_dir")
160
+ batch_op.drop_column("username")
150
161
 
151
162
  # ### end Alembic commands ###
152
163
 
153
164
 
154
165
  def downgrade() -> None:
155
166
  # ### commands auto generated by Alembic - please adjust! ###
156
- with op.batch_alter_table("user_settings", schema=None) as batch_op:
157
- batch_op.add_column(
158
- sa.Column(
159
- "ssh_tasks_dir",
160
- sa.VARCHAR(),
161
- autoincrement=False,
162
- nullable=True,
163
- )
164
- )
167
+ with op.batch_alter_table("user_oauth", schema=None) as batch_op:
165
168
  batch_op.add_column(
166
169
  sa.Column(
167
- "ssh_jobs_dir",
168
- sa.VARCHAR(),
169
- autoincrement=False,
170
- nullable=True,
170
+ "username", sa.VARCHAR(), autoincrement=False, nullable=True
171
171
  )
172
172
  )
173
-
174
- with op.batch_alter_table("user_oauth", schema=None) as batch_op:
175
173
  batch_op.drop_constraint(
176
174
  batch_op.f("fk_user_oauth_profile_id_profile"), type_="foreignkey"
177
175
  )
176
+ batch_op.drop_column("slurm_accounts")
177
+ batch_op.drop_column("project_dir")
178
178
  batch_op.drop_column("profile_id")
179
179
 
180
180
  with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fractal-server
3
- Version: 2.17.0a6
3
+ Version: 2.17.0a8
4
4
  Summary: Backend component of the Fractal analytics platform
5
5
  License-Expression: BSD-3-Clause
6
6
  License-File: LICENSE
@@ -1,4 +1,4 @@
1
- fractal_server/__init__.py,sha256=m6ro3f4bjmvJJ9DFzqslf5NSM9OxOGIbAGvKTxl1rZQ,25
1
+ fractal_server/__init__.py,sha256=BC6X5ShipZaJdAYWlIFWgdo5G_eS3ZXxSgaZP3AQsdU,25
2
2
  fractal_server/__main__.py,sha256=68FlTuST3zbzVofFI8JSYsSBrBQ07Bv3Mu3PsZX9Fw0,11423
3
3
  fractal_server/alembic.ini,sha256=MWwi7GzjzawI9cCAK1LW7NxIBQDUqD12-ptJoq5JpP0,3153
4
4
  fractal_server/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -6,18 +6,18 @@ fractal_server/app/db/__init__.py,sha256=sttX0mHVV0ESI1SJ1kcxUKiuEwqeP-BWsst0o_9
6
6
  fractal_server/app/models/__init__.py,sha256=xJWiGAwpXmCpnFMC4c_HTqoUCzMOXrakoGLUH_uMvdA,415
7
7
  fractal_server/app/models/linkusergroup.py,sha256=3KkkE4QIUAlTrBAZs_tVy0pGvAxUAq6yOEjflct_z2M,678
8
8
  fractal_server/app/models/linkuserproject.py,sha256=hvaxh3Lkiy2uUCwB8gvn8RorCpvxSSdzWdCS_U1GL7g,315
9
- fractal_server/app/models/security.py,sha256=snHmvnjYnq37qcXlUfieTUDz9bf-djOMeXMixP5EW3g,3736
10
- fractal_server/app/models/user_settings.py,sha256=6Cc4r6ZVcASwlcPtnKZfKe2OidzOafQlmzWYZNTpRYU,1126
9
+ fractal_server/app/models/security.py,sha256=VThWDEmzUP4SgLsAvd5WjJ1p2WVxBuc6D5TMQJaOyd8,3873
10
+ fractal_server/app/models/user_settings.py,sha256=u0GOK1JdqDmXzA8hK2JV93rZxY_rF-0oKMkArRolnN8,1201
11
11
  fractal_server/app/models/v2/__init__.py,sha256=A668GF4z_UPar6kAOwC-o_qUo3CIRJ3SmBGYTs3Xc7k,923
12
12
  fractal_server/app/models/v2/accounting.py,sha256=i-2TsjqyuclxFQ21C-TeDoss7ZBTRuXdzIJfVr2UxwE,1081
13
13
  fractal_server/app/models/v2/dataset.py,sha256=P_zy4dPQAqrCALQ6737VkAFk1SvcgYjnslGUZhPI8sc,1226
14
14
  fractal_server/app/models/v2/history.py,sha256=CBN2WVg9vW5pHU1RP8TkB_nnJrwnuifCcxgnd53UtEE,2163
15
15
  fractal_server/app/models/v2/job.py,sha256=YYzt3ef2CU1WXFNjlltR3ft2kM9T0Hq8oskSipQSxuM,2042
16
- fractal_server/app/models/v2/profile.py,sha256=dH5xftMYThd6xqw86a--2MPCysxGTLTsyyEsOCgcK4c,439
17
- fractal_server/app/models/v2/project.py,sha256=b6QZaMutwTfT1e_1mDDuwccgKVs4pcB0SMSYTtvfMrg,866
18
- fractal_server/app/models/v2/resource.py,sha256=RPQqOp1auoiXdPf27QazSXvzuUz9iPN1qfh7cRwxF64,3536
16
+ fractal_server/app/models/v2/profile.py,sha256=QqOE7XGeq-ckQAbGhcgzDN5zFFaTNrtcuWgOXy9psR8,440
17
+ fractal_server/app/models/v2/project.py,sha256=oXNcuNVDeNYZ60fwAx-Y_vnkS3xd9pwFdoT2pZmnBNI,918
18
+ fractal_server/app/models/v2/resource.py,sha256=ReaBGtKb3e0_1PZOZncdGqrttkrC-bsgDCv3wPCGfOs,3512
19
19
  fractal_server/app/models/v2/task.py,sha256=iBIQB8POQE5MyKvLZhw7jZWlBhbrThzCDzRTcgiAczQ,1493
20
- fractal_server/app/models/v2/task_group.py,sha256=sOlBhOkDyIJN19yD358TsQtDJOVSeGhHffrxgB3KkkQ,4753
20
+ fractal_server/app/models/v2/task_group.py,sha256=gHkuyIBw8hkoMCwHgo08SWVMc8T1MXC5xqoW2YNd5Sw,4753
21
21
  fractal_server/app/models/v2/workflow.py,sha256=gBjDXO-RytVT81aAlesImBhmVHrwNUrmsF_UsGa1qLM,1057
22
22
  fractal_server/app/models/v2/workflowtask.py,sha256=qkTc-hcFLpJUVsEUbnDq2BJL0qg9jagy2doZeusF1ek,1266
23
23
  fractal_server/app/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -29,30 +29,30 @@ fractal_server/app/routes/admin/v2/impersonate.py,sha256=ictDjuvBr3iLv3YtwkVRMNQ
29
29
  fractal_server/app/routes/admin/v2/job.py,sha256=sFgMbOtUCIJ-ri6YD3ZWP7XETZZDQsLqPfT1kaH9RHQ,8577
30
30
  fractal_server/app/routes/admin/v2/profile.py,sha256=0Y_1Qv-BA6cHVrxPTDDBOpttpfuJN8g1FqFlG6JiOD8,3164
31
31
  fractal_server/app/routes/admin/v2/project.py,sha256=rRq7ZDngr_29skASnte1xfycZCjK-WPdeTf7siBXiCU,1182
32
- fractal_server/app/routes/admin/v2/resource.py,sha256=uel-MN3f9TkkIJ7flEC4yPH7q2qGQxoaxPB0LxfZzYs,7796
32
+ fractal_server/app/routes/admin/v2/resource.py,sha256=X5ifGY3VEVeGqtixMorh5PT6m5EHS3yg6kzzlFWmlmo,6288
33
33
  fractal_server/app/routes/admin/v2/task.py,sha256=aACI0RXVODclIqfaFYyQ3ncB8YCcd36qy3a0vFGnsfo,4621
34
34
  fractal_server/app/routes/admin/v2/task_group.py,sha256=RV697PI79X9w5zEKQTlQplAsp-vDS4fGyXwqsHc5eY0,6210
35
35
  fractal_server/app/routes/admin/v2/task_group_lifecycle.py,sha256=W7LjIBAheyjrn0fEz0SsWINqcZK5HMB5GRGMjPrc6a4,9994
36
36
  fractal_server/app/routes/api/__init__.py,sha256=ewprevw6hZ0FWM-GPHoQZU0w-yfItqLeQT-Jr_Nbjnw,1658
37
37
  fractal_server/app/routes/api/v2/__init__.py,sha256=D3sRRsqkmZO6kBxUjg40q0aRDsnuXI4sOOfn0xF9JsM,2820
38
- fractal_server/app/routes/api/v2/_aux_functions.py,sha256=8xnXYPjzo1yZfcg18b5ZY5tu9OQ1e55fOnX1IEmDSHk,15019
38
+ fractal_server/app/routes/api/v2/_aux_functions.py,sha256=0OC5ydaX-XpscMRAsqiE03w2UVcd0xZ_vGs2PahDf1Y,14821
39
39
  fractal_server/app/routes/api/v2/_aux_functions_history.py,sha256=PXsqMQ3sfkABqAMI7v1_VAzUEDF_-kvaZyyhEicqsCw,4431
40
40
  fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py,sha256=5gNt35rYR8sHG2f1N8coQbOJacYIRJ5zUrmMEXcs2LQ,8585
41
41
  fractal_server/app/routes/api/v2/_aux_functions_task_version_update.py,sha256=PKjV7r8YsPRXoNiVSnOK4KBYVV3l_Yb_ZPrqAkMkXrQ,1182
42
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py,sha256=eAXGnC7yEYGn1g2VDfSGvApN2mw79r4fW7ykRlDY2CY,12468
42
+ fractal_server/app/routes/api/v2/_aux_functions_tasks.py,sha256=IJ_q_2x3ar1WM8u_3LM2UWdRERZU5g_N2nfQjW9Ujlc,13542
43
43
  fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py,sha256=8x1_q9FyCzItnPmdSdLQuwUTy4B9xCsXscp97_lJcpM,4635
44
44
  fractal_server/app/routes/api/v2/dataset.py,sha256=HhRrReo_isFJFMaUhQfxEQkLj0_lXNfGblzwpLG81VU,8623
45
45
  fractal_server/app/routes/api/v2/history.py,sha256=T-7GDLZ_x4cp9PMEozJoEFt4cGFYPDqv9-akYmN2JSU,17150
46
46
  fractal_server/app/routes/api/v2/images.py,sha256=_R-F1qrsFv3PPukrwXSa-5swNS4kNghY0DTrxjLC_7E,7897
47
47
  fractal_server/app/routes/api/v2/job.py,sha256=fyIYGcplsxzx91FxYWlImp1ATPk5IC8bfIcJkuZOhws,6526
48
48
  fractal_server/app/routes/api/v2/pre_submission_checks.py,sha256=DEnlFj6PMNMy9DrnH6_pMW9evdKl6iTlAb0zlzBR1Qs,4953
49
- fractal_server/app/routes/api/v2/project.py,sha256=eQcQs0ElMpUN0OTcgG7DXk70t9Dn8Fdd6oHeQ0WbgJI,4782
49
+ fractal_server/app/routes/api/v2/project.py,sha256=PkRR1lQayBvY8uEOMherXpvv_YwIMwxWzVMjC5nFfm4,4857
50
50
  fractal_server/app/routes/api/v2/status_legacy.py,sha256=vZA4AYMehXbNFgekl4j6p8idETC3IylQQL64CmFCr98,6330
51
- fractal_server/app/routes/api/v2/submit.py,sha256=l12Duakx0lV43LCyX4C6cCIBsg-BEc-XxjRRe8l8Wys,9203
52
- fractal_server/app/routes/api/v2/task.py,sha256=KDIBrQpIbbVrffOBtxah-gXiOnN-EG84ujXeGPOqKhg,7160
53
- fractal_server/app/routes/api/v2/task_collection.py,sha256=A0mfajyJVVrWNuI7YaPih7juSWOnks78NnJDwnGfvdo,12281
54
- fractal_server/app/routes/api/v2/task_collection_custom.py,sha256=9SqiMPuED5LEGKo3Tiq5KfXFqmjaZPg9KWL-Yn5jdLo,7010
55
- fractal_server/app/routes/api/v2/task_collection_pixi.py,sha256=QHqdnaddTW-DZKrnzLKwCPP5j3UiqxOtFO04aZeexdk,7244
51
+ fractal_server/app/routes/api/v2/submit.py,sha256=o0r58Dc6A1BHOUPp2OK7PKEHc4dIqnp3-kXBWnX8KsY,9373
52
+ fractal_server/app/routes/api/v2/task.py,sha256=88cMBcUL_QQ25GvwAEVSiyncnJ7nZ1EX6PlLNGsgNfU,7485
53
+ fractal_server/app/routes/api/v2/task_collection.py,sha256=03LXiSde7ODVsfARDdgSXWVDYMao2zce-2lqQHelF1M,12196
54
+ fractal_server/app/routes/api/v2/task_collection_custom.py,sha256=WjByW-raK-HU1IR02JNc39XMjkRftXyUSHwHrgnZFBw,6924
55
+ fractal_server/app/routes/api/v2/task_collection_pixi.py,sha256=oSSDyhdrgE71nXjIPPA2et9LlgEKkxIBlpBkrRuEuns,7158
56
56
  fractal_server/app/routes/api/v2/task_group.py,sha256=_M_munannm0wMmLdOwo-qATqrl0Vu_TsrpLb-2554fs,8104
57
57
  fractal_server/app/routes/api/v2/task_group_lifecycle.py,sha256=ZSn8Ibjwkp89v87OPBquULPc9xsdpBfRvV1PiQdiicw,10593
58
58
  fractal_server/app/routes/api/v2/task_version_update.py,sha256=JO5xggiRqay4zNlbjlRa26ygMEue2klSFPyLrXS215k,8254
@@ -64,7 +64,7 @@ fractal_server/app/routes/auth/_aux_auth.py,sha256=fyGxBVb6yrVrsE7-2tTyiJ7orb9Jz
64
64
  fractal_server/app/routes/auth/current_user.py,sha256=BsM_lm9iNiaL8_iMY8NZnfgSXhzEY8xgkQUgtC40h8o,5517
65
65
  fractal_server/app/routes/auth/group.py,sha256=wZnqzIOZHpoUW2Yp6dkAMRqIiM_otVdG4fZuBYmLocA,6919
66
66
  fractal_server/app/routes/auth/login.py,sha256=tSu6OBLOieoBtMZB4JkBAdEgH2Y8KqPGSbwy7NIypIo,566
67
- fractal_server/app/routes/auth/oauth.py,sha256=3TaGZpjgjUyqqbhdcUv1PwgiHdkUk41X5tc98FOIw-s,2288
67
+ fractal_server/app/routes/auth/oauth.py,sha256=5BTEKb7Cb6ASVmgiPb5kVbHcBg0rF8Iqho0aJZd7PPQ,2730
68
68
  fractal_server/app/routes/auth/register.py,sha256=Ola-lNdYYEOK8Wh0Q-TFKwIJ25e6UswlUojJ8QGBCfc,581
69
69
  fractal_server/app/routes/auth/router.py,sha256=-E87A8h2UvcLucy5xjzKiWbXHVKcqxUmmZGeV_utEzA,598
70
70
  fractal_server/app/routes/auth/users.py,sha256=Gjj3_W4Zu1RZJv0r578wPJrbicMmiSELmJGJDeStyus,6878
@@ -99,10 +99,9 @@ fractal_server/config/__init__.py,sha256=ZCmroNB50sUxJiFtkW0a4fFtmfyPnL4LWhtKY5F
99
99
  fractal_server/config/_data.py,sha256=9Jyt83yrSsr_0_9ANWDAXz88_jjyFlcB5VWJGXq8aUY,2311
100
100
  fractal_server/config/_database.py,sha256=YOBi3xuJno5wLGw1hKsjLm-bftaxVWiBNIQWVTMX3Ag,1661
101
101
  fractal_server/config/_email.py,sha256=j1QmZCyspNbD1xxkypc9Kv299tU3vTO1AqDFJ8-LZzQ,4201
102
- fractal_server/config/_main.py,sha256=y1WQPFjEyctsVpRX2LVKfR_nJjFQHAey_i8e6Hmsq6E,1605
102
+ fractal_server/config/_main.py,sha256=L0sLfePAlnvEPxxtzDdY3xJRhzTAvIO5pdEoTd99byY,1605
103
103
  fractal_server/config/_oauth.py,sha256=7J4FphGVFfVmtQycCkas6scEJQJGZUGEzQ-t2PZiqSo,1934
104
104
  fractal_server/config/_settings_config.py,sha256=tsyXQOnn9QKCFJD6hRo_dJXlQQyl70DbqgHMJoZ1xnY,144
105
- fractal_server/data_migrations/2_14_10.py,sha256=jzMg2c1zNO8C_Nho_9_EZJD6kR1-gkFNpNrMR5Hr8hM,1598
106
105
  fractal_server/data_migrations/README.md,sha256=_3AEFvDg9YkybDqCLlFPdDmGJvr6Tw7HRI14aZ3LOIw,398
107
106
  fractal_server/data_migrations/tools.py,sha256=LeMeASwYGtEqd-3wOLle6WARdTGAimoyMmRbbJl-hAM,572
108
107
  fractal_server/exceptions.py,sha256=7ftpWwNsTQmNonWCynhH5ErUh1haPPhIaVPrNHla7-o,53
@@ -114,7 +113,7 @@ fractal_server/images/tools.py,sha256=37jVIU6RiAGbiyucNDlKe9J3yN3Y47NOvv-RJor9Jm
114
113
  fractal_server/logger.py,sha256=2_IN0WcIzLEX20HFUUkwyZGSIi0yLbE0JamkzXREDZw,5302
115
114
  fractal_server/main.py,sha256=4HJO8EiGJp0Iw0p3xEm_zhwjnT4joFG1KuGccIIndjo,4419
116
115
  fractal_server/migrations/env.py,sha256=nfyBpMIOT3kny6t-b-tUjyRjZ4k906bb1_wCQ7me1BI,1353
117
- fractal_server/migrations/naming_convention.py,sha256=htbKrVdetx3pklowb_9Cdo5RqeF0fJ740DNecY5de_M,265
116
+ fractal_server/migrations/naming_convention.py,sha256=bSEMiMZeArmWKrUk-12lhnOw1pAFMg6LEl7yucohPqc,263
118
117
  fractal_server/migrations/versions/034a469ec2eb_task_groups.py,sha256=vrPhC8hfFu1c4HmLHNZyCuqEfecFD8-bWc49bXMNes0,6199
119
118
  fractal_server/migrations/versions/091b01f51f88_add_usergroup_and_linkusergroup_table.py,sha256=-BSS9AFTPcu3gYC-sYbawSy4MWQQx8TfMb5BW5EBKmQ,1450
120
119
  fractal_server/migrations/versions/0f5f85bb2ae7_add_pre_pinned_packages.py,sha256=FxnIvfktnSjy2nBKDIow24on1nMkLw1THhO84FtNZSo,1055
@@ -131,10 +130,10 @@ fractal_server/migrations/versions/5bf02391cfef_v2.py,sha256=axhNkr_H6R4rRbY7oGY
131
130
  fractal_server/migrations/versions/70e77f1c38b0_add_applyworkflow_first_task_index_and_.py,sha256=Q-DsMzG3IcUV2Ol1dhJWosDvKERamBE6QvA2zzS5zpQ,1632
132
131
  fractal_server/migrations/versions/71eefd1dd202_add_slurm_accounts.py,sha256=mbWuCkTpRAdGbRhW7lhXs_e5S6O37UAcCN6JfoY5H8A,1353
133
132
  fractal_server/migrations/versions/791ce783d3d8_add_indices.py,sha256=gNE6AgJgeJZY99Fbd336Z9see3gRMQvuNBC0xDk_5sw,1154
133
+ fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py,sha256=U7t_8n58taRkd9sxCXOshrTr9M5AhlsQne8SGKa5Jt4,6377
134
134
  fractal_server/migrations/versions/84bf0fffde30_add_dumps_to_applyworkflow.py,sha256=NSCuhANChsg76vBkShBl-9tQ4VEHubOjtAv1etHhlvY,2684
135
135
  fractal_server/migrations/versions/8e8f227a3e36_update_taskv2_post_2_7_0.py,sha256=68y9-fpSuKx6KPtM_9n8Ho0I1qwa8IoG-yJqXUYQrGg,1111
136
136
  fractal_server/migrations/versions/8f79bd162e35_add_docs_info_and_docs_link_to_task_.py,sha256=6pgODDtyAxevZvAJBj9IJ41inhV1RpwbpZr_qfPPu1A,1115
137
- fractal_server/migrations/versions/90f6508c6379_drop_useroauth_username.py,sha256=NomnX0HH2M4wvNOnsla6hNx8270stB-WBZrl94xlGNk,897
138
137
  fractal_server/migrations/versions/94a47ea2d3ff_remove_cache_dir_slurm_user_and_slurm_.py,sha256=yL3-Hvzw5jBLKj4LFP1z5ofZE9L9W3tLwYtPNW7z4ko,1508
139
138
  fractal_server/migrations/versions/969d84257cac_add_historyrun_task_id.py,sha256=4nLSYEMp_Tm7VRfo8p9YKLHVnoizTXaPV6lfcfvWhj0,1143
140
139
  fractal_server/migrations/versions/97f444d47249_add_applyworkflow_project_dump.py,sha256=eKTZm3EgUgapXBxO0RuHkEfTKic-TZG3ADaMpGLuc0k,1057
@@ -144,7 +143,6 @@ fractal_server/migrations/versions/9c5ae74c9b98_add_user_settings_table.py,sha25
144
143
  fractal_server/migrations/versions/9db60297b8b2_set_ondelete.py,sha256=F0IdXk8vclViOGKe2SOHO3MsQsqe7SsZRSqz9cXhhrE,7928
145
144
  fractal_server/migrations/versions/9fd26a2b0de4_add_workflow_timestamp_created.py,sha256=4l1AHGUsa0ONoJVZlr3fTXw_xbbQ8O7wlD92Az2aRfM,1849
146
145
  fractal_server/migrations/versions/a7f4d6137b53_add_workflow_dump_to_applyworkflow.py,sha256=ekDUML7ILpmdoqEclKbEUdyLi4uw9HSG_sTjG2hp_JE,867
147
- fractal_server/migrations/versions/a80ac5a352bf_resource_profile.py,sha256=sKJmepSBxz91xZx8fyM1LZt-ajit_kzGa3dHTXe-YWo,6371
148
146
  fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py,sha256=BftudWuSGvKGBzIL5AMb3yWkgTAuaKPBGsYcOzp_gLQ,1899
149
147
  fractal_server/migrations/versions/af8673379a5c_drop_old_filter_columns.py,sha256=9sLd0F7nO5chHHm7RZ4wBA-9bvWomS-av_odKwODADM,1551
150
148
  fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py,sha256=loDrqBB-9U3vqLKePEeJy4gK4EuPs_1F345mdrnoCt0,1293
@@ -159,7 +157,6 @@ fractal_server/migrations/versions/e81103413827_add_job_type_filters.py,sha256=t
159
157
  fractal_server/migrations/versions/efa89c30e0a4_add_project_timestamp_created.py,sha256=jilQW3QIqYQ4Q6hCnUiG7UtNMpA41ujqrB3tPFiPM1Q,1221
160
158
  fractal_server/migrations/versions/f37aceb45062_make_historyunit_logfile_required.py,sha256=jLHcVq9z0Ou20u-mwPf6EICDKY4dwFAzBgbRRx9_xDw,1007
161
159
  fractal_server/migrations/versions/f384e1c0cf5d_drop_task_default_args_columns.py,sha256=9BwqUS9Gf7UW_KjrzHbtViC880qhD452KAytkHWWZyk,746
162
- fractal_server/migrations/versions/f65ee53991e3_user_settings_related.py,sha256=q9oaVpp85X4EZt3f33ThyBDZD3_9trO2va9uAIh2yK0,1883
163
160
  fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py,sha256=TDWCaIoM0Q4SpRWmR9zr_rdp3lJXhCfBPTMhtrP5xYE,3950
164
161
  fractal_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
165
162
  fractal_server/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -257,8 +254,8 @@ fractal_server/types/validators/_workflow_task_arguments_validators.py,sha256=HL
257
254
  fractal_server/urls.py,sha256=QjIKAC1a46bCdiPMu3AlpgFbcv6a4l3ABcd5xz190Og,471
258
255
  fractal_server/utils.py,sha256=SYVVUuXe_nWyrJLsy7QA-KJscwc5PHEXjvsW4TK7XQI,2180
259
256
  fractal_server/zip_tools.py,sha256=H0w7wS5yE4ebj7hw1_77YQ959dl2c-L0WX6J_ro1TY4,4884
260
- fractal_server-2.17.0a6.dist-info/METADATA,sha256=X3vrUW4wnIOGZ8jAWbE4Ks9CaPszKHL2WuSPHtBk_aM,4226
261
- fractal_server-2.17.0a6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
262
- fractal_server-2.17.0a6.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
263
- fractal_server-2.17.0a6.dist-info/licenses/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
264
- fractal_server-2.17.0a6.dist-info/RECORD,,
257
+ fractal_server-2.17.0a8.dist-info/METADATA,sha256=pq54GpIDEoMvbVhOdhAZFeogzh1wQNJ0WcbSi0DsdSw,4226
258
+ fractal_server-2.17.0a8.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
259
+ fractal_server-2.17.0a8.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
260
+ fractal_server-2.17.0a8.dist-info/licenses/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
261
+ fractal_server-2.17.0a8.dist-info/RECORD,,
@@ -1,48 +0,0 @@
1
- import logging
2
-
3
- from sqlmodel import select
4
-
5
- from fractal_server.app.db import get_sync_db
6
- from fractal_server.app.models import HistoryRun
7
- from fractal_server.app.models import TaskV2
8
- from fractal_server.app.models import WorkflowTaskV2
9
-
10
- logger = logging.getLogger("fix_db")
11
- logger.setLevel(logging.INFO)
12
-
13
-
14
- def fix_db():
15
- logger.info("START execution of fix_db function")
16
-
17
- with next(get_sync_db()) as db:
18
- stm = select(HistoryRun).order_by(HistoryRun.id)
19
- history_runs = db.execute(stm).scalars().all()
20
-
21
- for hr in history_runs:
22
- logger.info(f"HistoryRun[{hr.id}] START")
23
- if hr.workflowtask_id is None:
24
- continue
25
- wft = db.get(WorkflowTaskV2, hr.workflowtask_id)
26
- if wft is None:
27
- logger.warning(
28
- f"WorkflowTaskV2[{hr.workflowtask_id}] not found. "
29
- "Trying to use HistoryRun.workflowtask_dump"
30
- )
31
- task_id = hr.workflowtask_dump.get("task_id")
32
- if task_id is not None and db.get(TaskV2, task_id) is not None:
33
- hr.task_id = task_id
34
- else:
35
- logger.warning(f"TaskV2[{task_id}] not found")
36
- else:
37
- hr.task_id = wft.task_id
38
- logger.info(
39
- f"HistoryRun[{hr.id}].task_id set to {wft.task_id}"
40
- )
41
-
42
- db.add(hr)
43
- logger.info(f"HistoryRun[{hr.id}] END")
44
-
45
- db.commit()
46
- logger.info("Changes committed.")
47
-
48
- logger.info("END execution of fix_db function")
@@ -1,36 +0,0 @@
1
- """drop useroauth.username
2
-
3
- Revision ID: 90f6508c6379
4
- Revises: a80ac5a352bf
5
- Create Date: 2025-10-15 16:09:40.922407
6
-
7
- """
8
- import sqlalchemy as sa
9
- from alembic import op
10
-
11
-
12
- # revision identifiers, used by Alembic.
13
- revision = "90f6508c6379"
14
- down_revision = "a80ac5a352bf"
15
- branch_labels = None
16
- depends_on = None
17
-
18
-
19
- def upgrade() -> None:
20
- # ### commands auto generated by Alembic - please adjust! ###
21
- with op.batch_alter_table("user_oauth", schema=None) as batch_op:
22
- batch_op.drop_column("username")
23
-
24
- # ### end Alembic commands ###
25
-
26
-
27
- def downgrade() -> None:
28
- # ### commands auto generated by Alembic - please adjust! ###
29
- with op.batch_alter_table("user_oauth", schema=None) as batch_op:
30
- batch_op.add_column(
31
- sa.Column(
32
- "username", sa.VARCHAR(), autoincrement=False, nullable=True
33
- )
34
- )
35
-
36
- # ### end Alembic commands ###
@@ -1,67 +0,0 @@
1
- """user-settings-related
2
-
3
- Revision ID: f65ee53991e3
4
- Revises: 90f6508c6379
5
- Create Date: 2025-10-22 15:16:42.217910
6
-
7
- """
8
- import sqlalchemy as sa
9
- from alembic import op
10
- from sqlalchemy.dialects import postgresql
11
-
12
- # revision identifiers, used by Alembic.
13
- revision = "f65ee53991e3"
14
- down_revision = "90f6508c6379"
15
- branch_labels = None
16
- depends_on = None
17
-
18
-
19
- def upgrade() -> None:
20
- # ### commands auto generated by Alembic - please adjust! ###
21
- with op.batch_alter_table("user_oauth", schema=None) as batch_op:
22
- batch_op.add_column(
23
- sa.Column(
24
- "project_dir",
25
- sa.String(),
26
- server_default="/PLACEHOLDER",
27
- nullable=False,
28
- )
29
- )
30
- batch_op.add_column(
31
- sa.Column(
32
- "slurm_accounts",
33
- postgresql.ARRAY(sa.String()),
34
- server_default="{}",
35
- nullable=True,
36
- )
37
- )
38
- batch_op.drop_constraint(
39
- batch_op.f("fk_user_oauth_user_settings_id_user_settings"),
40
- type_="foreignkey",
41
- )
42
- batch_op.drop_column("user_settings_id")
43
-
44
- # ### end Alembic commands ###
45
-
46
-
47
- def downgrade() -> None:
48
- # ### commands auto generated by Alembic - please adjust! ###
49
- with op.batch_alter_table("user_oauth", schema=None) as batch_op:
50
- batch_op.add_column(
51
- sa.Column(
52
- "user_settings_id",
53
- sa.INTEGER(),
54
- autoincrement=False,
55
- nullable=True,
56
- )
57
- )
58
- batch_op.create_foreign_key(
59
- batch_op.f("fk_user_oauth_user_settings_id_user_settings"),
60
- "user_settings",
61
- ["user_settings_id"],
62
- ["id"],
63
- )
64
- batch_op.drop_column("slurm_accounts")
65
- batch_op.drop_column("project_dir")
66
-
67
- # ### end Alembic commands ###