fractal-server 2.7.0a1__py3-none-any.whl → 2.7.0a3__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.
@@ -1 +1 @@
1
- __VERSION__ = "2.7.0a1"
1
+ __VERSION__ = "2.7.0a3"
@@ -57,10 +57,16 @@ set_db_parser.add_argument(
57
57
  )
58
58
 
59
59
  # fractalctl update-db-data
60
- subparsers.add_parser(
60
+ update_db_data_parser = subparsers.add_parser(
61
61
  "update-db-data",
62
62
  description="Apply data-migration script to an existing database.",
63
63
  )
64
+ update_db_data_parser.add_argument(
65
+ "--dry-run",
66
+ action="store_true",
67
+ help="If set, perform a dry run of the data migration.",
68
+ default=False,
69
+ )
64
70
 
65
71
 
66
72
  def save_openapi(dest="openapi.json"):
@@ -120,7 +126,7 @@ def set_db(skip_init_data: bool = False):
120
126
  print()
121
127
 
122
128
 
123
- def update_db_data():
129
+ def update_db_data(dry_run: bool = False):
124
130
  """
125
131
  Apply data migrations.
126
132
  """
@@ -185,7 +191,7 @@ def update_db_data():
185
191
  sys.exit()
186
192
 
187
193
  print("OK, now starting data-migration script\n")
188
- current_update_db_data_module.fix_db()
194
+ current_update_db_data_module.fix_db(dry_run=dry_run)
189
195
 
190
196
 
191
197
  def run():
@@ -196,7 +202,7 @@ def run():
196
202
  elif args.cmd == "set-db":
197
203
  set_db(skip_init_data=args.skip_init_data)
198
204
  elif args.cmd == "update-db-data":
199
- update_db_data()
205
+ update_db_data(dry_run=args.dry_run)
200
206
  elif args.cmd == "start":
201
207
  uvicorn.run(
202
208
  "fractal_server.main:app",
@@ -47,21 +47,35 @@ class TaskV2(SQLModel, table=True):
47
47
 
48
48
  taskgroupv2_id: Optional[int] = Field(foreign_key="taskgroupv2.id")
49
49
 
50
+ category: Optional[str] = None
51
+ modality: Optional[str] = None
52
+ authors: Optional[str] = None
53
+ tags: list[str] = Field(
54
+ sa_column=Column(JSON, server_default="[]", nullable=False)
55
+ )
56
+
50
57
 
51
58
  class TaskGroupV2(SQLModel, table=True):
52
59
 
53
60
  id: Optional[int] = Field(default=None, primary_key=True)
54
-
55
- user_id: int = Field(foreign_key="user_oauth.id")
56
- user_group_id: Optional[int] = Field(foreign_key="usergroup.id")
57
-
58
- active: bool = True
59
61
  task_list: list[TaskV2] = Relationship(
60
62
  sa_relationship_kwargs=dict(
61
63
  lazy="selectin", cascade="all, delete-orphan"
62
64
  ),
63
65
  )
64
66
 
67
+ user_id: int = Field(foreign_key="user_oauth.id")
68
+ user_group_id: Optional[int] = Field(foreign_key="usergroup.id")
69
+
70
+ origin: str
71
+ pkg_name: str
72
+ version: Optional[str] = None
73
+ python_version: Optional[str] = None
74
+ path: Optional[str] = None
75
+ venv_path: Optional[str] = None
76
+ pip_extras: Optional[str] = None
77
+
78
+ active: bool = True
65
79
  timestamp_created: datetime = Field(
66
80
  default_factory=get_timestamp,
67
81
  sa_column=Column(DateTime(timezone=True), nullable=False),
@@ -0,0 +1,16 @@
1
+ """
2
+ `admin/v2` module
3
+ """
4
+ from fastapi import APIRouter
5
+
6
+ from .job import router as job_router
7
+ from .project import router as project_router
8
+ from .task import router as task_router
9
+ from .task_group import router as task_group_router
10
+
11
+ router_admin_v2 = APIRouter()
12
+
13
+ router_admin_v2.include_router(job_router, prefix="/job")
14
+ router_admin_v2.include_router(project_router, prefix="/project")
15
+ router_admin_v2.include_router(task_router, prefix="/task")
16
+ router_admin_v2.include_router(task_group_router, prefix="/task-group")
@@ -1,10 +1,6 @@
1
- """
2
- Definition of `/admin` routes.
3
- """
4
1
  from datetime import datetime
5
2
  from datetime import timezone
6
3
  from pathlib import Path
7
- from typing import Literal
8
4
  from typing import Optional
9
5
 
10
6
  from fastapi import APIRouter
@@ -13,33 +9,26 @@ from fastapi import HTTPException
13
9
  from fastapi import Response
14
10
  from fastapi import status
15
11
  from fastapi.responses import StreamingResponse
16
- from pydantic import BaseModel
17
- from pydantic import EmailStr
18
- from pydantic import Field
19
12
  from sqlmodel import select
20
13
 
21
- from ....config import get_settings
22
- from ....syringe import Inject
23
- from ....utils import get_timestamp
24
- from ....zip_tools import _zip_folder_to_byte_stream_iterator
25
- from ...db import AsyncSession
26
- from ...db import get_async_db
27
- from ...models.v2 import JobV2
28
- from ...models.v2 import ProjectV2
29
- from ...models.v2 import TaskV2
30
- from ...models.v2 import WorkflowTaskV2
31
- from ...models.v2 import WorkflowV2
32
- from ...runner.filenames import WORKFLOW_LOG_FILENAME
33
- from ...schemas.v2 import JobReadV2
34
- from ...schemas.v2 import JobStatusTypeV2
35
- from ...schemas.v2 import JobUpdateV2
36
- from ...schemas.v2 import ProjectReadV2
37
- from ..aux._job import _write_shutdown_file
38
- from ..aux._runner import _check_shutdown_is_supported
14
+ from fractal_server.app.db import AsyncSession
15
+ from fractal_server.app.db import get_async_db
39
16
  from fractal_server.app.models import UserOAuth
17
+ from fractal_server.app.models.v2 import JobV2
18
+ from fractal_server.app.models.v2 import ProjectV2
40
19
  from fractal_server.app.routes.auth import current_active_superuser
20
+ from fractal_server.app.routes.aux._job import _write_shutdown_file
21
+ from fractal_server.app.routes.aux._runner import _check_shutdown_is_supported
22
+ from fractal_server.app.runner.filenames import WORKFLOW_LOG_FILENAME
23
+ from fractal_server.app.schemas.v2 import JobReadV2
24
+ from fractal_server.app.schemas.v2 import JobStatusTypeV2
25
+ from fractal_server.app.schemas.v2 import JobUpdateV2
26
+ from fractal_server.config import get_settings
27
+ from fractal_server.syringe import Inject
28
+ from fractal_server.utils import get_timestamp
29
+ from fractal_server.zip_tools import _zip_folder_to_byte_stream_iterator
41
30
 
42
- router_admin_v2 = APIRouter()
31
+ router = APIRouter()
43
32
 
44
33
 
45
34
  def _convert_to_db_timestamp(dt: datetime) -> datetime:
@@ -59,36 +48,7 @@ def _convert_to_db_timestamp(dt: datetime) -> datetime:
59
48
  return _dt
60
49
 
61
50
 
62
- @router_admin_v2.get("/project/", response_model=list[ProjectReadV2])
63
- async def view_project(
64
- id: Optional[int] = None,
65
- user_id: Optional[int] = None,
66
- user: UserOAuth = Depends(current_active_superuser),
67
- db: AsyncSession = Depends(get_async_db),
68
- ) -> list[ProjectReadV2]:
69
- """
70
- Query `ProjectV2` table.
71
-
72
- Args:
73
- id: If not `None`, select a given `project.id`.
74
- user_id: If not `None`, select a given `project.user_id`.
75
- """
76
-
77
- stm = select(ProjectV2)
78
-
79
- if id is not None:
80
- stm = stm.where(ProjectV2.id == id)
81
- if user_id is not None:
82
- stm = stm.where(ProjectV2.user_list.any(UserOAuth.id == user_id))
83
-
84
- res = await db.execute(stm)
85
- project_list = res.scalars().all()
86
- await db.close()
87
-
88
- return project_list
89
-
90
-
91
- @router_admin_v2.get("/job/", response_model=list[JobReadV2])
51
+ @router.get("/", response_model=list[JobReadV2])
92
52
  async def view_job(
93
53
  id: Optional[int] = None,
94
54
  user_id: Optional[int] = None,
@@ -164,7 +124,7 @@ async def view_job(
164
124
  return job_list
165
125
 
166
126
 
167
- @router_admin_v2.get("/job/{job_id}/", response_model=JobReadV2)
127
+ @router.get("/{job_id}/", response_model=JobReadV2)
168
128
  async def view_single_job(
169
129
  job_id: int = None,
170
130
  show_tmp_logs: bool = False,
@@ -190,10 +150,7 @@ async def view_single_job(
190
150
  return job
191
151
 
192
152
 
193
- @router_admin_v2.patch(
194
- "/job/{job_id}/",
195
- response_model=JobReadV2,
196
- )
153
+ @router.patch("/{job_id}/", response_model=JobReadV2)
197
154
  async def update_job(
198
155
  job_update: JobUpdateV2,
199
156
  job_id: int,
@@ -227,7 +184,7 @@ async def update_job(
227
184
  return job
228
185
 
229
186
 
230
- @router_admin_v2.get("/job/{job_id}/stop/", status_code=202)
187
+ @router.get("/{job_id}/stop/", status_code=202)
231
188
  async def stop_job(
232
189
  job_id: int,
233
190
  user: UserOAuth = Depends(current_active_superuser),
@@ -251,10 +208,7 @@ async def stop_job(
251
208
  return Response(status_code=status.HTTP_202_ACCEPTED)
252
209
 
253
210
 
254
- @router_admin_v2.get(
255
- "/job/{job_id}/download/",
256
- response_class=StreamingResponse,
257
- )
211
+ @router.get("/{job_id}/download/", response_class=StreamingResponse)
258
212
  async def download_job_logs(
259
213
  job_id: int,
260
214
  user: UserOAuth = Depends(current_active_superuser),
@@ -278,128 +232,3 @@ async def download_job_logs(
278
232
  media_type="application/x-zip-compressed",
279
233
  headers={"Content-Disposition": f"attachment;filename={zip_filename}"},
280
234
  )
281
-
282
-
283
- class TaskV2Minimal(BaseModel):
284
-
285
- id: int
286
- name: str
287
- type: str
288
- command_non_parallel: Optional[str]
289
- command_parallel: Optional[str]
290
- source: str
291
- owner: Optional[str]
292
- version: Optional[str]
293
-
294
-
295
- class ProjectUser(BaseModel):
296
-
297
- id: int
298
- email: EmailStr
299
-
300
-
301
- class TaskV2Relationship(BaseModel):
302
-
303
- workflow_id: int
304
- workflow_name: str
305
- project_id: int
306
- project_name: str
307
- project_users: list[ProjectUser] = Field(default_factory=list)
308
-
309
-
310
- class TaskV2Info(BaseModel):
311
-
312
- task: TaskV2Minimal
313
- relationships: list[TaskV2Relationship]
314
-
315
-
316
- @router_admin_v2.get("/task/", response_model=list[TaskV2Info])
317
- async def query_tasks(
318
- id: Optional[int] = None,
319
- source: Optional[str] = None,
320
- version: Optional[str] = None,
321
- name: Optional[str] = None,
322
- owner: Optional[str] = None,
323
- kind: Optional[Literal["common", "users"]] = None,
324
- max_number_of_results: int = 25,
325
- user: UserOAuth = Depends(current_active_superuser),
326
- db: AsyncSession = Depends(get_async_db),
327
- ) -> list[TaskV2Info]:
328
- """
329
- Query `TaskV2` table and get informations about related items
330
- (WorkflowV2s and ProjectV2s)
331
-
332
- Args:
333
- id: If not `None`, query for matching `task.id`.
334
- source: If not `None`, query for contained case insensitive
335
- `task.source`.
336
- version: If not `None`, query for matching `task.version`.
337
- name: If not `None`, query for contained case insensitive `task.name`.
338
- owner: If not `None`, query for matching `task.owner`.
339
- kind: If not `None`, query for TaskV2s that have (`users`) or don't
340
- have (`common`) a `task.owner`.
341
- max_number_of_results: The maximum length of the response.
342
- """
343
-
344
- stm = select(TaskV2)
345
-
346
- if id is not None:
347
- stm = stm.where(TaskV2.id == id)
348
- if source is not None:
349
- stm = stm.where(TaskV2.source.icontains(source))
350
- if version is not None:
351
- stm = stm.where(TaskV2.version == version)
352
- if name is not None:
353
- stm = stm.where(TaskV2.name.icontains(name))
354
- if owner is not None:
355
- stm = stm.where(TaskV2.owner == owner)
356
-
357
- if kind == "common":
358
- stm = stm.where(TaskV2.owner == None) # noqa E711
359
- elif kind == "users":
360
- stm = stm.where(TaskV2.owner != None) # noqa E711
361
-
362
- res = await db.execute(stm)
363
- task_list = res.scalars().all()
364
- if len(task_list) > max_number_of_results:
365
- await db.close()
366
- raise HTTPException(
367
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
368
- detail=(
369
- f"Too many Tasks ({len(task_list)} > {max_number_of_results})."
370
- " Please add more query filters."
371
- ),
372
- )
373
-
374
- task_info_list = []
375
-
376
- for task in task_list:
377
- stm = (
378
- select(WorkflowV2)
379
- .join(WorkflowTaskV2)
380
- .where(WorkflowTaskV2.workflow_id == WorkflowV2.id)
381
- .where(WorkflowTaskV2.task_id == task.id)
382
- )
383
- res = await db.execute(stm)
384
- wf_list = res.scalars().all()
385
-
386
- task_info_list.append(
387
- dict(
388
- task=task.model_dump(),
389
- relationships=[
390
- dict(
391
- workflow_id=workflow.id,
392
- workflow_name=workflow.name,
393
- project_id=workflow.project.id,
394
- project_name=workflow.project.name,
395
- project_users=[
396
- dict(id=user.id, email=user.email)
397
- for user in workflow.project.user_list
398
- ],
399
- )
400
- for workflow in wf_list
401
- ],
402
- )
403
- )
404
-
405
- return task_info_list
@@ -0,0 +1,43 @@
1
+ from typing import Optional
2
+
3
+ from fastapi import APIRouter
4
+ from fastapi import Depends
5
+ from sqlmodel import select
6
+
7
+ from fractal_server.app.db import AsyncSession
8
+ from fractal_server.app.db import get_async_db
9
+ from fractal_server.app.models import UserOAuth
10
+ from fractal_server.app.models.v2 import ProjectV2
11
+ from fractal_server.app.routes.auth import current_active_superuser
12
+ from fractal_server.app.schemas.v2 import ProjectReadV2
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ @router.get("/", response_model=list[ProjectReadV2])
18
+ async def view_project(
19
+ id: Optional[int] = None,
20
+ user_id: Optional[int] = None,
21
+ user: UserOAuth = Depends(current_active_superuser),
22
+ db: AsyncSession = Depends(get_async_db),
23
+ ) -> list[ProjectReadV2]:
24
+ """
25
+ Query `ProjectV2` table.
26
+
27
+ Args:
28
+ id: If not `None`, select a given `project.id`.
29
+ user_id: If not `None`, select a given `project.user_id`.
30
+ """
31
+
32
+ stm = select(ProjectV2)
33
+
34
+ if id is not None:
35
+ stm = stm.where(ProjectV2.id == id)
36
+ if user_id is not None:
37
+ stm = stm.where(ProjectV2.user_list.any(UserOAuth.id == user_id))
38
+
39
+ res = await db.execute(stm)
40
+ project_list = res.scalars().all()
41
+ await db.close()
42
+
43
+ return project_list
@@ -0,0 +1,146 @@
1
+ from typing import Literal
2
+ from typing import Optional
3
+
4
+ from fastapi import APIRouter
5
+ from fastapi import Depends
6
+ from fastapi import HTTPException
7
+ from fastapi import status
8
+ from pydantic import BaseModel
9
+ from pydantic import EmailStr
10
+ from pydantic import Field
11
+ from sqlmodel import select
12
+
13
+ from fractal_server.app.db import AsyncSession
14
+ from fractal_server.app.db import get_async_db
15
+ from fractal_server.app.models import UserOAuth
16
+ from fractal_server.app.models.v2 import TaskV2
17
+ from fractal_server.app.models.v2 import WorkflowTaskV2
18
+ from fractal_server.app.models.v2 import WorkflowV2
19
+ from fractal_server.app.routes.auth import current_active_superuser
20
+
21
+ router = APIRouter()
22
+
23
+
24
+ class TaskV2Minimal(BaseModel):
25
+
26
+ id: int
27
+ name: str
28
+ type: str
29
+ command_non_parallel: Optional[str]
30
+ command_parallel: Optional[str]
31
+ source: str
32
+ owner: Optional[str]
33
+ version: Optional[str]
34
+
35
+
36
+ class ProjectUser(BaseModel):
37
+
38
+ id: int
39
+ email: EmailStr
40
+
41
+
42
+ class TaskV2Relationship(BaseModel):
43
+
44
+ workflow_id: int
45
+ workflow_name: str
46
+ project_id: int
47
+ project_name: str
48
+ project_users: list[ProjectUser] = Field(default_factory=list)
49
+
50
+
51
+ class TaskV2Info(BaseModel):
52
+
53
+ task: TaskV2Minimal
54
+ relationships: list[TaskV2Relationship]
55
+
56
+
57
+ @router.get("/", response_model=list[TaskV2Info])
58
+ async def query_tasks(
59
+ id: Optional[int] = None,
60
+ source: Optional[str] = None,
61
+ version: Optional[str] = None,
62
+ name: Optional[str] = None,
63
+ owner: Optional[str] = None,
64
+ kind: Optional[Literal["common", "users"]] = None,
65
+ max_number_of_results: int = 25,
66
+ user: UserOAuth = Depends(current_active_superuser),
67
+ db: AsyncSession = Depends(get_async_db),
68
+ ) -> list[TaskV2Info]:
69
+ """
70
+ Query `TaskV2` table and get informations about related items
71
+ (WorkflowV2s and ProjectV2s)
72
+
73
+ Args:
74
+ id: If not `None`, query for matching `task.id`.
75
+ source: If not `None`, query for contained case insensitive
76
+ `task.source`.
77
+ version: If not `None`, query for matching `task.version`.
78
+ name: If not `None`, query for contained case insensitive `task.name`.
79
+ owner: If not `None`, query for matching `task.owner`.
80
+ kind: If not `None`, query for TaskV2s that have (`users`) or don't
81
+ have (`common`) a `task.owner`.
82
+ max_number_of_results: The maximum length of the response.
83
+ """
84
+
85
+ stm = select(TaskV2)
86
+
87
+ if id is not None:
88
+ stm = stm.where(TaskV2.id == id)
89
+ if source is not None:
90
+ stm = stm.where(TaskV2.source.icontains(source))
91
+ if version is not None:
92
+ stm = stm.where(TaskV2.version == version)
93
+ if name is not None:
94
+ stm = stm.where(TaskV2.name.icontains(name))
95
+ if owner is not None:
96
+ stm = stm.where(TaskV2.owner == owner)
97
+
98
+ if kind == "common":
99
+ stm = stm.where(TaskV2.owner == None) # noqa E711
100
+ elif kind == "users":
101
+ stm = stm.where(TaskV2.owner != None) # noqa E711
102
+
103
+ res = await db.execute(stm)
104
+ task_list = res.scalars().all()
105
+ if len(task_list) > max_number_of_results:
106
+ await db.close()
107
+ raise HTTPException(
108
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
109
+ detail=(
110
+ f"Too many Tasks ({len(task_list)} > {max_number_of_results})."
111
+ " Please add more query filters."
112
+ ),
113
+ )
114
+
115
+ task_info_list = []
116
+
117
+ for task in task_list:
118
+ stm = (
119
+ select(WorkflowV2)
120
+ .join(WorkflowTaskV2)
121
+ .where(WorkflowTaskV2.workflow_id == WorkflowV2.id)
122
+ .where(WorkflowTaskV2.task_id == task.id)
123
+ )
124
+ res = await db.execute(stm)
125
+ wf_list = res.scalars().all()
126
+
127
+ task_info_list.append(
128
+ dict(
129
+ task=task.model_dump(),
130
+ relationships=[
131
+ dict(
132
+ workflow_id=workflow.id,
133
+ workflow_name=workflow.name,
134
+ project_id=workflow.project.id,
135
+ project_name=workflow.project.name,
136
+ project_users=[
137
+ dict(id=user.id, email=user.email)
138
+ for user in workflow.project.user_list
139
+ ],
140
+ )
141
+ for workflow in wf_list
142
+ ],
143
+ )
144
+ )
145
+
146
+ return task_info_list
@@ -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)