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.
Files changed (57) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/security.py +16 -0
  3. fractal_server/app/models/v2/dataset.py +0 -4
  4. fractal_server/app/models/v2/job.py +4 -0
  5. fractal_server/app/models/v2/task.py +0 -1
  6. fractal_server/app/models/v2/task_group.py +4 -0
  7. fractal_server/app/models/v2/workflow.py +2 -0
  8. fractal_server/app/models/v2/workflowtask.py +3 -0
  9. fractal_server/app/routes/admin/v2/sharing.py +47 -0
  10. fractal_server/app/routes/admin/v2/task.py +0 -5
  11. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +6 -0
  12. fractal_server/app/routes/api/__init__.py +4 -52
  13. fractal_server/app/routes/api/alive.py +13 -0
  14. fractal_server/app/routes/api/settings.py +44 -0
  15. fractal_server/app/routes/api/v2/__init__.py +0 -2
  16. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +1 -20
  17. fractal_server/app/routes/api/v2/dataset.py +9 -8
  18. fractal_server/app/routes/api/v2/history.py +8 -8
  19. fractal_server/app/routes/api/v2/images.py +6 -5
  20. fractal_server/app/routes/api/v2/job.py +10 -9
  21. fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
  22. fractal_server/app/routes/api/v2/project.py +7 -6
  23. fractal_server/app/routes/api/v2/sharing.py +17 -9
  24. fractal_server/app/routes/api/v2/submit.py +5 -3
  25. fractal_server/app/routes/api/v2/task.py +7 -6
  26. fractal_server/app/routes/api/v2/task_collection.py +4 -2
  27. fractal_server/app/routes/api/v2/task_collection_custom.py +2 -2
  28. fractal_server/app/routes/api/v2/task_collection_pixi.py +4 -2
  29. fractal_server/app/routes/api/v2/task_group.py +9 -30
  30. fractal_server/app/routes/api/v2/task_group_lifecycle.py +10 -4
  31. fractal_server/app/routes/api/v2/task_version_update.py +4 -3
  32. fractal_server/app/routes/api/v2/workflow.py +10 -9
  33. fractal_server/app/routes/api/v2/workflow_import.py +14 -45
  34. fractal_server/app/routes/api/v2/workflowtask.py +7 -11
  35. fractal_server/app/routes/auth/__init__.py +18 -1
  36. fractal_server/app/routes/auth/current_user.py +8 -0
  37. fractal_server/app/routes/auth/users.py +11 -0
  38. fractal_server/app/routes/aux/_versions.py +42 -0
  39. fractal_server/app/schemas/user.py +7 -0
  40. fractal_server/app/schemas/v2/__init__.py +0 -1
  41. fractal_server/app/schemas/v2/dumps.py +0 -1
  42. fractal_server/app/schemas/v2/task.py +0 -5
  43. fractal_server/app/schemas/v2/workflow.py +2 -0
  44. fractal_server/app/schemas/v2/workflowtask.py +6 -2
  45. fractal_server/app/security/__init__.py +8 -3
  46. fractal_server/migrations/versions/18a26fcdea5d_drop_dataset_history.py +41 -0
  47. fractal_server/migrations/versions/1bf8785755f9_add_description_to_workflow_and_.py +53 -0
  48. fractal_server/migrations/versions/5fb08bf05b14_drop_taskv2_source.py +36 -0
  49. fractal_server/migrations/versions/cfd13f7954e7_add_fractal_server_version_to_jobv2_and_.py +52 -0
  50. fractal_server/migrations/versions/e53dc51fdf93_add_useroauth_is_guest.py +36 -0
  51. fractal_server/runner/v2/submit_workflow.py +0 -2
  52. {fractal_server-2.18.6.dist-info → fractal_server-2.19.0.dist-info}/METADATA +2 -2
  53. {fractal_server-2.18.6.dist-info → fractal_server-2.19.0.dist-info}/RECORD +56 -49
  54. fractal_server/app/routes/api/v2/status_legacy.py +0 -156
  55. {fractal_server-2.18.6.dist-info → fractal_server-2.19.0.dist-info}/WHEEL +0 -0
  56. {fractal_server-2.18.6.dist-info → fractal_server-2.19.0.dist-info}/entry_points.txt +0 -0
  57. {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 current_user_act_ver_prof
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(current_user_act_ver_prof),
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(current_user_act_ver_prof),
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(current_user_act_ver_prof),
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(current_user_act_ver_prof),
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(current_user_act_ver_prof),
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(current_user_act_ver_prof),
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(current_user_act_ver_prof),
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 current_user_act_ver_prof
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
- latest_task = max(matching_task_groups, key=lambda tg: tg.version or "")
151
- version = latest_task.version
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(current_user_act_ver_prof),
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
- if isinstance(task_import, TaskImportLegacy):
250
- task_id = await _get_task_by_source(
251
- source=task_import.source,
252
- task_groups_list=task_group_list,
253
- )
254
- else:
255
- task_id = await _get_task_by_taskimport(
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 current_user_act_ver_prof
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(current_user_act_ver_prof),
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(current_user_act_ver_prof),
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(current_user_act_ver_prof),
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
- raise HTTPException(
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(current_user_act_ver_prof),
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 current_user_act_ver_prof(
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
@@ -30,7 +30,6 @@ class TaskDump(BaseModel):
30
30
 
31
31
  command_non_parallel: str | None = None
32
32
  command_parallel: str | None = None
33
- source: str | None = None
34
33
  version: str | None = None
35
34
 
36
35
  input_types: dict[str, bool]
@@ -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 | TaskImportLegacy
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 = 100
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
- elif len(password) > max_length:
201
+ if len(password.encode("utf-8")) > max_length:
200
202
  raise InvalidPasswordException(
201
- f"The password is too long (maximum length: {min_length})."
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 ###