fractal-server 2.7.0a0__py3-none-any.whl → 2.7.0a2__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/__main__.py +10 -4
- fractal_server/app/models/linkusergroup.py +11 -0
- fractal_server/app/models/v2/task.py +19 -5
- fractal_server/app/routes/admin/v2/__init__.py +16 -0
- fractal_server/app/routes/admin/{v2.py → v2/job.py} +20 -191
- fractal_server/app/routes/admin/v2/project.py +43 -0
- fractal_server/app/routes/admin/v2/task.py +146 -0
- fractal_server/app/routes/admin/v2/task_group.py +134 -0
- fractal_server/app/routes/api/v2/task.py +13 -0
- fractal_server/app/routes/api/v2/task_collection_custom.py +7 -1
- fractal_server/app/routes/api/v2/task_group.py +11 -3
- fractal_server/app/routes/auth/_aux_auth.py +30 -29
- fractal_server/app/routes/auth/current_user.py +5 -5
- fractal_server/app/routes/auth/router.py +0 -2
- fractal_server/app/routes/auth/users.py +8 -7
- fractal_server/app/schemas/user.py +1 -2
- fractal_server/app/schemas/v2/manifest.py +12 -1
- fractal_server/app/schemas/v2/task.py +73 -25
- fractal_server/app/schemas/v2/task_group.py +28 -1
- fractal_server/data_migrations/2_7_0.py +274 -0
- fractal_server/migrations/versions/742b74e1cc6e_revamp_taskv2_and_taskgroupv2.py +101 -0
- fractal_server/migrations/versions/df7cc3501bf7_linkusergroup_timestamp_created.py +42 -0
- fractal_server/tasks/v2/background_operations.py +12 -1
- fractal_server/tasks/v2/background_operations_ssh.py +11 -1
- fractal_server/tasks/v2/endpoint_operations.py +42 -0
- {fractal_server-2.7.0a0.dist-info → fractal_server-2.7.0a2.dist-info}/METADATA +1 -1
- {fractal_server-2.7.0a0.dist-info → fractal_server-2.7.0a2.dist-info}/RECORD +31 -25
- fractal_server/app/routes/auth/group_names.py +0 -34
- {fractal_server-2.7.0a0.dist-info → fractal_server-2.7.0a2.dist-info}/LICENSE +0 -0
- {fractal_server-2.7.0a0.dist-info → fractal_server-2.7.0a2.dist-info}/WHEEL +0 -0
- {fractal_server-2.7.0a0.dist-info → fractal_server-2.7.0a2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,134 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from fastapi import APIRouter
|
4
|
+
from fastapi import Depends
|
5
|
+
from fastapi import HTTPException
|
6
|
+
from fastapi import Response
|
7
|
+
from fastapi import status
|
8
|
+
from sqlalchemy.sql.operators import is_
|
9
|
+
from sqlalchemy.sql.operators import is_not
|
10
|
+
from sqlmodel import select
|
11
|
+
|
12
|
+
from fractal_server.app.db import AsyncSession
|
13
|
+
from fractal_server.app.db import get_async_db
|
14
|
+
from fractal_server.app.models import UserOAuth
|
15
|
+
from fractal_server.app.models.v2 import TaskGroupV2
|
16
|
+
from fractal_server.app.models.v2 import WorkflowTaskV2
|
17
|
+
from fractal_server.app.routes.auth import current_active_superuser
|
18
|
+
from fractal_server.app.routes.auth._aux_auth import (
|
19
|
+
_verify_user_belongs_to_group,
|
20
|
+
)
|
21
|
+
from fractal_server.app.schemas.v2 import TaskGroupReadV2
|
22
|
+
from fractal_server.app.schemas.v2 import TaskGroupUpdateV2
|
23
|
+
|
24
|
+
router = APIRouter()
|
25
|
+
|
26
|
+
|
27
|
+
@router.get("/{task_group_id}/", response_model=TaskGroupReadV2)
|
28
|
+
async def query_task_group(
|
29
|
+
task_group_id: int,
|
30
|
+
user: UserOAuth = Depends(current_active_superuser),
|
31
|
+
db: AsyncSession = Depends(get_async_db),
|
32
|
+
) -> TaskGroupReadV2:
|
33
|
+
|
34
|
+
task_group = await db.get(TaskGroupV2, task_group_id)
|
35
|
+
if task_group is None:
|
36
|
+
raise HTTPException(
|
37
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
38
|
+
detail=f"TaskGroup {task_group_id} not found",
|
39
|
+
)
|
40
|
+
return task_group
|
41
|
+
|
42
|
+
|
43
|
+
@router.get("/", response_model=list[TaskGroupReadV2])
|
44
|
+
async def query_task_group_list(
|
45
|
+
user_id: Optional[int] = None,
|
46
|
+
user_group_id: Optional[int] = None,
|
47
|
+
private: Optional[bool] = None,
|
48
|
+
active: Optional[bool] = None,
|
49
|
+
user: UserOAuth = Depends(current_active_superuser),
|
50
|
+
db: AsyncSession = Depends(get_async_db),
|
51
|
+
) -> list[TaskGroupReadV2]:
|
52
|
+
|
53
|
+
stm = select(TaskGroupV2)
|
54
|
+
|
55
|
+
if user_group_id is not None and private is True:
|
56
|
+
raise HTTPException(
|
57
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
58
|
+
detail=f"Cannot set `user_group_id` with {private=}",
|
59
|
+
)
|
60
|
+
if user_id is not None:
|
61
|
+
stm = stm.where(TaskGroupV2.user_id == user_id)
|
62
|
+
if user_group_id is not None:
|
63
|
+
stm = stm.where(TaskGroupV2.user_group_id == user_group_id)
|
64
|
+
if private is not None:
|
65
|
+
if private is True:
|
66
|
+
stm = stm.where(is_(TaskGroupV2.user_group_id, None))
|
67
|
+
else:
|
68
|
+
stm = stm.where(is_not(TaskGroupV2.user_group_id, None))
|
69
|
+
if active is not None:
|
70
|
+
if active is True:
|
71
|
+
stm = stm.where(is_(TaskGroupV2.active, True))
|
72
|
+
else:
|
73
|
+
stm = stm.where(is_(TaskGroupV2.active, False))
|
74
|
+
|
75
|
+
res = await db.execute(stm)
|
76
|
+
task_groups_list = res.scalars().all()
|
77
|
+
return task_groups_list
|
78
|
+
|
79
|
+
|
80
|
+
@router.patch("/{task_group_id}/", response_model=TaskGroupReadV2)
|
81
|
+
async def patch_task_group(
|
82
|
+
task_group_id: int,
|
83
|
+
task_group_update: TaskGroupUpdateV2,
|
84
|
+
user: UserOAuth = Depends(current_active_superuser),
|
85
|
+
db: AsyncSession = Depends(get_async_db),
|
86
|
+
) -> list[TaskGroupReadV2]:
|
87
|
+
task_group = await db.get(TaskGroupV2, task_group_id)
|
88
|
+
if task_group is None:
|
89
|
+
raise HTTPException(
|
90
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
91
|
+
detail=f"TaskGroupV2 {task_group_id} not found",
|
92
|
+
)
|
93
|
+
|
94
|
+
for key, value in task_group_update.dict(exclude_unset=True).items():
|
95
|
+
if (key == "user_group_id") and (value is not None):
|
96
|
+
await _verify_user_belongs_to_group(
|
97
|
+
user_id=user.id, user_group_id=value, db=db
|
98
|
+
)
|
99
|
+
setattr(task_group, key, value)
|
100
|
+
|
101
|
+
db.add(task_group)
|
102
|
+
await db.commit()
|
103
|
+
await db.refresh(task_group)
|
104
|
+
return task_group
|
105
|
+
|
106
|
+
|
107
|
+
@router.delete("/{task_group_id}/", status_code=204)
|
108
|
+
async def delete_task_group(
|
109
|
+
task_group_id: int,
|
110
|
+
user: UserOAuth = Depends(current_active_superuser),
|
111
|
+
db: AsyncSession = Depends(get_async_db),
|
112
|
+
):
|
113
|
+
task_group = await db.get(TaskGroupV2, task_group_id)
|
114
|
+
if task_group is None:
|
115
|
+
raise HTTPException(
|
116
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
117
|
+
detail=f"TaskGroupV2 {task_group_id} not found",
|
118
|
+
)
|
119
|
+
|
120
|
+
stm = select(WorkflowTaskV2).where(
|
121
|
+
WorkflowTaskV2.task_id.in_({task.id for task in task_group.task_list})
|
122
|
+
)
|
123
|
+
res = await db.execute(stm)
|
124
|
+
workflow_tasks = res.scalars().all()
|
125
|
+
if workflow_tasks != []:
|
126
|
+
raise HTTPException(
|
127
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
128
|
+
detail=f"TaskV2 {workflow_tasks[0].task_id} is still in use",
|
129
|
+
)
|
130
|
+
|
131
|
+
await db.delete(task_group)
|
132
|
+
await db.commit()
|
133
|
+
|
134
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
@@ -6,6 +6,7 @@ from fastapi import Depends
|
|
6
6
|
from fastapi import HTTPException
|
7
7
|
from fastapi import Response
|
8
8
|
from fastapi import status
|
9
|
+
from sqlmodel import func
|
9
10
|
from sqlmodel import or_
|
10
11
|
from sqlmodel import select
|
11
12
|
|
@@ -36,6 +37,9 @@ logger = set_logger(__name__)
|
|
36
37
|
async def get_list_task(
|
37
38
|
args_schema_parallel: bool = True,
|
38
39
|
args_schema_non_parallel: bool = True,
|
40
|
+
category: Optional[str] = None,
|
41
|
+
modality: Optional[str] = None,
|
42
|
+
author: Optional[str] = None,
|
39
43
|
user: UserOAuth = Depends(current_active_user),
|
40
44
|
db: AsyncSession = Depends(get_async_db),
|
41
45
|
) -> list[TaskReadV2]:
|
@@ -57,6 +61,13 @@ async def get_list_task(
|
|
57
61
|
)
|
58
62
|
)
|
59
63
|
)
|
64
|
+
if category is not None:
|
65
|
+
stm = stm.where(func.lower(TaskV2.category) == category.lower())
|
66
|
+
if modality is not None:
|
67
|
+
stm = stm.where(func.lower(TaskV2.modality) == modality.lower())
|
68
|
+
if author is not None:
|
69
|
+
stm = stm.where(TaskV2.authors.icontains(author))
|
70
|
+
|
60
71
|
res = await db.execute(stm)
|
61
72
|
task_list = res.scalars().all()
|
62
73
|
await db.close()
|
@@ -216,6 +227,8 @@ async def create_task(
|
|
216
227
|
user_group_id=user_group_id,
|
217
228
|
active=True,
|
218
229
|
task_list=[db_task],
|
230
|
+
origin="other",
|
231
|
+
pkg_name=task.name,
|
219
232
|
)
|
220
233
|
db.add(db_task_group)
|
221
234
|
await db.commit()
|
@@ -186,9 +186,15 @@ async def collect_task_custom(
|
|
186
186
|
detail="\n".join(overlapping_tasks_v1_source_and_id),
|
187
187
|
)
|
188
188
|
|
189
|
+
# Prepare task-group attributes
|
190
|
+
task_group_attrs = dict(
|
191
|
+
origin="other",
|
192
|
+
pkg_name=task_collect.source, # FIXME
|
193
|
+
)
|
194
|
+
|
189
195
|
task_group = create_db_task_group_and_tasks(
|
190
196
|
task_list=task_list,
|
191
|
-
task_group_obj=TaskGroupCreateV2(),
|
197
|
+
task_group_obj=TaskGroupCreateV2(**task_group_attrs),
|
192
198
|
user_id=user.id,
|
193
199
|
user_group_id=user_group_id,
|
194
200
|
db=db_sync,
|
@@ -31,12 +31,17 @@ logger = set_logger(__name__)
|
|
31
31
|
async def get_task_group_list(
|
32
32
|
user: UserOAuth = Depends(current_active_user),
|
33
33
|
db: AsyncSession = Depends(get_async_db),
|
34
|
+
only_active: bool = False,
|
35
|
+
only_owner: bool = False,
|
34
36
|
) -> list[TaskGroupReadV2]:
|
35
37
|
"""
|
36
38
|
Get all accessible TaskGroups
|
37
39
|
"""
|
38
|
-
|
39
|
-
|
40
|
+
|
41
|
+
if only_owner:
|
42
|
+
condition = TaskGroupV2.user_id == user.id
|
43
|
+
else:
|
44
|
+
condition = or_(
|
40
45
|
TaskGroupV2.user_id == user.id,
|
41
46
|
TaskGroupV2.user_group_id.in_(
|
42
47
|
select(LinkUserGroup.group_id).where(
|
@@ -44,7 +49,10 @@ async def get_task_group_list(
|
|
44
49
|
)
|
45
50
|
),
|
46
51
|
)
|
47
|
-
)
|
52
|
+
stm = select(TaskGroupV2).where(condition)
|
53
|
+
if only_active:
|
54
|
+
stm = stm.where(TaskGroupV2.active)
|
55
|
+
|
48
56
|
res = await db.execute(stm)
|
49
57
|
task_groups = res.scalars().all()
|
50
58
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from fastapi import HTTPException
|
2
2
|
from fastapi import status
|
3
3
|
from sqlalchemy.ext.asyncio import AsyncSession
|
4
|
+
from sqlmodel import asc
|
4
5
|
from sqlmodel import select
|
5
6
|
|
6
7
|
from fractal_server.app.models.linkusergroup import LinkUserGroup
|
@@ -9,58 +10,58 @@ from fractal_server.app.models.security import UserOAuth
|
|
9
10
|
from fractal_server.app.schemas.user import UserRead
|
10
11
|
from fractal_server.app.schemas.user_group import UserGroupRead
|
11
12
|
from fractal_server.app.security import FRACTAL_DEFAULT_GROUP_NAME
|
13
|
+
from fractal_server.logger import set_logger
|
12
14
|
|
15
|
+
logger = set_logger(__name__)
|
13
16
|
|
14
|
-
|
17
|
+
|
18
|
+
async def _get_single_user_with_groups(
|
15
19
|
user: UserOAuth,
|
16
20
|
db: AsyncSession,
|
17
21
|
) -> UserRead:
|
18
22
|
"""
|
19
|
-
Enrich a user object by filling its `
|
23
|
+
Enrich a user object by filling its `group_ids_names` attribute.
|
20
24
|
|
21
25
|
Arguments:
|
22
26
|
user: The current `UserOAuth` object
|
23
27
|
db: Async db session
|
24
28
|
|
25
29
|
Returns:
|
26
|
-
A `UserRead` object with `
|
30
|
+
A `UserRead` object with `group_ids_names` dict
|
27
31
|
"""
|
28
32
|
stm_groups = (
|
29
33
|
select(UserGroup)
|
30
34
|
.join(LinkUserGroup)
|
31
|
-
.where(LinkUserGroup.user_id ==
|
35
|
+
.where(LinkUserGroup.user_id == user.id)
|
36
|
+
.order_by(asc(LinkUserGroup.timestamp_created))
|
32
37
|
)
|
33
38
|
res = await db.execute(stm_groups)
|
34
39
|
groups = res.scalars().unique().all()
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
+
group_ids_names = [(group.id, group.name) for group in groups]
|
41
|
+
|
42
|
+
# Check that Fractal Default Group is the first of the list. If not, fix.
|
43
|
+
index = next(
|
44
|
+
(
|
45
|
+
i
|
46
|
+
for i, group_tuple in enumerate(group_ids_names)
|
47
|
+
if group_tuple[1] == FRACTAL_DEFAULT_GROUP_NAME
|
48
|
+
),
|
49
|
+
None,
|
40
50
|
)
|
51
|
+
if index is None:
|
52
|
+
logger.warning(
|
53
|
+
f"User {user.id} not in "
|
54
|
+
f"default UserGroup '{FRACTAL_DEFAULT_GROUP_NAME}'"
|
55
|
+
)
|
56
|
+
elif index != 0:
|
57
|
+
default_group = group_ids_names.pop(index)
|
58
|
+
group_ids_names.insert(0, default_group)
|
59
|
+
else:
|
60
|
+
pass
|
41
61
|
|
42
|
-
|
43
|
-
async def _get_single_user_with_group_ids(
|
44
|
-
user: UserOAuth,
|
45
|
-
db: AsyncSession,
|
46
|
-
) -> UserRead:
|
47
|
-
"""
|
48
|
-
Enrich a user object by filling its `group_ids` attribute.
|
49
|
-
|
50
|
-
Arguments:
|
51
|
-
user: The current `UserOAuth` object
|
52
|
-
db: Async db session
|
53
|
-
|
54
|
-
Returns:
|
55
|
-
A `UserRead` object with `group_ids` set
|
56
|
-
"""
|
57
|
-
stm_links = select(LinkUserGroup).where(LinkUserGroup.user_id == user.id)
|
58
|
-
res = await db.execute(stm_links)
|
59
|
-
links = res.scalars().unique().all()
|
60
|
-
group_ids = [link.group_id for link in links]
|
61
62
|
return UserRead(
|
62
63
|
**user.model_dump(),
|
63
|
-
|
64
|
+
group_ids_names=group_ids_names,
|
64
65
|
oauth_accounts=user.oauth_accounts,
|
65
66
|
)
|
66
67
|
|
@@ -13,7 +13,7 @@ from ...schemas.user import UserRead
|
|
13
13
|
from ...schemas.user import UserUpdate
|
14
14
|
from ...schemas.user import UserUpdateStrict
|
15
15
|
from ..aux.validate_user_settings import verify_user_has_settings
|
16
|
-
from ._aux_auth import
|
16
|
+
from ._aux_auth import _get_single_user_with_groups
|
17
17
|
from fractal_server.app.models import LinkUserGroup
|
18
18
|
from fractal_server.app.models import UserGroup
|
19
19
|
from fractal_server.app.models import UserOAuth
|
@@ -28,15 +28,15 @@ router_current_user = APIRouter()
|
|
28
28
|
|
29
29
|
@router_current_user.get("/current-user/", response_model=UserRead)
|
30
30
|
async def get_current_user(
|
31
|
-
|
31
|
+
group_ids_names: bool = False,
|
32
32
|
user: UserOAuth = Depends(current_active_user),
|
33
33
|
db: AsyncSession = Depends(get_async_db),
|
34
34
|
):
|
35
35
|
"""
|
36
36
|
Return current user
|
37
37
|
"""
|
38
|
-
if
|
39
|
-
user_with_groups = await
|
38
|
+
if group_ids_names is True:
|
39
|
+
user_with_groups = await _get_single_user_with_groups(user, db)
|
40
40
|
return user_with_groups
|
41
41
|
else:
|
42
42
|
return user
|
@@ -65,7 +65,7 @@ async def patch_current_user(
|
|
65
65
|
patched_user = await db.get(
|
66
66
|
UserOAuth, validated_user.id, populate_existing=True
|
67
67
|
)
|
68
|
-
patched_user_with_groups = await
|
68
|
+
patched_user_with_groups = await _get_single_user_with_groups(
|
69
69
|
patched_user, db
|
70
70
|
)
|
71
71
|
return patched_user_with_groups
|
@@ -2,7 +2,6 @@ from fastapi import APIRouter
|
|
2
2
|
|
3
3
|
from .current_user import router_current_user
|
4
4
|
from .group import router_group
|
5
|
-
from .group_names import router_group_names
|
6
5
|
from .login import router_login
|
7
6
|
from .oauth import router_oauth
|
8
7
|
from .register import router_register
|
@@ -13,7 +12,6 @@ router_auth = APIRouter()
|
|
13
12
|
router_auth.include_router(router_register)
|
14
13
|
router_auth.include_router(router_current_user)
|
15
14
|
router_auth.include_router(router_login)
|
16
|
-
router_auth.include_router(router_group_names)
|
17
15
|
router_auth.include_router(router_users)
|
18
16
|
router_auth.include_router(router_group)
|
19
17
|
router_auth.include_router(router_oauth)
|
@@ -20,7 +20,7 @@ from ...schemas.user import UserRead
|
|
20
20
|
from ...schemas.user import UserUpdate
|
21
21
|
from ...schemas.user import UserUpdateWithNewGroupIds
|
22
22
|
from ..aux.validate_user_settings import verify_user_has_settings
|
23
|
-
from ._aux_auth import
|
23
|
+
from ._aux_auth import _get_single_user_with_groups
|
24
24
|
from fractal_server.app.models import LinkUserGroup
|
25
25
|
from fractal_server.app.models import UserGroup
|
26
26
|
from fractal_server.app.models import UserOAuth
|
@@ -41,13 +41,14 @@ logger = set_logger(__name__)
|
|
41
41
|
@router_users.get("/users/{user_id}/", response_model=UserRead)
|
42
42
|
async def get_user(
|
43
43
|
user_id: int,
|
44
|
-
|
44
|
+
group_ids_names: bool = True,
|
45
45
|
superuser: UserOAuth = Depends(current_active_superuser),
|
46
46
|
db: AsyncSession = Depends(get_async_db),
|
47
47
|
) -> UserRead:
|
48
48
|
user = await _user_or_404(user_id, db)
|
49
|
-
if
|
50
|
-
|
49
|
+
if group_ids_names:
|
50
|
+
user_with_groups = await _get_single_user_with_groups(user, db)
|
51
|
+
return user_with_groups
|
51
52
|
return user
|
52
53
|
|
53
54
|
|
@@ -163,12 +164,12 @@ async def patch_user(
|
|
163
164
|
# Nothing to do, just continue
|
164
165
|
patched_user = user_to_patch
|
165
166
|
|
166
|
-
# Enrich user object with `
|
167
|
-
|
167
|
+
# Enrich user object with `group_ids_names` attribute
|
168
|
+
patched_user_with_groups = await _get_single_user_with_groups(
|
168
169
|
patched_user, db
|
169
170
|
)
|
170
171
|
|
171
|
-
return
|
172
|
+
return patched_user_with_groups
|
172
173
|
|
173
174
|
|
174
175
|
@router_users.get("/users/", response_model=list[UserRead])
|
@@ -41,8 +41,7 @@ class UserRead(schemas.BaseUser[int]):
|
|
41
41
|
"""
|
42
42
|
|
43
43
|
username: Optional[str]
|
44
|
-
|
45
|
-
group_ids: Optional[list[int]] = None
|
44
|
+
group_ids_names: Optional[list[tuple[int, str]]] = None
|
46
45
|
oauth_accounts: list[OAuthAccountRead]
|
47
46
|
|
48
47
|
|
@@ -7,6 +7,8 @@ from pydantic import HttpUrl
|
|
7
7
|
from pydantic import root_validator
|
8
8
|
from pydantic import validator
|
9
9
|
|
10
|
+
from .._validators import valstr
|
11
|
+
|
10
12
|
|
11
13
|
class TaskManifestV2(BaseModel):
|
12
14
|
"""
|
@@ -50,6 +52,10 @@ class TaskManifestV2(BaseModel):
|
|
50
52
|
docs_info: Optional[str] = None
|
51
53
|
docs_link: Optional[HttpUrl] = None
|
52
54
|
|
55
|
+
category: Optional[str] = None
|
56
|
+
modality: Optional[str] = None
|
57
|
+
tags: list[str] = Field(default_factory=list)
|
58
|
+
|
53
59
|
@root_validator
|
54
60
|
def validate_executable_args_meta(cls, values):
|
55
61
|
|
@@ -128,7 +134,8 @@ class ManifestV2(BaseModel):
|
|
128
134
|
manifest_version: str
|
129
135
|
task_list: list[TaskManifestV2]
|
130
136
|
has_args_schemas: bool = False
|
131
|
-
args_schema_version: Optional[str]
|
137
|
+
args_schema_version: Optional[str] = None
|
138
|
+
authors: Optional[str] = None
|
132
139
|
|
133
140
|
@root_validator()
|
134
141
|
def _check_args_schemas_are_present(cls, values):
|
@@ -157,3 +164,7 @@ class ManifestV2(BaseModel):
|
|
157
164
|
if value != "2":
|
158
165
|
raise ValueError(f"Wrong manifest version (given {value})")
|
159
166
|
return value
|
167
|
+
|
168
|
+
_authors = validator("authors", allow_reuse=True)(
|
169
|
+
valstr("authors", accept_none=True)
|
170
|
+
)
|
@@ -9,8 +9,9 @@ from pydantic import HttpUrl
|
|
9
9
|
from pydantic import root_validator
|
10
10
|
from pydantic import validator
|
11
11
|
|
12
|
-
from
|
13
|
-
from
|
12
|
+
from fractal_server.app.schemas._validators import val_unique_list
|
13
|
+
from fractal_server.app.schemas._validators import valdictkeys
|
14
|
+
from fractal_server.app.schemas._validators import valstr
|
14
15
|
from fractal_server.string_tools import validate_cmd
|
15
16
|
|
16
17
|
|
@@ -18,22 +19,27 @@ class TaskCreateV2(BaseModel, extra=Extra.forbid):
|
|
18
19
|
|
19
20
|
name: str
|
20
21
|
|
21
|
-
command_non_parallel: Optional[str]
|
22
|
-
command_parallel: Optional[str]
|
22
|
+
command_non_parallel: Optional[str] = None
|
23
|
+
command_parallel: Optional[str] = None
|
23
24
|
source: str
|
24
25
|
|
25
|
-
meta_non_parallel: Optional[dict[str, Any]]
|
26
|
-
meta_parallel: Optional[dict[str, Any]]
|
27
|
-
version: Optional[str]
|
28
|
-
args_schema_non_parallel: Optional[dict[str, Any]]
|
29
|
-
args_schema_parallel: Optional[dict[str, Any]]
|
30
|
-
args_schema_version: Optional[str]
|
31
|
-
docs_info: Optional[str]
|
32
|
-
docs_link: Optional[HttpUrl]
|
26
|
+
meta_non_parallel: Optional[dict[str, Any]] = None
|
27
|
+
meta_parallel: Optional[dict[str, Any]] = None
|
28
|
+
version: Optional[str] = None
|
29
|
+
args_schema_non_parallel: Optional[dict[str, Any]] = None
|
30
|
+
args_schema_parallel: Optional[dict[str, Any]] = None
|
31
|
+
args_schema_version: Optional[str] = None
|
32
|
+
docs_info: Optional[str] = None
|
33
|
+
docs_link: Optional[HttpUrl] = None
|
33
34
|
|
34
35
|
input_types: dict[str, bool] = Field(default={})
|
35
36
|
output_types: dict[str, bool] = Field(default={})
|
36
37
|
|
38
|
+
category: Optional[str] = None
|
39
|
+
modality: Optional[str] = None
|
40
|
+
tags: list[str] = Field(default_factory=list)
|
41
|
+
authors: Optional[str] = None
|
42
|
+
|
37
43
|
# Validators
|
38
44
|
@root_validator
|
39
45
|
def validate_commands(cls, values):
|
@@ -83,6 +89,22 @@ class TaskCreateV2(BaseModel, extra=Extra.forbid):
|
|
83
89
|
valdictkeys("output_types")
|
84
90
|
)
|
85
91
|
|
92
|
+
_category = validator("category", allow_reuse=True)(
|
93
|
+
valstr("category", accept_none=True)
|
94
|
+
)
|
95
|
+
_modality = validator("modality", allow_reuse=True)(
|
96
|
+
valstr("modality", accept_none=True)
|
97
|
+
)
|
98
|
+
_authors = validator("authors", allow_reuse=True)(
|
99
|
+
valstr("authors", accept_none=True)
|
100
|
+
)
|
101
|
+
|
102
|
+
@validator("tags")
|
103
|
+
def validate_list_of_strings(cls, value):
|
104
|
+
for i, tag in enumerate(value):
|
105
|
+
value[i] = valstr(f"tags[{i}]")(tag)
|
106
|
+
return val_unique_list("tags")(value)
|
107
|
+
|
86
108
|
|
87
109
|
class TaskReadV2(BaseModel):
|
88
110
|
|
@@ -90,31 +112,41 @@ class TaskReadV2(BaseModel):
|
|
90
112
|
name: str
|
91
113
|
type: Literal["parallel", "non_parallel", "compound"]
|
92
114
|
source: str
|
93
|
-
version: Optional[str]
|
115
|
+
version: Optional[str] = None
|
94
116
|
|
95
|
-
command_non_parallel: Optional[str]
|
96
|
-
command_parallel: Optional[str]
|
117
|
+
command_non_parallel: Optional[str] = None
|
118
|
+
command_parallel: Optional[str] = None
|
97
119
|
meta_parallel: dict[str, Any]
|
98
120
|
meta_non_parallel: dict[str, Any]
|
99
121
|
args_schema_non_parallel: Optional[dict[str, Any]] = None
|
100
122
|
args_schema_parallel: Optional[dict[str, Any]] = None
|
101
|
-
args_schema_version: Optional[str]
|
102
|
-
docs_info: Optional[str]
|
103
|
-
docs_link: Optional[HttpUrl]
|
123
|
+
args_schema_version: Optional[str] = None
|
124
|
+
docs_info: Optional[str] = None
|
125
|
+
docs_link: Optional[HttpUrl] = None
|
104
126
|
input_types: dict[str, bool]
|
105
127
|
output_types: dict[str, bool]
|
106
128
|
|
107
|
-
taskgroupv2_id: Optional[int]
|
129
|
+
taskgroupv2_id: Optional[int] = None
|
130
|
+
|
131
|
+
category: Optional[str] = None
|
132
|
+
modality: Optional[str] = None
|
133
|
+
authors: Optional[str] = None
|
134
|
+
tags: list[str]
|
108
135
|
|
109
136
|
|
110
137
|
class TaskUpdateV2(BaseModel):
|
111
138
|
|
112
|
-
name: Optional[str]
|
113
|
-
version: Optional[str]
|
114
|
-
command_parallel: Optional[str]
|
115
|
-
command_non_parallel: Optional[str]
|
116
|
-
input_types: Optional[dict[str, bool]]
|
117
|
-
output_types: Optional[dict[str, bool]]
|
139
|
+
name: Optional[str] = None
|
140
|
+
version: Optional[str] = None
|
141
|
+
command_parallel: Optional[str] = None
|
142
|
+
command_non_parallel: Optional[str] = None
|
143
|
+
input_types: Optional[dict[str, bool]] = None
|
144
|
+
output_types: Optional[dict[str, bool]] = None
|
145
|
+
|
146
|
+
category: Optional[str] = None
|
147
|
+
modality: Optional[str] = None
|
148
|
+
authors: Optional[str] = None
|
149
|
+
tags: Optional[list[str]] = None
|
118
150
|
|
119
151
|
# Validators
|
120
152
|
@validator("input_types", "output_types")
|
@@ -140,6 +172,22 @@ class TaskUpdateV2(BaseModel):
|
|
140
172
|
valdictkeys("output_types")
|
141
173
|
)
|
142
174
|
|
175
|
+
_category = validator("category", allow_reuse=True)(
|
176
|
+
valstr("category", accept_none=True)
|
177
|
+
)
|
178
|
+
_modality = validator("modality", allow_reuse=True)(
|
179
|
+
valstr("modality", accept_none=True)
|
180
|
+
)
|
181
|
+
_authors = validator("authors", allow_reuse=True)(
|
182
|
+
valstr("authors", accept_none=True)
|
183
|
+
)
|
184
|
+
|
185
|
+
@validator("tags")
|
186
|
+
def validate_tags(cls, value):
|
187
|
+
for i, tag in enumerate(value):
|
188
|
+
value[i] = valstr(f"tags[{i}]")(tag)
|
189
|
+
return val_unique_list("tags")(value)
|
190
|
+
|
143
191
|
|
144
192
|
class TaskImportV2(BaseModel):
|
145
193
|
|
@@ -1,23 +1,50 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
from typing import Literal
|
1
3
|
from typing import Optional
|
2
4
|
|
3
5
|
from pydantic import BaseModel
|
6
|
+
from pydantic import validator
|
4
7
|
|
5
8
|
from .task import TaskReadV2
|
6
9
|
|
7
10
|
|
8
11
|
class TaskGroupCreateV2(BaseModel):
|
9
12
|
active: bool = True
|
13
|
+
origin: Literal["pypi", "wheel-file", "other"]
|
14
|
+
pkg_name: str
|
15
|
+
version: Optional[str] = None
|
16
|
+
python_version: Optional[str] = None
|
17
|
+
path: Optional[str] = None
|
18
|
+
venv_path: Optional[str] = None
|
19
|
+
pip_extras: Optional[str] = None
|
10
20
|
|
11
21
|
|
12
22
|
class TaskGroupReadV2(BaseModel):
|
13
23
|
|
14
24
|
id: int
|
25
|
+
task_list: list[TaskReadV2]
|
26
|
+
|
15
27
|
user_id: int
|
16
28
|
user_group_id: Optional[int] = None
|
29
|
+
|
30
|
+
origin: Literal["pypi", "wheel-file", "other"]
|
31
|
+
pkg_name: str
|
32
|
+
version: Optional[str] = None
|
33
|
+
python_version: Optional[str] = None
|
34
|
+
path: Optional[str] = None
|
35
|
+
venv_path: Optional[str] = None
|
36
|
+
pip_extras: Optional[str] = None
|
37
|
+
|
17
38
|
active: bool
|
18
|
-
|
39
|
+
timestamp_created: datetime
|
19
40
|
|
20
41
|
|
21
42
|
class TaskGroupUpdateV2(BaseModel):
|
22
43
|
user_group_id: Optional[int] = None
|
23
44
|
active: Optional[bool] = None
|
45
|
+
|
46
|
+
@validator("active")
|
47
|
+
def active_cannot_be_None(cls, value):
|
48
|
+
if value is None:
|
49
|
+
raise ValueError("`active` cannot be set to None")
|
50
|
+
return value
|