fractal-server 2.18.6__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/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/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 -5
- fractal_server/app/routes/api/v2/job.py +10 -9
- fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
- fractal_server/app/routes/api/v2/project.py +7 -6
- 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 -6
- 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 +9 -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 -9
- fractal_server/app/routes/api/v2/workflow_import.py +14 -45
- fractal_server/app/routes/api/v2/workflowtask.py +7 -11
- fractal_server/app/routes/auth/__init__.py +18 -1
- fractal_server/app/routes/auth/current_user.py +8 -0
- 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/v2/submit_workflow.py +0 -2
- {fractal_server-2.18.6.dist-info → fractal_server-2.19.0.dist-info}/METADATA +2 -2
- {fractal_server-2.18.6.dist-info → fractal_server-2.19.0.dist-info}/RECORD +56 -49
- fractal_server/app/routes/api/v2/status_legacy.py +0 -156
- {fractal_server-2.18.6.dist-info → fractal_server-2.19.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.18.6.dist-info → fractal_server-2.19.0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.18.6.dist-info → fractal_server-2.19.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
"""
|
|
@@ -99,7 +100,7 @@ async def create_workflow(
|
|
|
99
100
|
async def read_workflow(
|
|
100
101
|
project_id: int,
|
|
101
102
|
workflow_id: int,
|
|
102
|
-
user: UserOAuth = Depends(
|
|
103
|
+
user: UserOAuth = Depends(get_api_guest),
|
|
103
104
|
db: AsyncSession = Depends(get_async_db),
|
|
104
105
|
) -> WorkflowReadWithWarnings | None:
|
|
105
106
|
"""
|
|
@@ -134,7 +135,7 @@ async def update_workflow(
|
|
|
134
135
|
project_id: int,
|
|
135
136
|
workflow_id: int,
|
|
136
137
|
patch: WorkflowUpdate,
|
|
137
|
-
user: UserOAuth = Depends(
|
|
138
|
+
user: UserOAuth = Depends(get_api_user),
|
|
138
139
|
db: AsyncSession = Depends(get_async_db),
|
|
139
140
|
) -> WorkflowReadWithWarnings | None:
|
|
140
141
|
"""
|
|
@@ -148,7 +149,7 @@ async def update_workflow(
|
|
|
148
149
|
db=db,
|
|
149
150
|
)
|
|
150
151
|
|
|
151
|
-
if patch.name:
|
|
152
|
+
if patch.name and patch.name != workflow.name:
|
|
152
153
|
await _check_workflow_exists(
|
|
153
154
|
name=patch.name, project_id=project_id, db=db
|
|
154
155
|
)
|
|
@@ -208,7 +209,7 @@ async def update_workflow(
|
|
|
208
209
|
async def delete_workflow(
|
|
209
210
|
project_id: int,
|
|
210
211
|
workflow_id: int,
|
|
211
|
-
user: UserOAuth = Depends(
|
|
212
|
+
user: UserOAuth = Depends(get_api_user),
|
|
212
213
|
db: AsyncSession = Depends(get_async_db),
|
|
213
214
|
) -> Response:
|
|
214
215
|
"""
|
|
@@ -254,7 +255,7 @@ async def delete_workflow(
|
|
|
254
255
|
async def export_workflow(
|
|
255
256
|
project_id: int,
|
|
256
257
|
workflow_id: int,
|
|
257
|
-
user: UserOAuth = Depends(
|
|
258
|
+
user: UserOAuth = Depends(get_api_guest),
|
|
258
259
|
db: AsyncSession = Depends(get_async_db),
|
|
259
260
|
) -> WorkflowExport | None:
|
|
260
261
|
"""
|
|
@@ -295,7 +296,7 @@ class WorkflowTaskTypeFiltersInfo(BaseModel):
|
|
|
295
296
|
async def get_workflow_type_filters(
|
|
296
297
|
project_id: int,
|
|
297
298
|
workflow_id: int,
|
|
298
|
-
user: UserOAuth = Depends(
|
|
299
|
+
user: UserOAuth = Depends(get_api_guest),
|
|
299
300
|
db: AsyncSession = Depends(get_async_db),
|
|
300
301
|
) -> list[WorkflowTaskTypeFiltersInfo]:
|
|
301
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,13 +193,8 @@ 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)
|
|
@@ -214,7 +210,7 @@ async def delete_workflowtask(
|
|
|
214
210
|
project_id: int,
|
|
215
211
|
workflow_id: int,
|
|
216
212
|
workflow_task_id: int,
|
|
217
|
-
user: UserOAuth = Depends(
|
|
213
|
+
user: UserOAuth = Depends(get_api_user),
|
|
218
214
|
db: AsyncSession = Depends(get_async_db),
|
|
219
215
|
) -> Response:
|
|
220
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`
|
|
@@ -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 ###
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Add description to workflow and description and alias to workflow task
|
|
2
|
+
|
|
3
|
+
Revision ID: 1bf8785755f9
|
|
4
|
+
Revises: 5fb08bf05b14
|
|
5
|
+
Create Date: 2026-01-26 09:03:18.396841
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
import sqlmodel
|
|
11
|
+
from alembic import op
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision = "1bf8785755f9"
|
|
15
|
+
down_revision = "5fb08bf05b14"
|
|
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("workflowtaskv2", schema=None) as batch_op:
|
|
23
|
+
batch_op.add_column(
|
|
24
|
+
sa.Column(
|
|
25
|
+
"alias", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
batch_op.add_column(
|
|
29
|
+
sa.Column(
|
|
30
|
+
"description", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
with op.batch_alter_table("workflowv2", schema=None) as batch_op:
|
|
35
|
+
batch_op.add_column(
|
|
36
|
+
sa.Column(
|
|
37
|
+
"description", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# ### end Alembic commands ###
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def downgrade() -> None:
|
|
45
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
46
|
+
with op.batch_alter_table("workflowv2", schema=None) as batch_op:
|
|
47
|
+
batch_op.drop_column("description")
|
|
48
|
+
|
|
49
|
+
with op.batch_alter_table("workflowtaskv2", schema=None) as batch_op:
|
|
50
|
+
batch_op.drop_column("description")
|
|
51
|
+
batch_op.drop_column("alias")
|
|
52
|
+
|
|
53
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""drop TaskV2.source
|
|
2
|
+
|
|
3
|
+
Revision ID: 5fb08bf05b14
|
|
4
|
+
Revises: e53dc51fdf93
|
|
5
|
+
Create Date: 2026-01-21 12:50:39.072816
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
from alembic import op
|
|
11
|
+
|
|
12
|
+
# revision identifiers, used by Alembic.
|
|
13
|
+
revision = "5fb08bf05b14"
|
|
14
|
+
down_revision = "e53dc51fdf93"
|
|
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("taskv2", schema=None) as batch_op:
|
|
22
|
+
batch_op.drop_column("source")
|
|
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("taskv2", schema=None) as batch_op:
|
|
30
|
+
batch_op.add_column(
|
|
31
|
+
sa.Column(
|
|
32
|
+
"source", sa.VARCHAR(), autoincrement=False, nullable=True
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# ### end Alembic commands ###
|