fractal-server 2.6.4__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +1 -1
  3. fractal_server/app/models/linkusergroup.py +11 -0
  4. fractal_server/app/models/v2/__init__.py +2 -0
  5. fractal_server/app/models/v2/collection_state.py +1 -0
  6. fractal_server/app/models/v2/task.py +67 -2
  7. fractal_server/app/routes/admin/v2/__init__.py +16 -0
  8. fractal_server/app/routes/admin/{v2.py → v2/job.py} +20 -191
  9. fractal_server/app/routes/admin/v2/project.py +43 -0
  10. fractal_server/app/routes/admin/v2/task.py +133 -0
  11. fractal_server/app/routes/admin/v2/task_group.py +162 -0
  12. fractal_server/app/routes/api/v1/task_collection.py +4 -4
  13. fractal_server/app/routes/api/v2/__init__.py +8 -0
  14. fractal_server/app/routes/api/v2/_aux_functions.py +1 -68
  15. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +343 -0
  16. fractal_server/app/routes/api/v2/submit.py +16 -35
  17. fractal_server/app/routes/api/v2/task.py +85 -110
  18. fractal_server/app/routes/api/v2/task_collection.py +184 -196
  19. fractal_server/app/routes/api/v2/task_collection_custom.py +70 -64
  20. fractal_server/app/routes/api/v2/task_group.py +173 -0
  21. fractal_server/app/routes/api/v2/workflow.py +39 -102
  22. fractal_server/app/routes/api/v2/workflow_import.py +360 -0
  23. fractal_server/app/routes/api/v2/workflowtask.py +4 -8
  24. fractal_server/app/routes/auth/_aux_auth.py +86 -40
  25. fractal_server/app/routes/auth/current_user.py +5 -5
  26. fractal_server/app/routes/auth/group.py +73 -23
  27. fractal_server/app/routes/auth/router.py +0 -2
  28. fractal_server/app/routes/auth/users.py +8 -7
  29. fractal_server/app/runner/executors/slurm/ssh/executor.py +82 -63
  30. fractal_server/app/runner/v2/__init__.py +13 -7
  31. fractal_server/app/runner/v2/task_interface.py +4 -9
  32. fractal_server/app/schemas/user.py +1 -2
  33. fractal_server/app/schemas/v2/__init__.py +7 -0
  34. fractal_server/app/schemas/v2/dataset.py +2 -7
  35. fractal_server/app/schemas/v2/dumps.py +1 -2
  36. fractal_server/app/schemas/v2/job.py +1 -1
  37. fractal_server/app/schemas/v2/manifest.py +25 -1
  38. fractal_server/app/schemas/v2/project.py +1 -1
  39. fractal_server/app/schemas/v2/task.py +95 -36
  40. fractal_server/app/schemas/v2/task_collection.py +8 -6
  41. fractal_server/app/schemas/v2/task_group.py +85 -0
  42. fractal_server/app/schemas/v2/workflow.py +7 -2
  43. fractal_server/app/schemas/v2/workflowtask.py +9 -6
  44. fractal_server/app/security/__init__.py +8 -1
  45. fractal_server/config.py +8 -28
  46. fractal_server/data_migrations/2_7_0.py +323 -0
  47. fractal_server/images/models.py +2 -4
  48. fractal_server/main.py +1 -1
  49. fractal_server/migrations/versions/034a469ec2eb_task_groups.py +184 -0
  50. fractal_server/ssh/_fabric.py +186 -73
  51. fractal_server/string_tools.py +6 -2
  52. fractal_server/tasks/utils.py +19 -5
  53. fractal_server/tasks/v1/_TaskCollectPip.py +1 -1
  54. fractal_server/tasks/v1/background_operations.py +5 -5
  55. fractal_server/tasks/v1/get_collection_data.py +2 -2
  56. fractal_server/tasks/v2/_venv_pip.py +67 -70
  57. fractal_server/tasks/v2/background_operations.py +180 -69
  58. fractal_server/tasks/v2/background_operations_ssh.py +57 -70
  59. fractal_server/tasks/v2/database_operations.py +44 -0
  60. fractal_server/tasks/v2/endpoint_operations.py +104 -116
  61. fractal_server/tasks/v2/templates/_1_create_venv.sh +9 -5
  62. fractal_server/tasks/v2/templates/{_2_upgrade_pip.sh → _2_preliminary_pip_operations.sh} +1 -0
  63. fractal_server/tasks/v2/utils.py +5 -0
  64. fractal_server/utils.py +3 -2
  65. {fractal_server-2.6.4.dist-info → fractal_server-2.7.0.dist-info}/METADATA +3 -7
  66. {fractal_server-2.6.4.dist-info → fractal_server-2.7.0.dist-info}/RECORD +69 -60
  67. fractal_server/app/routes/auth/group_names.py +0 -34
  68. fractal_server/tasks/v2/_TaskCollectPip.py +0 -132
  69. {fractal_server-2.6.4.dist-info → fractal_server-2.7.0.dist-info}/LICENSE +0 -0
  70. {fractal_server-2.6.4.dist-info → fractal_server-2.7.0.dist-info}/WHEEL +0 -0
  71. {fractal_server-2.6.4.dist-info → fractal_server-2.7.0.dist-info}/entry_points.txt +0 -0
@@ -2,17 +2,48 @@ from pathlib import Path
2
2
  from typing import Optional
3
3
 
4
4
  from ..utils import COLLECTION_FREEZE_FILENAME
5
+ from fractal_server.app.models.v2 import TaskGroupV2
5
6
  from fractal_server.config import get_settings
6
7
  from fractal_server.logger import get_logger
7
8
  from fractal_server.syringe import Inject
8
- from fractal_server.tasks.v2._TaskCollectPip import _TaskCollectPip
9
9
  from fractal_server.tasks.v2.utils import get_python_interpreter_v2
10
10
  from fractal_server.utils import execute_command
11
11
 
12
12
 
13
- async def _pip_install(
13
+ async def _init_venv_v2(
14
+ *,
14
15
  venv_path: Path,
15
- task_pkg: _TaskCollectPip,
16
+ python_version: Optional[str] = None,
17
+ logger_name: str,
18
+ ) -> Path:
19
+ """
20
+ Set a virtual environment at `path/venv`
21
+
22
+ Args:
23
+ path : Path
24
+ path to the venv actual directory (not its parent).
25
+ python_version : default=None
26
+ Python version the virtual environment will be based upon
27
+
28
+ Returns:
29
+ python_bin : Path
30
+ path to python interpreter
31
+ """
32
+ logger = get_logger(logger_name)
33
+ logger.debug(f"[_init_venv_v2] {venv_path=}")
34
+ interpreter = get_python_interpreter_v2(python_version=python_version)
35
+ logger.debug(f"[_init_venv_v2] {interpreter=}")
36
+ await execute_command(
37
+ command=f"{interpreter} -m venv {venv_path}",
38
+ logger_name=logger_name,
39
+ )
40
+ python_bin = venv_path / "bin/python"
41
+ logger.debug(f"[_init_venv_v2] {python_bin=}")
42
+ return python_bin
43
+
44
+
45
+ async def _pip_install(
46
+ task_group: TaskGroupV2,
16
47
  logger_name: str,
17
48
  ) -> Path:
18
49
  """
@@ -30,48 +61,45 @@ async def _pip_install(
30
61
 
31
62
  logger = get_logger(logger_name)
32
63
 
33
- pip = venv_path / "venv/bin/pip"
34
-
35
- extras = f"[{task_pkg.package_extras}]" if task_pkg.package_extras else ""
36
-
37
- if task_pkg.is_local_package:
38
- pip_install_str = f"{task_pkg.package_path.as_posix()}{extras}"
39
- else:
40
- version_string = (
41
- f"=={task_pkg.package_version}" if task_pkg.package_version else ""
42
- )
43
- pip_install_str = f"{task_pkg.package_name}{extras}{version_string}"
64
+ python_bin = Path(task_group.venv_path) / "bin/python"
65
+ pip_install_str = task_group.pip_install_string
66
+ logger.info(f"{pip_install_str=}")
44
67
 
45
68
  await execute_command(
46
- cwd=venv_path,
69
+ cwd=Path(task_group.venv_path),
47
70
  command=(
48
- f"{pip} install --upgrade "
71
+ f"{python_bin} -m pip install --upgrade "
49
72
  f"'pip<={settings.FRACTAL_MAX_PIP_VERSION}'"
50
73
  ),
51
74
  logger_name=logger_name,
52
75
  )
53
76
  await execute_command(
54
- cwd=venv_path,
55
- command=f"{pip} install {pip_install_str}",
77
+ cwd=Path(task_group.venv_path),
78
+ command=f"{python_bin} -m pip install setuptools",
79
+ logger_name=logger_name,
80
+ )
81
+ await execute_command(
82
+ cwd=Path(task_group.venv_path),
83
+ command=f"{python_bin} -m pip install {pip_install_str}",
56
84
  logger_name=logger_name,
57
85
  )
58
- if task_pkg.pinned_package_versions:
86
+
87
+ if task_group.pinned_package_versions:
59
88
  for (
60
89
  pinned_pkg_name,
61
90
  pinned_pkg_version,
62
- ) in task_pkg.pinned_package_versions.items():
63
-
91
+ ) in task_group.pinned_package_versions.items():
64
92
  logger.debug(
65
93
  "Specific version required: "
66
94
  f"{pinned_pkg_name}=={pinned_pkg_version}"
67
95
  )
68
96
  logger.debug(
69
97
  "Preliminary check: verify that "
70
- f"{pinned_pkg_version} is already installed"
98
+ f"{pinned_pkg_name} is already installed"
71
99
  )
72
100
  stdout_show = await execute_command(
73
- cwd=venv_path,
74
- command=f"{pip} show {pinned_pkg_name}",
101
+ cwd=Path(task_group.venv_path),
102
+ command=f"{python_bin} -m pip show {pinned_pkg_name}",
75
103
  logger_name=logger_name,
76
104
  )
77
105
  current_version = next(
@@ -87,9 +115,9 @@ async def _pip_install(
87
115
  f"install version {pinned_pkg_version}."
88
116
  )
89
117
  await execute_command(
90
- cwd=venv_path,
118
+ cwd=Path(task_group.venv_path),
91
119
  command=(
92
- f"{pip} install "
120
+ f"{python_bin} -m pip install "
93
121
  f"{pinned_pkg_name}=={pinned_pkg_version}"
94
122
  ),
95
123
  logger_name=logger_name,
@@ -102,8 +130,8 @@ async def _pip_install(
102
130
 
103
131
  # Extract package installation path from `pip show`
104
132
  stdout_show = await execute_command(
105
- cwd=venv_path,
106
- command=f"{pip} show {task_pkg.package_name}",
133
+ cwd=Path(task_group.venv_path),
134
+ command=f"{python_bin} -m pip show {task_group.pkg_name}",
107
135
  logger_name=logger_name,
108
136
  )
109
137
 
@@ -124,58 +152,26 @@ async def _pip_install(
124
152
  # characters with underscore (_) characters, so the .dist-info directory
125
153
  # always has exactly one dash (-) character in its stem, separating the
126
154
  # name and version fields.
127
- package_root = location / (task_pkg.package_name.replace("-", "_"))
155
+ package_root = location / (task_group.pkg_name.replace("-", "_"))
128
156
  logger.debug(f"[_pip install] {location=}")
129
- logger.debug(f"[_pip install] {task_pkg.package_name=}")
157
+ logger.debug(f"[_pip install] {task_group.pkg_name=}")
130
158
  logger.debug(f"[_pip install] {package_root=}")
131
159
 
132
160
  # Run `pip freeze --all` and store its output
133
161
  stdout_freeze = await execute_command(
134
- cwd=venv_path, command=f"{pip} freeze --all", logger_name=logger_name
162
+ cwd=Path(task_group.venv_path),
163
+ command=f"{python_bin} -m pip freeze --all",
164
+ logger_name=logger_name,
135
165
  )
136
- with (venv_path / COLLECTION_FREEZE_FILENAME).open("w") as f:
166
+ with (Path(task_group.path) / COLLECTION_FREEZE_FILENAME).open("w") as f:
137
167
  f.write(stdout_freeze)
138
168
 
139
169
  return package_root
140
170
 
141
171
 
142
- async def _init_venv_v2(
143
- *,
144
- path: Path,
145
- python_version: Optional[str] = None,
146
- logger_name: str,
147
- ) -> Path:
148
- """
149
- Set a virtual environment at `path/venv`
150
-
151
- Args:
152
- path : Path
153
- path to directory in which to set up the virtual environment
154
- python_version : default=None
155
- Python version the virtual environment will be based upon
156
-
157
- Returns:
158
- python_bin : Path
159
- path to python interpreter
160
- """
161
- logger = get_logger(logger_name)
162
- logger.debug(f"[_init_venv] {path=}")
163
- interpreter = get_python_interpreter_v2(python_version=python_version)
164
- logger.debug(f"[_init_venv] {interpreter=}")
165
- await execute_command(
166
- cwd=path,
167
- command=f"{interpreter} -m venv venv",
168
- logger_name=logger_name,
169
- )
170
- python_bin = path / "venv/bin/python"
171
- logger.debug(f"[_init_venv] {python_bin=}")
172
- return python_bin
173
-
174
-
175
172
  async def _create_venv_install_package_pip(
176
173
  *,
177
- task_pkg: _TaskCollectPip,
178
- path: Path,
174
+ task_group: TaskGroupV2,
179
175
  logger_name: str,
180
176
  ) -> tuple[Path, Path]:
181
177
  """
@@ -191,11 +187,12 @@ async def _create_venv_install_package_pip(
191
187
  package_root: the location of the package manifest
192
188
  """
193
189
  python_bin = await _init_venv_v2(
194
- path=path,
195
- python_version=task_pkg.python_version,
190
+ venv_path=Path(task_group.venv_path),
191
+ python_version=task_group.python_version,
196
192
  logger_name=logger_name,
197
193
  )
198
194
  package_root = await _pip_install(
199
- venv_path=path, task_pkg=task_pkg, logger_name=logger_name
195
+ task_group=task_group,
196
+ logger_name=logger_name,
200
197
  )
201
198
  return python_bin, package_root
@@ -5,21 +5,23 @@ is used as a background task for the task-collection endpoint.
5
5
  import json
6
6
  from pathlib import Path
7
7
  from shutil import rmtree as shell_rmtree
8
+ from tempfile import TemporaryDirectory
8
9
  from typing import Optional
10
+ from typing import Union
11
+ from zipfile import ZipFile
9
12
 
10
13
  from sqlalchemy.orm import Session as DBSyncSession
11
14
  from sqlalchemy.orm.attributes import flag_modified
15
+ from sqlmodel import select
12
16
 
13
- from ...string_tools import slugify_task_name_for_source
14
- from ..utils import get_absolute_venv_path
15
- from ..utils import get_collection_freeze
16
- from ..utils import get_collection_log
17
+ from ..utils import get_collection_freeze_v2
18
+ from ..utils import get_collection_log_v2
17
19
  from ..utils import get_collection_path
18
20
  from ..utils import get_log_path
19
- from ._TaskCollectPip import _TaskCollectPip
21
+ from .database_operations import create_db_tasks_and_update_task_group
20
22
  from fractal_server.app.db import get_sync_db
21
23
  from fractal_server.app.models.v2 import CollectionStateV2
22
- from fractal_server.app.models.v2 import TaskV2
24
+ from fractal_server.app.models.v2 import TaskGroupV2
23
25
  from fractal_server.app.schemas.v2 import CollectionStatusV2
24
26
  from fractal_server.app.schemas.v2 import TaskCreateV2
25
27
  from fractal_server.app.schemas.v2 import TaskReadV2
@@ -28,38 +30,8 @@ from fractal_server.logger import get_logger
28
30
  from fractal_server.logger import reset_logger_handlers
29
31
  from fractal_server.logger import set_logger
30
32
  from fractal_server.tasks.v2._venv_pip import _create_venv_install_package_pip
31
-
32
-
33
- def _get_task_type(task: TaskCreateV2) -> str:
34
- if task.command_non_parallel is None:
35
- return "parallel"
36
- elif task.command_parallel is None:
37
- return "non_parallel"
38
- else:
39
- return "compound"
40
-
41
-
42
- def _insert_tasks(
43
- task_list: list[TaskCreateV2],
44
- db: DBSyncSession,
45
- owner: Optional[str] = None,
46
- ) -> list[TaskV2]:
47
- """
48
- Insert tasks into database
49
- """
50
-
51
- owner_dict = dict(owner=owner) if owner is not None else dict()
52
-
53
- task_db_list = [
54
- TaskV2(**t.dict(), **owner_dict, type=_get_task_type(t))
55
- for t in task_list
56
- ]
57
- db.add_all(task_db_list)
58
- db.commit()
59
- for t in task_db_list:
60
- db.refresh(t)
61
- db.close()
62
- return task_db_list
33
+ from fractal_server.tasks.v2.utils import get_python_interpreter_v2
34
+ from fractal_server.utils import execute_command
63
35
 
64
36
 
65
37
  def _set_collection_state_data_status(
@@ -113,7 +85,8 @@ def _handle_failure(
113
85
  logger_name: str,
114
86
  exception: Exception,
115
87
  db: DBSyncSession,
116
- venv_path: Optional[Path] = None,
88
+ task_group_id: int,
89
+ path: Optional[Path] = None,
117
90
  ):
118
91
  """
119
92
  Note: `venv_path` is only required to trigger the folder deletion.
@@ -144,11 +117,32 @@ def _handle_failure(
144
117
  db=db,
145
118
  )
146
119
  # Delete corrupted package dir
147
- if venv_path is not None:
148
- logger.info(f"Now delete temporary folder {venv_path}")
149
- shell_rmtree(venv_path)
120
+ if path is not None and Path(path).exists():
121
+ logger.info(f"Now delete temporary folder {path}")
122
+ shell_rmtree(path)
150
123
  logger.info("Temporary folder deleted")
151
124
 
125
+ # Delete TaskGroupV2 object / and apply cascade operation to FKs
126
+ logger.info(f"Now delete TaskGroupV2 with {task_group_id=}")
127
+ logger.info("Start of CollectionStateV2 cascade operations.")
128
+ stm = select(CollectionStateV2).where(
129
+ CollectionStateV2.taskgroupv2_id == task_group_id
130
+ )
131
+ res = db.execute(stm)
132
+ collection_states = res.scalars().all()
133
+ for collection_state in collection_states:
134
+ logger.info(
135
+ f"Setting CollectionStateV2[{collection_state.id}].taskgroupv2_id "
136
+ "to None."
137
+ )
138
+ collection_state.taskgroupv2_id = None
139
+ db.add(collection_state)
140
+ logger.info("End of CollectionStateV2 cascade operations.")
141
+ task_group = db.get(TaskGroupV2, task_group_id)
142
+ db.delete(task_group)
143
+ db.commit()
144
+ logger.info(f"TaskGroupV2 with {task_group_id=} deleted")
145
+
152
146
  reset_logger_handlers(logger)
153
147
  return
154
148
 
@@ -156,7 +150,6 @@ def _handle_failure(
156
150
  def _prepare_tasks_metadata(
157
151
  *,
158
152
  package_manifest: ManifestV2,
159
- package_source: str,
160
153
  python_bin: Path,
161
154
  package_root: Path,
162
155
  package_version: Optional[str] = None,
@@ -166,7 +159,6 @@ def _prepare_tasks_metadata(
166
159
 
167
160
  Args:
168
161
  package_manifest:
169
- package_source:
170
162
  python_bin:
171
163
  package_root:
172
164
  package_version:
@@ -177,8 +169,6 @@ def _prepare_tasks_metadata(
177
169
  task_attributes = {}
178
170
  if package_version is not None:
179
171
  task_attributes["version"] = package_version
180
- task_name_slug = slugify_task_name_for_source(_task.name)
181
- task_attributes["source"] = f"{package_source}:{task_name_slug}"
182
172
  if package_manifest.has_args_schemas:
183
173
  task_attributes[
184
174
  "args_schema_version"
@@ -203,6 +193,7 @@ def _prepare_tasks_metadata(
203
193
  }
204
194
  ),
205
195
  **task_attributes,
196
+ authors=package_manifest.authors,
206
197
  )
207
198
  task_list.append(task_obj)
208
199
  return task_list
@@ -231,10 +222,95 @@ def _check_task_files_exist(task_list: list[TaskCreateV2]) -> None:
231
222
  )
232
223
 
233
224
 
225
+ async def _download_package(
226
+ *,
227
+ python_version: str,
228
+ pkg_name: str,
229
+ version: str,
230
+ dest: Union[str, Path],
231
+ ) -> Path:
232
+ """
233
+ Download package to destination and return wheel-file path.
234
+ """
235
+ python_bin = get_python_interpreter_v2(python_version=python_version)
236
+ pip = f"{python_bin} -m pip"
237
+ package_and_version = f"{pkg_name}=={version}"
238
+ cmd = f"{pip} download --no-deps {package_and_version} -d {dest}"
239
+ stdout = await execute_command(command=cmd)
240
+ pkg_file = next(
241
+ line.split()[-1] for line in stdout.split("\n") if "Saved" in line
242
+ )
243
+ return Path(pkg_file)
244
+
245
+
246
+ def _load_manifest_from_wheel(
247
+ wheel_file_path: str,
248
+ logger_name: str,
249
+ ) -> ManifestV2:
250
+ """
251
+ Given a wheel file on-disk, extract the Fractal manifest.
252
+ """
253
+ logger = get_logger(logger_name)
254
+
255
+ with ZipFile(wheel_file_path) as wheel:
256
+ namelist = wheel.namelist()
257
+ try:
258
+ manifest = next(
259
+ name
260
+ for name in namelist
261
+ if "__FRACTAL_MANIFEST__.json" in name
262
+ )
263
+ except StopIteration:
264
+ msg = (
265
+ f"{wheel_file_path} does not include __FRACTAL_MANIFEST__.json"
266
+ )
267
+ logger.error(msg)
268
+ raise ValueError(msg)
269
+ with wheel.open(manifest) as manifest_fd:
270
+ manifest_dict = json.load(manifest_fd)
271
+ manifest_version = str(manifest_dict["manifest_version"])
272
+ if manifest_version != "2":
273
+ msg = f"Manifest version {manifest_version=} not supported"
274
+ logger.error(msg)
275
+ raise ValueError(msg)
276
+ pkg_manifest = ManifestV2(**manifest_dict)
277
+ return pkg_manifest
278
+
279
+
280
+ async def _get_package_manifest(
281
+ *,
282
+ task_group: TaskGroupV2,
283
+ logger_name: str,
284
+ ) -> ManifestV2:
285
+ wheel_file_path = task_group.wheel_path
286
+ if wheel_file_path is None:
287
+ with TemporaryDirectory() as tmpdir:
288
+ # Copy or download the package wheel file to tmpdir
289
+ wheel_file_path = await _download_package(
290
+ python_version=task_group.python_version,
291
+ pkg_name=task_group.pkg_name,
292
+ version=task_group.version,
293
+ dest=tmpdir,
294
+ )
295
+ wheel_file_path = wheel_file_path.as_posix()
296
+ # Read package manifest from temporary wheel file
297
+ manifest = _load_manifest_from_wheel(
298
+ wheel_file_path=wheel_file_path,
299
+ logger_name=logger_name,
300
+ )
301
+ else:
302
+ # Read package manifest from wheel file
303
+ manifest = _load_manifest_from_wheel(
304
+ wheel_file_path=wheel_file_path,
305
+ logger_name=logger_name,
306
+ )
307
+ return manifest
308
+
309
+
234
310
  async def background_collect_pip(
311
+ *,
235
312
  state_id: int,
236
- venv_path: Path,
237
- task_pkg: _TaskCollectPip,
313
+ task_group: TaskGroupV2,
238
314
  ) -> None:
239
315
  """
240
316
  Setup venv, install package, collect tasks.
@@ -248,24 +324,48 @@ async def background_collect_pip(
248
324
  5. Handle failures by copying the log into the state and deleting the
249
325
  package directory.
250
326
  """
251
- logger_name = task_pkg.package.replace("/", "_")
327
+ logger_name = (
328
+ f"{task_group.user_id}-{task_group.pkg_name}-{task_group.version}"
329
+ )
330
+
331
+ try:
332
+ Path(task_group.path).mkdir(parents=True, exist_ok=False)
333
+ except FileExistsError as e:
334
+ logger = set_logger(
335
+ logger_name=logger_name,
336
+ log_file_path=get_log_path(Path(task_group.path)),
337
+ )
338
+
339
+ logfile_path = get_log_path(Path(task_group.path))
340
+ with next(get_sync_db()) as db:
341
+ _handle_failure(
342
+ state_id=state_id,
343
+ log_file_path=logfile_path,
344
+ logger_name=logger_name,
345
+ exception=e,
346
+ db=db,
347
+ path=None, # Do not remove an existing path
348
+ task_group_id=task_group.id,
349
+ )
350
+ return
351
+
252
352
  logger = set_logger(
253
353
  logger_name=logger_name,
254
- log_file_path=get_log_path(venv_path),
354
+ log_file_path=get_log_path(Path(task_group.path)),
255
355
  )
256
356
 
257
357
  # Start
258
358
  logger.debug("START")
259
- for key, value in task_pkg.dict(exclude={"package_manifest"}).items():
260
- logger.debug(f"task_pkg.{key}: {value}")
359
+ for key, value in task_group.model_dump().items():
360
+ logger.debug(f"task_group.{key}: {value}")
261
361
 
262
362
  with next(get_sync_db()) as db:
263
-
264
363
  try:
265
- # Block 1: preliminary checks (only proceed if version and
266
- # manifest attributes are set).
267
- # Required: task_pkg
268
- task_pkg.check()
364
+ # Block 1: get and validate manfifest
365
+ pkg_manifest = await _get_package_manifest(
366
+ task_group=task_group,
367
+ logger_name=logger_name,
368
+ )
269
369
 
270
370
  # Block 2: create venv and run pip install
271
371
  # Required: state_id, venv_path, task_pkg
@@ -277,8 +377,7 @@ async def background_collect_pip(
277
377
  db=db,
278
378
  )
279
379
  python_bin, package_root = await _create_venv_install_package_pip(
280
- path=venv_path,
281
- task_pkg=task_pkg,
380
+ task_group=task_group,
282
381
  logger_name=logger_name,
283
382
  )
284
383
  logger.debug("installing - END")
@@ -294,27 +393,38 @@ async def background_collect_pip(
294
393
  )
295
394
  logger.debug("collecting - prepare tasks and update db " "- START")
296
395
  task_list = _prepare_tasks_metadata(
297
- package_manifest=task_pkg.package_manifest,
298
- package_version=task_pkg.package_version,
299
- package_source=task_pkg.package_source,
396
+ package_manifest=pkg_manifest,
397
+ package_version=task_group.version,
300
398
  package_root=package_root,
301
399
  python_bin=python_bin,
302
400
  )
303
401
  _check_task_files_exist(task_list=task_list)
304
- tasks = _insert_tasks(task_list=task_list, db=db)
402
+
403
+ # Prepare some task-group attributes
404
+ task_group = create_db_tasks_and_update_task_group(
405
+ task_list=task_list,
406
+ task_group_id=task_group.id,
407
+ db=db,
408
+ )
409
+
305
410
  logger.debug("collecting - prepare tasks and update db " "- END")
306
411
  logger.debug("collecting - END")
307
412
 
308
413
  # Block 4: finalize (write collection files, write metadata to DB)
309
414
  logger.debug("finalising - START")
310
- collection_path = get_collection_path(venv_path)
415
+ collection_path = get_collection_path(Path(task_group.path))
311
416
  collection_state = db.get(CollectionStateV2, state_id)
312
417
  task_read_list = [
313
- TaskReadV2(**task.model_dump()).dict() for task in tasks
418
+ TaskReadV2(**task.model_dump()).dict()
419
+ for task in task_group.task_list
314
420
  ]
315
421
  collection_state.data["task_list"] = task_read_list
316
- collection_state.data["log"] = get_collection_log(venv_path)
317
- collection_state.data["freeze"] = get_collection_freeze(venv_path)
422
+ collection_state.data["log"] = get_collection_log_v2(
423
+ Path(task_group.path)
424
+ )
425
+ collection_state.data["freeze"] = get_collection_freeze_v2(
426
+ Path(task_group.path)
427
+ )
318
428
  with collection_path.open("w") as f:
319
429
  json.dump(collection_state.data, f, indent=2)
320
430
 
@@ -323,14 +433,15 @@ async def background_collect_pip(
323
433
  logger.debug("finalising - END")
324
434
 
325
435
  except Exception as e:
326
- logfile_path = get_log_path(get_absolute_venv_path(venv_path))
436
+ logfile_path = get_log_path(Path(task_group.path))
327
437
  _handle_failure(
328
438
  state_id=state_id,
329
439
  log_file_path=logfile_path,
330
440
  logger_name=logger_name,
331
441
  exception=e,
332
442
  db=db,
333
- venv_path=venv_path,
443
+ path=task_group.path,
444
+ task_group_id=task_group.id,
334
445
  )
335
446
  return
336
447