fractal-server 2.18.5__py3-none-any.whl → 2.19.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fractal_server/__init__.py +1 -1
- fractal_server/app/db/__init__.py +1 -7
- fractal_server/app/models/security.py +16 -0
- fractal_server/app/models/v2/dataset.py +0 -4
- fractal_server/app/models/v2/job.py +4 -0
- fractal_server/app/models/v2/task.py +0 -1
- fractal_server/app/models/v2/task_group.py +4 -0
- fractal_server/app/models/v2/workflow.py +2 -0
- fractal_server/app/models/v2/workflowtask.py +3 -0
- fractal_server/app/routes/admin/v2/job.py +0 -2
- fractal_server/app/routes/admin/v2/sharing.py +47 -0
- fractal_server/app/routes/admin/v2/task.py +0 -5
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +6 -0
- fractal_server/app/routes/api/__init__.py +4 -52
- fractal_server/app/routes/api/alive.py +13 -0
- fractal_server/app/routes/api/settings.py +44 -0
- fractal_server/app/routes/api/v2/__init__.py +0 -2
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +1 -20
- fractal_server/app/routes/api/v2/dataset.py +9 -8
- fractal_server/app/routes/api/v2/history.py +8 -8
- fractal_server/app/routes/api/v2/images.py +6 -6
- fractal_server/app/routes/api/v2/job.py +10 -12
- fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
- fractal_server/app/routes/api/v2/project.py +7 -9
- fractal_server/app/routes/api/v2/sharing.py +17 -9
- fractal_server/app/routes/api/v2/submit.py +5 -3
- fractal_server/app/routes/api/v2/task.py +7 -9
- fractal_server/app/routes/api/v2/task_collection.py +4 -2
- fractal_server/app/routes/api/v2/task_collection_custom.py +2 -2
- fractal_server/app/routes/api/v2/task_collection_pixi.py +4 -2
- fractal_server/app/routes/api/v2/task_group.py +10 -30
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +10 -4
- fractal_server/app/routes/api/v2/task_version_update.py +4 -3
- fractal_server/app/routes/api/v2/workflow.py +10 -11
- fractal_server/app/routes/api/v2/workflow_import.py +14 -45
- fractal_server/app/routes/api/v2/workflowtask.py +7 -12
- fractal_server/app/routes/auth/__init__.py +18 -1
- fractal_server/app/routes/auth/current_user.py +8 -0
- fractal_server/app/routes/auth/oauth.py +3 -1
- fractal_server/app/routes/auth/users.py +11 -0
- fractal_server/app/routes/aux/_versions.py +42 -0
- fractal_server/app/schemas/user.py +7 -0
- fractal_server/app/schemas/v2/__init__.py +0 -1
- fractal_server/app/schemas/v2/dumps.py +0 -1
- fractal_server/app/schemas/v2/task.py +0 -5
- fractal_server/app/schemas/v2/workflow.py +2 -0
- fractal_server/app/schemas/v2/workflowtask.py +6 -2
- fractal_server/app/security/__init__.py +8 -3
- fractal_server/migrations/versions/18a26fcdea5d_drop_dataset_history.py +41 -0
- fractal_server/migrations/versions/1bf8785755f9_add_description_to_workflow_and_.py +53 -0
- fractal_server/migrations/versions/5fb08bf05b14_drop_taskv2_source.py +36 -0
- fractal_server/migrations/versions/cfd13f7954e7_add_fractal_server_version_to_jobv2_and_.py +52 -0
- fractal_server/migrations/versions/e53dc51fdf93_add_useroauth_is_guest.py +36 -0
- fractal_server/runner/executors/local/runner.py +2 -0
- fractal_server/runner/executors/slurm_ssh/runner.py +5 -0
- fractal_server/runner/executors/slurm_sudo/runner.py +5 -0
- fractal_server/runner/v2/runner.py +0 -1
- fractal_server/runner/v2/submit_workflow.py +0 -3
- {fractal_server-2.18.5.dist-info → fractal_server-2.19.0.dist-info}/METADATA +3 -3
- {fractal_server-2.18.5.dist-info → fractal_server-2.19.0.dist-info}/RECORD +63 -56
- {fractal_server-2.18.5.dist-info → fractal_server-2.19.0.dist-info}/WHEEL +1 -1
- fractal_server/app/routes/api/v2/status_legacy.py +0 -156
- {fractal_server-2.18.5.dist-info → fractal_server-2.19.0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.18.5.dist-info → fractal_server-2.19.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -17,7 +17,8 @@ from fractal_server.app.models import LinkUserGroup
|
|
|
17
17
|
from fractal_server.app.models import UserOAuth
|
|
18
18
|
from fractal_server.app.models.v2 import TaskGroupV2
|
|
19
19
|
from fractal_server.app.models.v2 import TaskV2
|
|
20
|
-
from fractal_server.app.routes.auth import
|
|
20
|
+
from fractal_server.app.routes.auth import get_api_guest
|
|
21
|
+
from fractal_server.app.routes.auth import get_api_user
|
|
21
22
|
from fractal_server.app.schemas.v2 import TaskType
|
|
22
23
|
from fractal_server.app.schemas.v2 import WorkflowTaskRead
|
|
23
24
|
from fractal_server.app.schemas.v2 import WorkflowTaskReplace
|
|
@@ -77,7 +78,7 @@ class TaskVersionRead(BaseModel):
|
|
|
77
78
|
async def get_workflow_version_update_candidates(
|
|
78
79
|
project_id: int,
|
|
79
80
|
workflow_id: int,
|
|
80
|
-
user: UserOAuth = Depends(
|
|
81
|
+
user: UserOAuth = Depends(get_api_guest),
|
|
81
82
|
db: AsyncSession = Depends(get_async_db),
|
|
82
83
|
) -> list[list[TaskVersionRead]]:
|
|
83
84
|
workflow = await _get_workflow_check_access(
|
|
@@ -180,7 +181,7 @@ async def replace_workflowtask(
|
|
|
180
181
|
workflow_task_id: int,
|
|
181
182
|
task_id: int,
|
|
182
183
|
replace: WorkflowTaskReplace,
|
|
183
|
-
user: UserOAuth = Depends(
|
|
184
|
+
user: UserOAuth = Depends(get_api_user),
|
|
184
185
|
db: AsyncSession = Depends(get_async_db),
|
|
185
186
|
) -> WorkflowTaskRead:
|
|
186
187
|
# Get objects from database
|
|
@@ -14,7 +14,8 @@ from fractal_server.app.models import UserOAuth
|
|
|
14
14
|
from fractal_server.app.models.v2 import JobV2
|
|
15
15
|
from fractal_server.app.models.v2 import TaskGroupV2
|
|
16
16
|
from fractal_server.app.models.v2 import WorkflowV2
|
|
17
|
-
from fractal_server.app.routes.auth import
|
|
17
|
+
from fractal_server.app.routes.auth import get_api_guest
|
|
18
|
+
from fractal_server.app.routes.auth import get_api_user
|
|
18
19
|
from fractal_server.app.schemas.v2 import WorkflowCreate
|
|
19
20
|
from fractal_server.app.schemas.v2 import WorkflowExport
|
|
20
21
|
from fractal_server.app.schemas.v2 import WorkflowRead
|
|
@@ -39,7 +40,7 @@ router = APIRouter()
|
|
|
39
40
|
)
|
|
40
41
|
async def get_workflow_list(
|
|
41
42
|
project_id: int,
|
|
42
|
-
user: UserOAuth = Depends(
|
|
43
|
+
user: UserOAuth = Depends(get_api_guest),
|
|
43
44
|
db: AsyncSession = Depends(get_async_db),
|
|
44
45
|
) -> list[WorkflowRead] | None:
|
|
45
46
|
"""
|
|
@@ -69,7 +70,7 @@ async def get_workflow_list(
|
|
|
69
70
|
async def create_workflow(
|
|
70
71
|
project_id: int,
|
|
71
72
|
workflow: WorkflowCreate,
|
|
72
|
-
user: UserOAuth = Depends(
|
|
73
|
+
user: UserOAuth = Depends(get_api_user),
|
|
73
74
|
db: AsyncSession = Depends(get_async_db),
|
|
74
75
|
) -> WorkflowRead | None:
|
|
75
76
|
"""
|
|
@@ -89,7 +90,6 @@ async def create_workflow(
|
|
|
89
90
|
db.add(db_workflow)
|
|
90
91
|
await db.commit()
|
|
91
92
|
await db.refresh(db_workflow)
|
|
92
|
-
await db.close()
|
|
93
93
|
return db_workflow
|
|
94
94
|
|
|
95
95
|
|
|
@@ -100,7 +100,7 @@ async def create_workflow(
|
|
|
100
100
|
async def read_workflow(
|
|
101
101
|
project_id: int,
|
|
102
102
|
workflow_id: int,
|
|
103
|
-
user: UserOAuth = Depends(
|
|
103
|
+
user: UserOAuth = Depends(get_api_guest),
|
|
104
104
|
db: AsyncSession = Depends(get_async_db),
|
|
105
105
|
) -> WorkflowReadWithWarnings | None:
|
|
106
106
|
"""
|
|
@@ -135,7 +135,7 @@ async def update_workflow(
|
|
|
135
135
|
project_id: int,
|
|
136
136
|
workflow_id: int,
|
|
137
137
|
patch: WorkflowUpdate,
|
|
138
|
-
user: UserOAuth = Depends(
|
|
138
|
+
user: UserOAuth = Depends(get_api_user),
|
|
139
139
|
db: AsyncSession = Depends(get_async_db),
|
|
140
140
|
) -> WorkflowReadWithWarnings | None:
|
|
141
141
|
"""
|
|
@@ -149,7 +149,7 @@ async def update_workflow(
|
|
|
149
149
|
db=db,
|
|
150
150
|
)
|
|
151
151
|
|
|
152
|
-
if patch.name:
|
|
152
|
+
if patch.name and patch.name != workflow.name:
|
|
153
153
|
await _check_workflow_exists(
|
|
154
154
|
name=patch.name, project_id=project_id, db=db
|
|
155
155
|
)
|
|
@@ -189,7 +189,6 @@ async def update_workflow(
|
|
|
189
189
|
|
|
190
190
|
await db.commit()
|
|
191
191
|
await db.refresh(workflow)
|
|
192
|
-
await db.close()
|
|
193
192
|
|
|
194
193
|
wftask_list_with_warnings = await _add_warnings_to_workflow_tasks(
|
|
195
194
|
wftask_list=workflow.task_list, user_id=user.id, db=db
|
|
@@ -210,7 +209,7 @@ async def update_workflow(
|
|
|
210
209
|
async def delete_workflow(
|
|
211
210
|
project_id: int,
|
|
212
211
|
workflow_id: int,
|
|
213
|
-
user: UserOAuth = Depends(
|
|
212
|
+
user: UserOAuth = Depends(get_api_user),
|
|
214
213
|
db: AsyncSession = Depends(get_async_db),
|
|
215
214
|
) -> Response:
|
|
216
215
|
"""
|
|
@@ -256,7 +255,7 @@ async def delete_workflow(
|
|
|
256
255
|
async def export_workflow(
|
|
257
256
|
project_id: int,
|
|
258
257
|
workflow_id: int,
|
|
259
|
-
user: UserOAuth = Depends(
|
|
258
|
+
user: UserOAuth = Depends(get_api_guest),
|
|
260
259
|
db: AsyncSession = Depends(get_async_db),
|
|
261
260
|
) -> WorkflowExport | None:
|
|
262
261
|
"""
|
|
@@ -297,7 +296,7 @@ class WorkflowTaskTypeFiltersInfo(BaseModel):
|
|
|
297
296
|
async def get_workflow_type_filters(
|
|
298
297
|
project_id: int,
|
|
299
298
|
workflow_id: int,
|
|
300
|
-
user: UserOAuth = Depends(
|
|
299
|
+
user: UserOAuth = Depends(get_api_guest),
|
|
301
300
|
db: AsyncSession = Depends(get_async_db),
|
|
302
301
|
) -> list[WorkflowTaskTypeFiltersInfo]:
|
|
303
302
|
"""
|
|
@@ -15,12 +15,12 @@ from fractal_server.app.models.v2 import WorkflowV2
|
|
|
15
15
|
from fractal_server.app.routes.api.v2._aux_task_group_disambiguation import (
|
|
16
16
|
_disambiguate_task_groups,
|
|
17
17
|
)
|
|
18
|
-
from fractal_server.app.routes.auth import
|
|
18
|
+
from fractal_server.app.routes.auth import get_api_user
|
|
19
19
|
from fractal_server.app.routes.auth._aux_auth import (
|
|
20
20
|
_get_default_usergroup_id_or_none,
|
|
21
21
|
)
|
|
22
|
+
from fractal_server.app.routes.aux._versions import _version_sort_key
|
|
22
23
|
from fractal_server.app.schemas.v2 import TaskImport
|
|
23
|
-
from fractal_server.app.schemas.v2 import TaskImportLegacy
|
|
24
24
|
from fractal_server.app.schemas.v2 import WorkflowImport
|
|
25
25
|
from fractal_server.app.schemas.v2 import WorkflowReadWithWarnings
|
|
26
26
|
from fractal_server.app.schemas.v2 import WorkflowTaskCreate
|
|
@@ -73,32 +73,6 @@ async def _get_user_accessible_taskgroups(
|
|
|
73
73
|
return accessible_task_groups
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
async def _get_task_by_source(
|
|
77
|
-
source: str,
|
|
78
|
-
task_groups_list: list[TaskGroupV2],
|
|
79
|
-
) -> int | None:
|
|
80
|
-
"""
|
|
81
|
-
Find task with a given source.
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
source: `source` of the task to be imported.
|
|
85
|
-
task_groups_list: Current list of valid task groups.
|
|
86
|
-
|
|
87
|
-
Return:
|
|
88
|
-
`id` of the matching task, or `None`.
|
|
89
|
-
"""
|
|
90
|
-
task_id = next(
|
|
91
|
-
iter(
|
|
92
|
-
task.id
|
|
93
|
-
for task_group in task_groups_list
|
|
94
|
-
for task in task_group.task_list
|
|
95
|
-
if task.source == source
|
|
96
|
-
),
|
|
97
|
-
None,
|
|
98
|
-
)
|
|
99
|
-
return task_id
|
|
100
|
-
|
|
101
|
-
|
|
102
76
|
async def _get_task_by_taskimport(
|
|
103
77
|
*,
|
|
104
78
|
task_import: TaskImport,
|
|
@@ -141,14 +115,15 @@ async def _get_task_by_taskimport(
|
|
|
141
115
|
return None
|
|
142
116
|
|
|
143
117
|
# Determine target `version`
|
|
144
|
-
# Note that task_import.version cannot be "", due to a validator
|
|
145
118
|
if task_import.version is None:
|
|
146
119
|
logger.debug(
|
|
147
120
|
"[_get_task_by_taskimport] "
|
|
148
121
|
"No version requested, looking for latest."
|
|
149
122
|
)
|
|
150
|
-
|
|
151
|
-
|
|
123
|
+
version = max(
|
|
124
|
+
[tg.version for tg in matching_task_groups],
|
|
125
|
+
key=_version_sort_key,
|
|
126
|
+
)
|
|
152
127
|
logger.debug(
|
|
153
128
|
f"[_get_task_by_taskimport] Latest version set to {version}."
|
|
154
129
|
)
|
|
@@ -213,7 +188,7 @@ async def _get_task_by_taskimport(
|
|
|
213
188
|
async def import_workflow(
|
|
214
189
|
project_id: int,
|
|
215
190
|
workflow_import: WorkflowImport,
|
|
216
|
-
user: UserOAuth = Depends(
|
|
191
|
+
user: UserOAuth = Depends(get_api_user),
|
|
217
192
|
db: AsyncSession = Depends(get_async_db),
|
|
218
193
|
) -> WorkflowReadWithWarnings:
|
|
219
194
|
"""
|
|
@@ -246,19 +221,13 @@ async def import_workflow(
|
|
|
246
221
|
list_task_ids = []
|
|
247
222
|
for wf_task in workflow_import.task_list:
|
|
248
223
|
task_import = wf_task.task
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
task_import=task_import,
|
|
257
|
-
user_id=user.id,
|
|
258
|
-
default_group_id=default_group_id,
|
|
259
|
-
task_groups_list=task_group_list,
|
|
260
|
-
db=db,
|
|
261
|
-
)
|
|
224
|
+
task_id = await _get_task_by_taskimport(
|
|
225
|
+
task_import=task_import,
|
|
226
|
+
user_id=user.id,
|
|
227
|
+
default_group_id=default_group_id,
|
|
228
|
+
task_groups_list=task_group_list,
|
|
229
|
+
db=db,
|
|
230
|
+
)
|
|
262
231
|
if task_id is None:
|
|
263
232
|
raise HTTPException(
|
|
264
233
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
@@ -9,7 +9,8 @@ from fastapi import status
|
|
|
9
9
|
from fractal_server.app.db import AsyncSession
|
|
10
10
|
from fractal_server.app.db import get_async_db
|
|
11
11
|
from fractal_server.app.models import UserOAuth
|
|
12
|
-
from fractal_server.app.routes.auth import
|
|
12
|
+
from fractal_server.app.routes.auth import get_api_guest
|
|
13
|
+
from fractal_server.app.routes.auth import get_api_user
|
|
13
14
|
from fractal_server.app.schemas.v2 import TaskType
|
|
14
15
|
from fractal_server.app.schemas.v2 import WorkflowTaskCreate
|
|
15
16
|
from fractal_server.app.schemas.v2 import WorkflowTaskRead
|
|
@@ -36,7 +37,7 @@ async def create_workflowtask(
|
|
|
36
37
|
workflow_id: int,
|
|
37
38
|
task_id: int,
|
|
38
39
|
wftask: WorkflowTaskCreate,
|
|
39
|
-
user: UserOAuth = Depends(
|
|
40
|
+
user: UserOAuth = Depends(get_api_user),
|
|
40
41
|
db: AsyncSession = Depends(get_async_db),
|
|
41
42
|
) -> WorkflowTaskRead | None:
|
|
42
43
|
"""
|
|
@@ -106,7 +107,7 @@ async def read_workflowtask(
|
|
|
106
107
|
project_id: int,
|
|
107
108
|
workflow_id: int,
|
|
108
109
|
workflow_task_id: int,
|
|
109
|
-
user: UserOAuth = Depends(
|
|
110
|
+
user: UserOAuth = Depends(get_api_guest),
|
|
110
111
|
db: AsyncSession = Depends(get_async_db),
|
|
111
112
|
):
|
|
112
113
|
workflow_task, _ = await _get_workflow_task_check_access(
|
|
@@ -129,7 +130,7 @@ async def update_workflowtask(
|
|
|
129
130
|
workflow_id: int,
|
|
130
131
|
workflow_task_id: int,
|
|
131
132
|
workflow_task_update: WorkflowTaskUpdate,
|
|
132
|
-
user: UserOAuth = Depends(
|
|
133
|
+
user: UserOAuth = Depends(get_api_user),
|
|
133
134
|
db: AsyncSession = Depends(get_async_db),
|
|
134
135
|
) -> WorkflowTaskRead | None:
|
|
135
136
|
"""
|
|
@@ -192,17 +193,11 @@ async def update_workflowtask(
|
|
|
192
193
|
if not actual_args:
|
|
193
194
|
actual_args = None
|
|
194
195
|
setattr(db_wf_task, key, actual_args)
|
|
195
|
-
elif key in ["meta_parallel", "meta_non_parallel", "type_filters"]:
|
|
196
|
-
setattr(db_wf_task, key, value)
|
|
197
196
|
else:
|
|
198
|
-
|
|
199
|
-
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
200
|
-
detail=f"patch_workflow_task endpoint cannot set {key=}",
|
|
201
|
-
)
|
|
197
|
+
setattr(db_wf_task, key, value)
|
|
202
198
|
|
|
203
199
|
await db.commit()
|
|
204
200
|
await db.refresh(db_wf_task)
|
|
205
|
-
await db.close()
|
|
206
201
|
|
|
207
202
|
return db_wf_task
|
|
208
203
|
|
|
@@ -215,7 +210,7 @@ async def delete_workflowtask(
|
|
|
215
210
|
project_id: int,
|
|
216
211
|
workflow_id: int,
|
|
217
212
|
workflow_task_id: int,
|
|
218
|
-
user: UserOAuth = Depends(
|
|
213
|
+
user: UserOAuth = Depends(get_api_user),
|
|
219
214
|
db: AsyncSession = Depends(get_async_db),
|
|
220
215
|
) -> Response:
|
|
221
216
|
"""
|
|
@@ -57,7 +57,7 @@ current_user_act_ver = fastapi_users.current_user(
|
|
|
57
57
|
)
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
async def
|
|
60
|
+
async def get_api_guest(
|
|
61
61
|
user: UserOAuth = Depends(current_user_act_ver),
|
|
62
62
|
) -> UserOAuth:
|
|
63
63
|
"""
|
|
@@ -76,6 +76,23 @@ async def current_user_act_ver_prof(
|
|
|
76
76
|
return user
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
async def get_api_user(
|
|
80
|
+
user: UserOAuth = Depends(get_api_guest),
|
|
81
|
+
) -> UserOAuth:
|
|
82
|
+
"""
|
|
83
|
+
Require a active&verified non-guest user, with a non-null `profile_id`.
|
|
84
|
+
|
|
85
|
+
Raises 401 if user does not exist or is not active.
|
|
86
|
+
Raises 403 if user is not verified, is a guest or has null `profile_id`.
|
|
87
|
+
"""
|
|
88
|
+
if user.is_guest:
|
|
89
|
+
raise HTTPException(
|
|
90
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
91
|
+
detail="This feature is not available for guest users.",
|
|
92
|
+
)
|
|
93
|
+
return user
|
|
94
|
+
|
|
95
|
+
|
|
79
96
|
current_superuser_act = fastapi_users.current_user(
|
|
80
97
|
active=True,
|
|
81
98
|
superuser=True,
|
|
@@ -4,6 +4,8 @@ Definition of `/auth/current-user/` endpoints
|
|
|
4
4
|
|
|
5
5
|
from fastapi import APIRouter
|
|
6
6
|
from fastapi import Depends
|
|
7
|
+
from fastapi import HTTPException
|
|
8
|
+
from fastapi import status
|
|
7
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
10
|
from sqlmodel import select
|
|
9
11
|
|
|
@@ -52,6 +54,12 @@ async def patch_current_user(
|
|
|
52
54
|
Note: a user cannot patch their own password (as enforced within the
|
|
53
55
|
`UserUpdateStrict` schema).
|
|
54
56
|
"""
|
|
57
|
+
if current_user.is_guest:
|
|
58
|
+
raise HTTPException(
|
|
59
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
60
|
+
detail="This feature is not available for guest users.",
|
|
61
|
+
)
|
|
62
|
+
|
|
55
63
|
update = UserUpdate(**user_update.model_dump(exclude_unset=True))
|
|
56
64
|
|
|
57
65
|
# NOTE: here it would be relevant to catch an `InvalidPasswordException`
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
1
3
|
from fastapi import APIRouter
|
|
2
4
|
from httpx_oauth.clients.github import GitHubOAuth2
|
|
3
5
|
from httpx_oauth.clients.google import GoogleOAuth2
|
|
@@ -25,7 +27,7 @@ class FractalOpenID(OpenID):
|
|
|
25
27
|
super().__init__(**kwargs)
|
|
26
28
|
self.email_claim = email_claim
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
@override
|
|
29
31
|
async def get_id_email(self, token: str) -> tuple[str, str | None]:
|
|
30
32
|
"""
|
|
31
33
|
Identical to the parent-class method (httpx-oauth version 0.16.1),
|
|
@@ -83,6 +83,17 @@ async def patch_user(
|
|
|
83
83
|
db=db,
|
|
84
84
|
)
|
|
85
85
|
|
|
86
|
+
will_be_superuser = (
|
|
87
|
+
user_update.is_superuser
|
|
88
|
+
if user_update.is_superuser is not None
|
|
89
|
+
else user_to_patch.is_superuser
|
|
90
|
+
)
|
|
91
|
+
if user_update.is_guest and will_be_superuser:
|
|
92
|
+
raise HTTPException(
|
|
93
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
94
|
+
detail="Superuser cannot be guest.",
|
|
95
|
+
)
|
|
96
|
+
|
|
86
97
|
# Modify user attributes
|
|
87
98
|
try:
|
|
88
99
|
user = await user_manager.update(
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
from fastapi import status
|
|
3
|
+
from packaging.version import InvalidVersion
|
|
4
|
+
from packaging.version import Version
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _version_sort_key(version: str | None) -> tuple[int, Version | str | None]:
|
|
8
|
+
"""
|
|
9
|
+
Returns a tuple used as (reverse) ordering key for TaskGroups in
|
|
10
|
+
`get_task_group_list`.
|
|
11
|
+
The parsable versions are the first in order, sorted according to the
|
|
12
|
+
sorting rules of packaging.version.Version.
|
|
13
|
+
Next in order we have the non-null non-parsable versions, sorted
|
|
14
|
+
alphabetically.
|
|
15
|
+
"""
|
|
16
|
+
if version is None:
|
|
17
|
+
return (0, None)
|
|
18
|
+
try:
|
|
19
|
+
return (2, Version(version))
|
|
20
|
+
except InvalidVersion:
|
|
21
|
+
return (1, version)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _find_latest_version_or_422(versions: list[str]) -> str:
|
|
25
|
+
"""
|
|
26
|
+
> For PEP 440 versions, this is easy enough for the client to do (using
|
|
27
|
+
> the `packaging` library [...]. For non-standard versions, there is no
|
|
28
|
+
> well-defined ordering, and clients will need to decide on what rule is
|
|
29
|
+
> appropriate for their needs.
|
|
30
|
+
(https://peps.python.org/pep-0700/#why-not-provide-a-latest-version-value)
|
|
31
|
+
|
|
32
|
+
The `versions` array is coming from the PyPI API, and its elements are
|
|
33
|
+
assumed parsable.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
latest = max(versions, key=lambda v_str: Version(v_str))
|
|
37
|
+
return latest
|
|
38
|
+
except InvalidVersion as e:
|
|
39
|
+
raise HTTPException(
|
|
40
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
41
|
+
detail=f"Cannot find latest version (original error: {str(e)}).",
|
|
42
|
+
)
|
|
@@ -41,13 +41,16 @@ class UserRead(schemas.BaseUser[int]):
|
|
|
41
41
|
Schema for `User` read from database.
|
|
42
42
|
|
|
43
43
|
Attributes:
|
|
44
|
+
is_guest:
|
|
44
45
|
group_ids_names:
|
|
45
46
|
oauth_accounts:
|
|
46
47
|
profile_id:
|
|
47
48
|
project_dirs:
|
|
48
49
|
slurm_accounts:
|
|
50
|
+
|
|
49
51
|
"""
|
|
50
52
|
|
|
53
|
+
is_guest: bool
|
|
51
54
|
group_ids_names: list[tuple[int, str]] | None = None
|
|
52
55
|
oauth_accounts: list[OAuthAccountRead]
|
|
53
56
|
profile_id: int | None = None
|
|
@@ -65,6 +68,7 @@ class UserUpdate(schemas.BaseUserUpdate):
|
|
|
65
68
|
is_active:
|
|
66
69
|
is_superuser:
|
|
67
70
|
is_verified:
|
|
71
|
+
is_guest:
|
|
68
72
|
profile_id:
|
|
69
73
|
project_dirs:
|
|
70
74
|
slurm_accounts:
|
|
@@ -76,6 +80,7 @@ class UserUpdate(schemas.BaseUserUpdate):
|
|
|
76
80
|
is_active: bool = None
|
|
77
81
|
is_superuser: bool = None
|
|
78
82
|
is_verified: bool = None
|
|
83
|
+
is_guest: bool = None
|
|
79
84
|
profile_id: int | None = None
|
|
80
85
|
project_dirs: Annotated[
|
|
81
86
|
ListUniqueAbsolutePathStr, AfterValidator(_validate_cmd_list)
|
|
@@ -100,11 +105,13 @@ class UserCreate(schemas.BaseUserCreate):
|
|
|
100
105
|
Schema for `User` creation.
|
|
101
106
|
|
|
102
107
|
Attributes:
|
|
108
|
+
is_guest:
|
|
103
109
|
profile_id:
|
|
104
110
|
project_dirs:
|
|
105
111
|
slurm_accounts:
|
|
106
112
|
"""
|
|
107
113
|
|
|
114
|
+
is_guest: bool = False
|
|
108
115
|
profile_id: int | None = None
|
|
109
116
|
project_dirs: Annotated[
|
|
110
117
|
ListUniqueAbsolutePathStr, AfterValidator(_validate_cmd_list)
|
|
@@ -47,7 +47,6 @@ from .status_legacy import WorkflowTaskStatusType # noqa F401
|
|
|
47
47
|
from .task import TaskCreate # noqa F401
|
|
48
48
|
from .task import TaskExport # noqa F401
|
|
49
49
|
from .task import TaskImport # noqa F401
|
|
50
|
-
from .task import TaskImportLegacy # noqa F401
|
|
51
50
|
from .task import TaskRead # noqa F401
|
|
52
51
|
from .task import TaskType # noqa F401
|
|
53
52
|
from .task import TaskUpdate # noqa F401
|
|
@@ -94,7 +94,6 @@ class TaskRead(BaseModel):
|
|
|
94
94
|
id: int
|
|
95
95
|
name: str
|
|
96
96
|
type: TaskType
|
|
97
|
-
source: str | None = None
|
|
98
97
|
version: str | None = None
|
|
99
98
|
|
|
100
99
|
command_non_parallel: str | None = None
|
|
@@ -139,10 +138,6 @@ class TaskImport(BaseModel):
|
|
|
139
138
|
name: NonEmptyStr
|
|
140
139
|
|
|
141
140
|
|
|
142
|
-
class TaskImportLegacy(BaseModel):
|
|
143
|
-
source: NonEmptyStr
|
|
144
|
-
|
|
145
|
-
|
|
146
141
|
class TaskExport(BaseModel):
|
|
147
142
|
pkg_name: NonEmptyStr
|
|
148
143
|
version: NonEmptyStr | None = None
|
|
@@ -29,6 +29,7 @@ class WorkflowRead(BaseModel):
|
|
|
29
29
|
task_list: list[WorkflowTaskRead]
|
|
30
30
|
project: ProjectRead
|
|
31
31
|
timestamp_created: AwareDatetime
|
|
32
|
+
description: str | None
|
|
32
33
|
|
|
33
34
|
@field_serializer("timestamp_created")
|
|
34
35
|
def serialize_datetime(v: datetime) -> str:
|
|
@@ -44,6 +45,7 @@ class WorkflowUpdate(BaseModel):
|
|
|
44
45
|
|
|
45
46
|
name: NonEmptyStr = None
|
|
46
47
|
reordered_workflowtask_ids: ListUniqueNonNegativeInt | None = None
|
|
48
|
+
description: str | None = None
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
class WorkflowImport(BaseModel):
|
|
@@ -11,7 +11,6 @@ from fractal_server.types import WorkflowTaskArgument
|
|
|
11
11
|
|
|
12
12
|
from .task import TaskExport
|
|
13
13
|
from .task import TaskImport
|
|
14
|
-
from .task import TaskImportLegacy
|
|
15
14
|
from .task import TaskRead
|
|
16
15
|
from .task import TaskType
|
|
17
16
|
|
|
@@ -50,6 +49,9 @@ class WorkflowTaskRead(BaseModel):
|
|
|
50
49
|
task_id: int
|
|
51
50
|
task: TaskRead
|
|
52
51
|
|
|
52
|
+
alias: str | None = None
|
|
53
|
+
description: str | None = None
|
|
54
|
+
|
|
53
55
|
|
|
54
56
|
class WorkflowTaskReadWithWarning(WorkflowTaskRead):
|
|
55
57
|
warning: str | None = None
|
|
@@ -63,6 +65,8 @@ class WorkflowTaskUpdate(BaseModel):
|
|
|
63
65
|
args_non_parallel: WorkflowTaskArgument | None = None
|
|
64
66
|
args_parallel: WorkflowTaskArgument | None = None
|
|
65
67
|
type_filters: TypeFilters = None
|
|
68
|
+
description: str | None = None
|
|
69
|
+
alias: str | None = None
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
class WorkflowTaskImport(BaseModel):
|
|
@@ -75,7 +79,7 @@ class WorkflowTaskImport(BaseModel):
|
|
|
75
79
|
type_filters: TypeFilters | None = None
|
|
76
80
|
input_filters: dict[str, Any] | None = None
|
|
77
81
|
|
|
78
|
-
task: TaskImport
|
|
82
|
+
task: TaskImport
|
|
79
83
|
|
|
80
84
|
@model_validator(mode="before")
|
|
81
85
|
@classmethod
|
|
@@ -21,6 +21,7 @@ from collections.abc import AsyncGenerator
|
|
|
21
21
|
from typing import Any
|
|
22
22
|
from typing import Generic
|
|
23
23
|
from typing import Self
|
|
24
|
+
from typing import override
|
|
24
25
|
|
|
25
26
|
from fastapi import Depends
|
|
26
27
|
from fastapi import Request
|
|
@@ -188,19 +189,22 @@ class UserManager(IntegerIDMixin, BaseUserManager[UserOAuth, int]):
|
|
|
188
189
|
password_helper=password_helper,
|
|
189
190
|
)
|
|
190
191
|
|
|
192
|
+
@override
|
|
191
193
|
async def validate_password(self, password: str, user: UserOAuth) -> None:
|
|
192
194
|
# check password length
|
|
193
195
|
min_length = 4
|
|
194
|
-
max_length =
|
|
196
|
+
max_length = 72
|
|
195
197
|
if len(password) < min_length:
|
|
196
198
|
raise InvalidPasswordException(
|
|
197
199
|
f"The password is too short (minimum length: {min_length})."
|
|
198
200
|
)
|
|
199
|
-
|
|
201
|
+
if len(password.encode("utf-8")) > max_length:
|
|
200
202
|
raise InvalidPasswordException(
|
|
201
|
-
|
|
203
|
+
"The password is too long "
|
|
204
|
+
f"(maximum length: {max_length} bytes)."
|
|
202
205
|
)
|
|
203
206
|
|
|
207
|
+
@override
|
|
204
208
|
async def oauth_callback(
|
|
205
209
|
self: Self,
|
|
206
210
|
oauth_name: str,
|
|
@@ -324,6 +328,7 @@ class UserManager(IntegerIDMixin, BaseUserManager[UserOAuth, int]):
|
|
|
324
328
|
|
|
325
329
|
return user
|
|
326
330
|
|
|
331
|
+
@override
|
|
327
332
|
async def on_after_register(
|
|
328
333
|
self, user: UserOAuth, request: Request | None = None
|
|
329
334
|
):
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""drop dataset.history
|
|
2
|
+
|
|
3
|
+
Revision ID: 18a26fcdea5d
|
|
4
|
+
Revises: 1bf8785755f9
|
|
5
|
+
Create Date: 2026-01-29 10:15:18.467384
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
from sqlalchemy.dialects import postgresql
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision = "18a26fcdea5d"
|
|
15
|
+
down_revision = "1bf8785755f9"
|
|
16
|
+
branch_labels = None
|
|
17
|
+
depends_on = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade() -> None:
|
|
21
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
22
|
+
with op.batch_alter_table("datasetv2", schema=None) as batch_op:
|
|
23
|
+
batch_op.drop_column("history")
|
|
24
|
+
|
|
25
|
+
# ### end Alembic commands ###
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def downgrade() -> None:
|
|
29
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
30
|
+
with op.batch_alter_table("datasetv2", schema=None) as batch_op:
|
|
31
|
+
batch_op.add_column(
|
|
32
|
+
sa.Column(
|
|
33
|
+
"history",
|
|
34
|
+
postgresql.JSONB(astext_type=sa.Text()),
|
|
35
|
+
server_default=sa.text("'[]'::json"),
|
|
36
|
+
autoincrement=False,
|
|
37
|
+
nullable=False,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# ### end Alembic commands ###
|