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.
Files changed (32) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +10 -4
  3. fractal_server/app/models/linkusergroup.py +11 -0
  4. fractal_server/app/models/v2/task.py +19 -5
  5. fractal_server/app/routes/admin/v2/__init__.py +16 -0
  6. fractal_server/app/routes/admin/{v2.py → v2/job.py} +20 -191
  7. fractal_server/app/routes/admin/v2/project.py +43 -0
  8. fractal_server/app/routes/admin/v2/task.py +146 -0
  9. fractal_server/app/routes/admin/v2/task_group.py +134 -0
  10. fractal_server/app/routes/api/v2/task.py +13 -0
  11. fractal_server/app/routes/api/v2/task_collection_custom.py +7 -1
  12. fractal_server/app/routes/api/v2/task_group.py +11 -3
  13. fractal_server/app/routes/auth/_aux_auth.py +30 -29
  14. fractal_server/app/routes/auth/current_user.py +5 -5
  15. fractal_server/app/routes/auth/router.py +0 -2
  16. fractal_server/app/routes/auth/users.py +8 -7
  17. fractal_server/app/schemas/user.py +1 -2
  18. fractal_server/app/schemas/v2/manifest.py +12 -1
  19. fractal_server/app/schemas/v2/task.py +73 -25
  20. fractal_server/app/schemas/v2/task_group.py +28 -1
  21. fractal_server/data_migrations/2_7_0.py +274 -0
  22. fractal_server/migrations/versions/742b74e1cc6e_revamp_taskv2_and_taskgroupv2.py +101 -0
  23. fractal_server/migrations/versions/df7cc3501bf7_linkusergroup_timestamp_created.py +42 -0
  24. fractal_server/tasks/v2/background_operations.py +12 -1
  25. fractal_server/tasks/v2/background_operations_ssh.py +11 -1
  26. fractal_server/tasks/v2/endpoint_operations.py +42 -0
  27. {fractal_server-2.7.0a0.dist-info → fractal_server-2.7.0a2.dist-info}/METADATA +1 -1
  28. {fractal_server-2.7.0a0.dist-info → fractal_server-2.7.0a2.dist-info}/RECORD +31 -25
  29. fractal_server/app/routes/auth/group_names.py +0 -34
  30. {fractal_server-2.7.0a0.dist-info → fractal_server-2.7.0a2.dist-info}/LICENSE +0 -0
  31. {fractal_server-2.7.0a0.dist-info → fractal_server-2.7.0a2.dist-info}/WHEEL +0 -0
  32. {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
- stm = select(TaskGroupV2).where(
39
- or_(
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
- async def _get_single_user_with_group_names(
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 `group_names` attribute.
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 `group_names` set
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 == UserOAuth.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
- group_names = [group.name for group in groups]
36
- return UserRead(
37
- **user.model_dump(),
38
- group_names=group_names,
39
- oauth_accounts=user.oauth_accounts,
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
- group_ids=group_ids,
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 _get_single_user_with_group_names
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
- group_names: bool = False,
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 group_names is True:
39
- user_with_groups = await _get_single_user_with_group_names(user, db)
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 _get_single_user_with_group_names(
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 _get_single_user_with_group_ids
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
- group_ids: bool = True,
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 group_ids:
50
- user = await _get_single_user_with_group_ids(user, db)
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 `group_ids` attribute
167
- patched_user_with_group_ids = await _get_single_user_with_group_ids(
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 patched_user_with_group_ids
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
- group_names: Optional[list[str]] = None
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 .._validators import valdictkeys
13
- from .._validators import valstr
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
- task_list: list[TaskReadV2]
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