fractal-server 2.7.0a3__py3-none-any.whl → 2.7.0a4__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 +3 -9
- fractal_server/app/models/v2/collection_state.py +1 -0
- fractal_server/app/models/v2/task.py +27 -3
- fractal_server/app/routes/admin/v2/task.py +5 -13
- fractal_server/app/routes/admin/v2/task_group.py +21 -0
- fractal_server/app/routes/api/v1/task_collection.py +2 -2
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +75 -2
- fractal_server/app/routes/api/v2/task.py +16 -42
- fractal_server/app/routes/api/v2/task_collection.py +148 -187
- fractal_server/app/routes/api/v2/task_collection_custom.py +31 -58
- fractal_server/app/routes/api/v2/task_group.py +25 -1
- fractal_server/app/routes/api/v2/workflow.py +11 -46
- fractal_server/app/routes/auth/_aux_auth.py +15 -12
- fractal_server/app/routes/auth/group.py +46 -23
- fractal_server/app/runner/v2/task_interface.py +4 -9
- fractal_server/app/schemas/v2/dataset.py +2 -7
- fractal_server/app/schemas/v2/dumps.py +1 -1
- fractal_server/app/schemas/v2/job.py +1 -1
- fractal_server/app/schemas/v2/project.py +1 -1
- fractal_server/app/schemas/v2/task.py +5 -5
- fractal_server/app/schemas/v2/task_collection.py +8 -6
- fractal_server/app/schemas/v2/task_group.py +31 -3
- fractal_server/app/schemas/v2/workflow.py +2 -2
- fractal_server/app/schemas/v2/workflowtask.py +2 -2
- fractal_server/data_migrations/2_7_0.py +1 -11
- fractal_server/images/models.py +2 -4
- fractal_server/main.py +1 -1
- fractal_server/migrations/versions/034a469ec2eb_task_groups.py +184 -0
- fractal_server/string_tools.py +6 -2
- fractal_server/tasks/v1/_TaskCollectPip.py +1 -1
- fractal_server/tasks/v1/background_operations.py +2 -2
- fractal_server/tasks/v2/_venv_pip.py +62 -70
- fractal_server/tasks/v2/background_operations.py +168 -49
- fractal_server/tasks/v2/background_operations_ssh.py +35 -77
- fractal_server/tasks/v2/database_operations.py +7 -17
- fractal_server/tasks/v2/endpoint_operations.py +0 -134
- fractal_server/tasks/v2/templates/_1_create_venv.sh +9 -5
- fractal_server/tasks/v2/utils.py +5 -0
- fractal_server/utils.py +3 -2
- {fractal_server-2.7.0a3.dist-info → fractal_server-2.7.0a4.dist-info}/METADATA +1 -1
- {fractal_server-2.7.0a3.dist-info → fractal_server-2.7.0a4.dist-info}/RECORD +45 -48
- fractal_server/migrations/versions/742b74e1cc6e_revamp_taskv2_and_taskgroupv2.py +0 -101
- fractal_server/migrations/versions/7cf1baae8fb4_task_group_v2.py +0 -66
- fractal_server/migrations/versions/df7cc3501bf7_linkusergroup_timestamp_created.py +0 -42
- fractal_server/tasks/v2/_TaskCollectPip.py +0 -132
- {fractal_server-2.7.0a3.dist-info → fractal_server-2.7.0a4.dist-info}/LICENSE +0 -0
- {fractal_server-2.7.0a3.dist-info → fractal_server-2.7.0a4.dist-info}/WHEEL +0 -0
- {fractal_server-2.7.0a3.dist-info → fractal_server-2.7.0a4.dist-info}/entry_points.txt +0 -0
fractal_server/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "2.7.
|
1
|
+
__VERSION__ = "2.7.0a4"
|
fractal_server/__main__.py
CHANGED
@@ -61,12 +61,6 @@ 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
|
-
)
|
70
64
|
|
71
65
|
|
72
66
|
def save_openapi(dest="openapi.json"):
|
@@ -126,7 +120,7 @@ def set_db(skip_init_data: bool = False):
|
|
126
120
|
print()
|
127
121
|
|
128
122
|
|
129
|
-
def update_db_data(
|
123
|
+
def update_db_data():
|
130
124
|
"""
|
131
125
|
Apply data migrations.
|
132
126
|
"""
|
@@ -191,7 +185,7 @@ def update_db_data(dry_run: bool = False):
|
|
191
185
|
sys.exit()
|
192
186
|
|
193
187
|
print("OK, now starting data-migration script\n")
|
194
|
-
current_update_db_data_module.fix_db(
|
188
|
+
current_update_db_data_module.fix_db()
|
195
189
|
|
196
190
|
|
197
191
|
def run():
|
@@ -202,7 +196,7 @@ def run():
|
|
202
196
|
elif args.cmd == "set-db":
|
203
197
|
set_db(skip_init_data=args.skip_init_data)
|
204
198
|
elif args.cmd == "update-db-data":
|
205
|
-
update_db_data(
|
199
|
+
update_db_data()
|
206
200
|
elif args.cmd == "start":
|
207
201
|
uvicorn.run(
|
208
202
|
"fractal_server.main:app",
|
@@ -14,6 +14,7 @@ from ....utils import get_timestamp
|
|
14
14
|
class CollectionStateV2(SQLModel, table=True):
|
15
15
|
|
16
16
|
id: Optional[int] = Field(default=None, primary_key=True)
|
17
|
+
taskgroupv2_id: Optional[int] = Field(foreign_key="taskgroupv2.id")
|
17
18
|
data: dict[str, Any] = Field(sa_column=Column(JSON), default={})
|
18
19
|
timestamp: datetime = Field(
|
19
20
|
default_factory=get_timestamp,
|
@@ -14,14 +14,13 @@ from fractal_server.utils import get_timestamp
|
|
14
14
|
|
15
15
|
|
16
16
|
class TaskV2(SQLModel, table=True):
|
17
|
-
|
18
17
|
id: Optional[int] = Field(default=None, primary_key=True)
|
19
18
|
name: str
|
20
19
|
|
21
20
|
type: str
|
22
21
|
command_non_parallel: Optional[str] = None
|
23
22
|
command_parallel: Optional[str] = None
|
24
|
-
source: str =
|
23
|
+
source: Optional[str] = None
|
25
24
|
|
26
25
|
meta_non_parallel: dict[str, Any] = Field(
|
27
26
|
sa_column=Column(JSON, server_default="{}", default={}, nullable=False)
|
@@ -56,7 +55,6 @@ class TaskV2(SQLModel, table=True):
|
|
56
55
|
|
57
56
|
|
58
57
|
class TaskGroupV2(SQLModel, table=True):
|
59
|
-
|
60
58
|
id: Optional[int] = Field(default=None, primary_key=True)
|
61
59
|
task_list: list[TaskV2] = Relationship(
|
62
60
|
sa_relationship_kwargs=dict(
|
@@ -73,10 +71,36 @@ class TaskGroupV2(SQLModel, table=True):
|
|
73
71
|
python_version: Optional[str] = None
|
74
72
|
path: Optional[str] = None
|
75
73
|
venv_path: Optional[str] = None
|
74
|
+
wheel_path: Optional[str] = None
|
76
75
|
pip_extras: Optional[str] = None
|
76
|
+
pinned_package_versions: dict[str, str] = Field(
|
77
|
+
sa_column=Column(
|
78
|
+
JSON,
|
79
|
+
server_default="{}",
|
80
|
+
default={},
|
81
|
+
nullable=True,
|
82
|
+
),
|
83
|
+
)
|
77
84
|
|
78
85
|
active: bool = True
|
79
86
|
timestamp_created: datetime = Field(
|
80
87
|
default_factory=get_timestamp,
|
81
88
|
sa_column=Column(DateTime(timezone=True), nullable=False),
|
82
89
|
)
|
90
|
+
|
91
|
+
@property
|
92
|
+
def pip_install_string(self) -> str:
|
93
|
+
"""
|
94
|
+
Prepare string to be used in `python -m pip install`.
|
95
|
+
"""
|
96
|
+
extras = f"[{self.pip_extras}]" if self.pip_extras is not None else ""
|
97
|
+
|
98
|
+
if self.wheel_path is not None:
|
99
|
+
return f"{self.wheel_path}{extras}"
|
100
|
+
else:
|
101
|
+
if self.version is None:
|
102
|
+
raise ValueError(
|
103
|
+
"Cannot run `pip_install_string` with "
|
104
|
+
f"{self.pkg_name=}, {self.wheel_path=}, {self.version=}."
|
105
|
+
)
|
106
|
+
return f"{self.pkg_name}{extras}=={self.version}"
|
@@ -1,4 +1,3 @@
|
|
1
|
-
from typing import Literal
|
2
1
|
from typing import Optional
|
3
2
|
|
4
3
|
from fastapi import APIRouter
|
@@ -26,11 +25,12 @@ class TaskV2Minimal(BaseModel):
|
|
26
25
|
id: int
|
27
26
|
name: str
|
28
27
|
type: str
|
29
|
-
|
28
|
+
taskgroupv2_id: int
|
29
|
+
command_non_parallel: Optional[str] = None
|
30
30
|
command_parallel: Optional[str]
|
31
|
-
source: str
|
32
|
-
owner: Optional[str]
|
33
|
-
version: Optional[str]
|
31
|
+
source: Optional[str] = None
|
32
|
+
owner: Optional[str] = None
|
33
|
+
version: Optional[str] = None
|
34
34
|
|
35
35
|
|
36
36
|
class ProjectUser(BaseModel):
|
@@ -61,7 +61,6 @@ async def query_tasks(
|
|
61
61
|
version: Optional[str] = None,
|
62
62
|
name: Optional[str] = None,
|
63
63
|
owner: Optional[str] = None,
|
64
|
-
kind: Optional[Literal["common", "users"]] = None,
|
65
64
|
max_number_of_results: int = 25,
|
66
65
|
user: UserOAuth = Depends(current_active_superuser),
|
67
66
|
db: AsyncSession = Depends(get_async_db),
|
@@ -77,8 +76,6 @@ async def query_tasks(
|
|
77
76
|
version: If not `None`, query for matching `task.version`.
|
78
77
|
name: If not `None`, query for contained case insensitive `task.name`.
|
79
78
|
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
79
|
max_number_of_results: The maximum length of the response.
|
83
80
|
"""
|
84
81
|
|
@@ -95,11 +92,6 @@ async def query_tasks(
|
|
95
92
|
if owner is not None:
|
96
93
|
stm = stm.where(TaskV2.owner == owner)
|
97
94
|
|
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
95
|
res = await db.execute(stm)
|
104
96
|
task_list = res.scalars().all()
|
105
97
|
if len(task_list) > max_number_of_results:
|
@@ -12,6 +12,7 @@ from sqlmodel import select
|
|
12
12
|
from fractal_server.app.db import AsyncSession
|
13
13
|
from fractal_server.app.db import get_async_db
|
14
14
|
from fractal_server.app.models import UserOAuth
|
15
|
+
from fractal_server.app.models.v2 import CollectionStateV2
|
15
16
|
from fractal_server.app.models.v2 import TaskGroupV2
|
16
17
|
from fractal_server.app.models.v2 import WorkflowTaskV2
|
17
18
|
from fractal_server.app.routes.auth import current_active_superuser
|
@@ -20,9 +21,12 @@ from fractal_server.app.routes.auth._aux_auth import (
|
|
20
21
|
)
|
21
22
|
from fractal_server.app.schemas.v2 import TaskGroupReadV2
|
22
23
|
from fractal_server.app.schemas.v2 import TaskGroupUpdateV2
|
24
|
+
from fractal_server.logger import set_logger
|
23
25
|
|
24
26
|
router = APIRouter()
|
25
27
|
|
28
|
+
logger = set_logger(__name__)
|
29
|
+
|
26
30
|
|
27
31
|
@router.get("/{task_group_id}/", response_model=TaskGroupReadV2)
|
28
32
|
async def query_task_group(
|
@@ -128,6 +132,23 @@ async def delete_task_group(
|
|
128
132
|
detail=f"TaskV2 {workflow_tasks[0].task_id} is still in use",
|
129
133
|
)
|
130
134
|
|
135
|
+
# Cascade operations: set foreign-keys to null for CollectionStateV2 which
|
136
|
+
# are in relationship with the current TaskGroupV2
|
137
|
+
logger.debug("Start of cascade operations on CollectionStateV2.")
|
138
|
+
stm = select(CollectionStateV2).where(
|
139
|
+
CollectionStateV2.taskgroupv2_id == task_group_id
|
140
|
+
)
|
141
|
+
res = await db.execute(stm)
|
142
|
+
collection_states = res.scalars().all()
|
143
|
+
for collection_state in collection_states:
|
144
|
+
logger.debug(
|
145
|
+
f"Setting CollectionStateV2[{collection_state.id}].taskgroupv2_id "
|
146
|
+
"to None."
|
147
|
+
)
|
148
|
+
collection_state.taskgroupv2_id = None
|
149
|
+
db.add(collection_state)
|
150
|
+
logger.debug("End of cascade operations on CollectionStateV2.")
|
151
|
+
|
131
152
|
await db.delete(task_group)
|
132
153
|
await db.commit()
|
133
154
|
|
@@ -26,7 +26,7 @@ from ._aux_functions import _raise_if_v1_is_read_only
|
|
26
26
|
from fractal_server.app.models import UserOAuth
|
27
27
|
from fractal_server.app.routes.auth import current_active_user
|
28
28
|
from fractal_server.app.routes.auth import current_active_verified_user
|
29
|
-
from fractal_server.string_tools import
|
29
|
+
from fractal_server.string_tools import slugify_task_name_for_source_v1
|
30
30
|
from fractal_server.tasks.utils import get_collection_log
|
31
31
|
from fractal_server.tasks.v1._TaskCollectPip import _TaskCollectPip
|
32
32
|
from fractal_server.tasks.v1.background_operations import (
|
@@ -160,7 +160,7 @@ async def collect_tasks_pip(
|
|
160
160
|
|
161
161
|
# Check that tasks are not already in the DB
|
162
162
|
for new_task in task_pkg.package_manifest.task_list:
|
163
|
-
new_task_name_slug =
|
163
|
+
new_task_name_slug = slugify_task_name_for_source_v1(new_task.name)
|
164
164
|
new_task_source = f"{task_pkg.package_source}:{new_task_name_slug}"
|
165
165
|
stm = select(Task).where(Task.source == new_task_source)
|
166
166
|
res = await db.execute(stm)
|
@@ -2,6 +2,7 @@
|
|
2
2
|
Auxiliary functions to get task and task-group object from the database or
|
3
3
|
perform simple checks
|
4
4
|
"""
|
5
|
+
from typing import Any
|
5
6
|
from typing import Optional
|
6
7
|
|
7
8
|
from fastapi import HTTPException
|
@@ -12,7 +13,8 @@ from ....db import AsyncSession
|
|
12
13
|
from ....models import LinkUserGroup
|
13
14
|
from ....models.v2 import TaskGroupV2
|
14
15
|
from ....models.v2 import TaskV2
|
15
|
-
from
|
16
|
+
from ....models.v2 import WorkflowTaskV2
|
17
|
+
from ...auth._aux_auth import _get_default_usergroup_id
|
16
18
|
from ...auth._aux_auth import _verify_user_belongs_to_group
|
17
19
|
|
18
20
|
|
@@ -201,9 +203,80 @@ async def _get_valid_user_group_id(
|
|
201
203
|
elif private is True:
|
202
204
|
user_group_id = None
|
203
205
|
elif user_group_id is None:
|
204
|
-
user_group_id = await
|
206
|
+
user_group_id = await _get_default_usergroup_id(db=db)
|
205
207
|
else:
|
206
208
|
await _verify_user_belongs_to_group(
|
207
209
|
user_id=user_id, user_group_id=user_group_id, db=db
|
208
210
|
)
|
209
211
|
return user_group_id
|
212
|
+
|
213
|
+
|
214
|
+
async def _verify_non_duplication_user_constraint(
|
215
|
+
db: AsyncSession,
|
216
|
+
user_id: int,
|
217
|
+
pkg_name: str,
|
218
|
+
version: Optional[str],
|
219
|
+
):
|
220
|
+
stm = (
|
221
|
+
select(TaskGroupV2)
|
222
|
+
.where(TaskGroupV2.user_id == user_id)
|
223
|
+
.where(TaskGroupV2.pkg_name == pkg_name)
|
224
|
+
.where(TaskGroupV2.version == version)
|
225
|
+
)
|
226
|
+
res = await db.execute(stm)
|
227
|
+
duplicate = res.scalars().all()
|
228
|
+
if duplicate:
|
229
|
+
raise HTTPException(
|
230
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
231
|
+
detail=(
|
232
|
+
"There is already a TaskGroupV2 with "
|
233
|
+
f"({pkg_name=}, {version=}, {user_id=})."
|
234
|
+
),
|
235
|
+
)
|
236
|
+
|
237
|
+
|
238
|
+
async def _verify_non_duplication_group_constraint(
|
239
|
+
db: AsyncSession,
|
240
|
+
user_group_id: Optional[int],
|
241
|
+
pkg_name: str,
|
242
|
+
version: Optional[str],
|
243
|
+
):
|
244
|
+
if user_group_id is None:
|
245
|
+
return
|
246
|
+
|
247
|
+
stm = (
|
248
|
+
select(TaskGroupV2)
|
249
|
+
.where(TaskGroupV2.user_group_id == user_group_id)
|
250
|
+
.where(TaskGroupV2.pkg_name == pkg_name)
|
251
|
+
.where(TaskGroupV2.version == version)
|
252
|
+
)
|
253
|
+
res = await db.execute(stm)
|
254
|
+
duplicate = res.scalars().all()
|
255
|
+
if duplicate:
|
256
|
+
raise HTTPException(
|
257
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
258
|
+
detail=(
|
259
|
+
"There is already a TaskGroupV2 with "
|
260
|
+
f"({pkg_name=}, {version=}, {user_group_id=})."
|
261
|
+
),
|
262
|
+
)
|
263
|
+
|
264
|
+
|
265
|
+
async def _add_warnings_to_workflow_tasks(
|
266
|
+
wftask_list: list[WorkflowTaskV2], user_id: int, db: AsyncSession
|
267
|
+
) -> list[dict[str, Any]]:
|
268
|
+
wftask_list_with_warnings = []
|
269
|
+
for wftask in wftask_list:
|
270
|
+
wftask_data = dict(wftask.model_dump(), task=wftask.task)
|
271
|
+
try:
|
272
|
+
task_group = await _get_task_group_read_access(
|
273
|
+
task_group_id=wftask.task.taskgroupv2_id,
|
274
|
+
user_id=user_id,
|
275
|
+
db=db,
|
276
|
+
)
|
277
|
+
if not task_group.active:
|
278
|
+
wftask_data["warning"] = "Task is not active."
|
279
|
+
except HTTPException:
|
280
|
+
wftask_data["warning"] = "Current user has no access to this task."
|
281
|
+
wftask_list_with_warnings.append(wftask_data)
|
282
|
+
return wftask_list_with_warnings
|
@@ -10,15 +10,15 @@ from sqlmodel import func
|
|
10
10
|
from sqlmodel import or_
|
11
11
|
from sqlmodel import select
|
12
12
|
|
13
|
-
from ...aux.validate_user_settings import verify_user_has_settings
|
14
13
|
from ._aux_functions_tasks import _get_task_full_access
|
15
14
|
from ._aux_functions_tasks import _get_task_read_access
|
16
15
|
from ._aux_functions_tasks import _get_valid_user_group_id
|
16
|
+
from ._aux_functions_tasks import _verify_non_duplication_group_constraint
|
17
|
+
from ._aux_functions_tasks import _verify_non_duplication_user_constraint
|
17
18
|
from fractal_server.app.db import AsyncSession
|
18
19
|
from fractal_server.app.db import get_async_db
|
19
20
|
from fractal_server.app.models import LinkUserGroup
|
20
21
|
from fractal_server.app.models import UserOAuth
|
21
|
-
from fractal_server.app.models.v1 import Task as TaskV1
|
22
22
|
from fractal_server.app.models.v2 import TaskGroupV2
|
23
23
|
from fractal_server.app.models.v2 import TaskV2
|
24
24
|
from fractal_server.app.routes.auth import current_active_user
|
@@ -102,7 +102,7 @@ async def patch_task(
|
|
102
102
|
db: AsyncSession = Depends(get_async_db),
|
103
103
|
) -> Optional[TaskReadV2]:
|
104
104
|
"""
|
105
|
-
Edit a specific task (restricted to
|
105
|
+
Edit a specific task (restricted to task owner)
|
106
106
|
"""
|
107
107
|
|
108
108
|
# Retrieve task from database
|
@@ -183,52 +183,26 @@ async def create_task(
|
|
183
183
|
),
|
184
184
|
)
|
185
185
|
|
186
|
-
# Set task.owner attribute - FIXME: remove this block
|
187
|
-
if user.username:
|
188
|
-
owner = user.username
|
189
|
-
else:
|
190
|
-
verify_user_has_settings(user)
|
191
|
-
if user.settings.slurm_user:
|
192
|
-
owner = user.settings.slurm_user
|
193
|
-
else:
|
194
|
-
raise HTTPException(
|
195
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
196
|
-
detail=(
|
197
|
-
"Cannot add a new task because current user does not "
|
198
|
-
"have `username` or `slurm_user` attributes."
|
199
|
-
),
|
200
|
-
)
|
201
|
-
|
202
|
-
# Prepend owner to task.source
|
203
|
-
task.source = f"{owner}:{task.source}"
|
204
|
-
|
205
|
-
# Verify that source is not already in use (note: this check is only useful
|
206
|
-
# to provide a user-friendly error message, but `task.source` uniqueness is
|
207
|
-
# already guaranteed by a constraint in the table definition).
|
208
|
-
stm = select(TaskV2).where(TaskV2.source == task.source)
|
209
|
-
res = await db.execute(stm)
|
210
|
-
if res.scalars().all():
|
211
|
-
raise HTTPException(
|
212
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
213
|
-
detail=f"Source '{task.source}' already used by some TaskV2",
|
214
|
-
)
|
215
|
-
stm = select(TaskV1).where(TaskV1.source == task.source)
|
216
|
-
res = await db.execute(stm)
|
217
|
-
if res.scalars().all():
|
218
|
-
raise HTTPException(
|
219
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
220
|
-
detail=f"Source '{task.source}' already used by some TaskV1",
|
221
|
-
)
|
222
186
|
# Add task
|
223
|
-
db_task = TaskV2(**task.dict(),
|
224
|
-
|
187
|
+
db_task = TaskV2(**task.dict(), type=task_type)
|
188
|
+
pkg_name = db_task.name
|
189
|
+
await _verify_non_duplication_user_constraint(
|
190
|
+
db=db, pkg_name=pkg_name, user_id=user.id, version=db_task.version
|
191
|
+
)
|
192
|
+
await _verify_non_duplication_group_constraint(
|
193
|
+
db=db,
|
194
|
+
pkg_name=pkg_name,
|
195
|
+
user_group_id=user_group_id,
|
196
|
+
version=db_task.version,
|
197
|
+
)
|
225
198
|
db_task_group = TaskGroupV2(
|
226
199
|
user_id=user.id,
|
227
200
|
user_group_id=user_group_id,
|
228
201
|
active=True,
|
229
202
|
task_list=[db_task],
|
230
203
|
origin="other",
|
231
|
-
|
204
|
+
version=db_task.version,
|
205
|
+
pkg_name=pkg_name,
|
232
206
|
)
|
233
207
|
db.add(db_task_group)
|
234
208
|
await db.commit()
|