fractal-server 2.2.0a0__py3-none-any.whl → 2.3.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 (69) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/db/__init__.py +1 -1
  3. fractal_server/app/models/v1/state.py +1 -2
  4. fractal_server/app/routes/admin/v1.py +2 -2
  5. fractal_server/app/routes/admin/v2.py +2 -2
  6. fractal_server/app/routes/api/v1/job.py +2 -2
  7. fractal_server/app/routes/api/v1/task_collection.py +4 -4
  8. fractal_server/app/routes/api/v2/__init__.py +23 -3
  9. fractal_server/app/routes/api/v2/job.py +2 -2
  10. fractal_server/app/routes/api/v2/submit.py +6 -0
  11. fractal_server/app/routes/api/v2/task_collection.py +74 -34
  12. fractal_server/app/routes/api/v2/task_collection_custom.py +170 -0
  13. fractal_server/app/routes/api/v2/task_collection_ssh.py +125 -0
  14. fractal_server/app/routes/aux/_runner.py +10 -2
  15. fractal_server/app/runner/compress_folder.py +120 -0
  16. fractal_server/app/runner/executors/slurm/__init__.py +0 -3
  17. fractal_server/app/runner/executors/slurm/_batching.py +0 -1
  18. fractal_server/app/runner/executors/slurm/_slurm_config.py +9 -9
  19. fractal_server/app/runner/executors/slurm/ssh/__init__.py +3 -0
  20. fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +112 -0
  21. fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +120 -0
  22. fractal_server/app/runner/executors/slurm/ssh/executor.py +1488 -0
  23. fractal_server/app/runner/executors/slurm/sudo/__init__.py +3 -0
  24. fractal_server/app/runner/executors/slurm/{_check_jobs_status.py → sudo/_check_jobs_status.py} +1 -1
  25. fractal_server/app/runner/executors/slurm/{_executor_wait_thread.py → sudo/_executor_wait_thread.py} +1 -1
  26. fractal_server/app/runner/executors/slurm/{_subprocess_run_as_user.py → sudo/_subprocess_run_as_user.py} +1 -1
  27. fractal_server/app/runner/executors/slurm/{executor.py → sudo/executor.py} +12 -12
  28. fractal_server/app/runner/extract_archive.py +38 -0
  29. fractal_server/app/runner/v1/__init__.py +78 -40
  30. fractal_server/app/runner/v1/_slurm/__init__.py +1 -1
  31. fractal_server/app/runner/v2/__init__.py +183 -82
  32. fractal_server/app/runner/v2/_local_experimental/__init__.py +22 -12
  33. fractal_server/app/runner/v2/_local_experimental/executor.py +12 -8
  34. fractal_server/app/runner/v2/_slurm/__init__.py +1 -6
  35. fractal_server/app/runner/v2/_slurm_ssh/__init__.py +125 -0
  36. fractal_server/app/runner/v2/_slurm_ssh/_submit_setup.py +83 -0
  37. fractal_server/app/runner/v2/_slurm_ssh/get_slurm_config.py +182 -0
  38. fractal_server/app/runner/v2/runner_functions_low_level.py +9 -11
  39. fractal_server/app/runner/versions.py +30 -0
  40. fractal_server/app/schemas/v1/__init__.py +1 -0
  41. fractal_server/app/schemas/{state.py → v1/state.py} +4 -21
  42. fractal_server/app/schemas/v2/__init__.py +4 -1
  43. fractal_server/app/schemas/v2/task_collection.py +101 -30
  44. fractal_server/config.py +222 -21
  45. fractal_server/main.py +27 -1
  46. fractal_server/migrations/env.py +1 -1
  47. fractal_server/ssh/__init__.py +4 -0
  48. fractal_server/ssh/_fabric.py +245 -0
  49. fractal_server/tasks/utils.py +12 -64
  50. fractal_server/tasks/v1/background_operations.py +2 -2
  51. fractal_server/tasks/{endpoint_operations.py → v1/endpoint_operations.py} +7 -12
  52. fractal_server/tasks/v1/utils.py +67 -0
  53. fractal_server/tasks/v2/_TaskCollectPip.py +61 -32
  54. fractal_server/tasks/v2/_venv_pip.py +195 -0
  55. fractal_server/tasks/v2/background_operations.py +257 -295
  56. fractal_server/tasks/v2/background_operations_ssh.py +317 -0
  57. fractal_server/tasks/v2/endpoint_operations.py +136 -0
  58. fractal_server/tasks/v2/templates/_1_create_venv.sh +46 -0
  59. fractal_server/tasks/v2/templates/_2_upgrade_pip.sh +30 -0
  60. fractal_server/tasks/v2/templates/_3_pip_install.sh +32 -0
  61. fractal_server/tasks/v2/templates/_4_pip_freeze.sh +21 -0
  62. fractal_server/tasks/v2/templates/_5_pip_show.sh +59 -0
  63. fractal_server/tasks/v2/utils.py +54 -0
  64. {fractal_server-2.2.0a0.dist-info → fractal_server-2.3.0.dist-info}/METADATA +6 -2
  65. {fractal_server-2.2.0a0.dist-info → fractal_server-2.3.0.dist-info}/RECORD +68 -44
  66. fractal_server/tasks/v2/get_collection_data.py +0 -14
  67. {fractal_server-2.2.0a0.dist-info → fractal_server-2.3.0.dist-info}/LICENSE +0 -0
  68. {fractal_server-2.2.0a0.dist-info → fractal_server-2.3.0.dist-info}/WHEEL +0 -0
  69. {fractal_server-2.2.0a0.dist-info → fractal_server-2.3.0.dist-info}/entry_points.txt +0 -0
@@ -5,288 +5,230 @@ 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 typing import Optional
8
9
 
9
- from ..utils import _init_venv
10
- from ..utils import _normalize_package_name
10
+ from sqlalchemy.orm import Session as DBSyncSession
11
+ from sqlalchemy.orm.attributes import flag_modified
12
+
13
+ from ..utils import get_absolute_venv_path
14
+ from ..utils import get_collection_freeze
11
15
  from ..utils import get_collection_log
12
16
  from ..utils import get_collection_path
13
17
  from ..utils import get_log_path
14
18
  from ..utils import slugify_task_name
15
19
  from ._TaskCollectPip import _TaskCollectPip
16
- from fractal_server.app.db import DBSyncSession
17
20
  from fractal_server.app.db import get_sync_db
18
21
  from fractal_server.app.models.v2 import CollectionStateV2
19
22
  from fractal_server.app.models.v2 import TaskV2
20
- from fractal_server.app.schemas.v2 import TaskCollectStatusV2
23
+ from fractal_server.app.schemas.v2 import CollectionStatusV2
21
24
  from fractal_server.app.schemas.v2 import TaskCreateV2
22
25
  from fractal_server.app.schemas.v2 import TaskReadV2
26
+ from fractal_server.app.schemas.v2.manifest import ManifestV2
23
27
  from fractal_server.logger import get_logger
24
28
  from fractal_server.logger import reset_logger_handlers
25
29
  from fractal_server.logger import set_logger
26
- from fractal_server.utils import execute_command
30
+ from fractal_server.tasks.v2._venv_pip import _create_venv_install_package_pip
27
31
 
28
32
 
29
- async def _pip_install(
30
- venv_path: Path,
31
- task_pkg: _TaskCollectPip,
32
- logger_name: str,
33
- ) -> Path:
34
- """
35
- Install package in venv
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"
36
40
 
37
- Args:
38
- venv_path:
39
- task_pkg:
40
- logger_name:
41
41
 
42
- Returns:
43
- The location of the package.
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
44
49
  """
45
50
 
46
- logger = get_logger(logger_name)
47
-
48
- pip = venv_path / "venv/bin/pip"
49
-
50
- extras = f"[{task_pkg.package_extras}]" if task_pkg.package_extras else ""
51
+ owner_dict = dict(owner=owner) if owner is not None else dict()
51
52
 
52
- if task_pkg.is_local_package:
53
- pip_install_str = f"{task_pkg.package_path.as_posix()}{extras}"
54
- else:
55
- version_string = (
56
- f"=={task_pkg.package_version}" if task_pkg.package_version else ""
57
- )
58
- pip_install_str = f"{task_pkg.package}{extras}{version_string}"
59
-
60
- cmd_install = f"{pip} install {pip_install_str}"
61
- cmd_inspect = f"{pip} show {task_pkg.package}"
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
62
63
 
63
- await execute_command(
64
- cwd=venv_path,
65
- command=f"{pip} install --upgrade pip",
66
- logger_name=logger_name,
67
- )
68
- await execute_command(
69
- cwd=venv_path, command=cmd_install, logger_name=logger_name
70
- )
71
- if task_pkg.pinned_package_versions:
72
- for (
73
- pinned_pkg_name,
74
- pinned_pkg_version,
75
- ) in task_pkg.pinned_package_versions.items():
76
-
77
- logger.debug(
78
- "Specific version required: "
79
- f"{pinned_pkg_name}=={pinned_pkg_version}"
80
- )
81
- logger.debug(
82
- "Preliminary check: verify that "
83
- f"{pinned_pkg_version} is already installed"
84
- )
85
- stdout_inspect = await execute_command(
86
- cwd=venv_path,
87
- command=f"{pip} show {pinned_pkg_name}",
88
- logger_name=logger_name,
89
- )
90
- current_version = next(
91
- line.split()[-1]
92
- for line in stdout_inspect.split("\n")
93
- if line.startswith("Version:")
94
- )
95
- if current_version != pinned_pkg_version:
96
- logger.debug(
97
- f"Currently installed version of {pinned_pkg_name} "
98
- f"({current_version}) differs from pinned version "
99
- f"({pinned_pkg_version}); "
100
- f"install version {pinned_pkg_version}."
101
- )
102
- await execute_command(
103
- cwd=venv_path,
104
- command=(
105
- f"{pip} install "
106
- f"{pinned_pkg_name}=={pinned_pkg_version}"
107
- ),
108
- logger_name=logger_name,
109
- )
110
- else:
111
- logger.debug(
112
- f"Currently installed version of {pinned_pkg_name} "
113
- f"({current_version}) already matches the pinned version."
114
- )
115
64
 
116
- # Extract package installation path from `pip show`
117
- stdout_inspect = await execute_command(
118
- cwd=venv_path, command=cmd_inspect, logger_name=logger_name
119
- )
65
+ def _set_collection_state_data_status(
66
+ *,
67
+ state_id: int,
68
+ new_status: CollectionStatusV2,
69
+ logger_name: str,
70
+ db: DBSyncSession,
71
+ ):
72
+ logger = get_logger(logger_name)
73
+ logger.debug(f"{state_id=} - set state.data['status'] to {new_status}")
74
+ collection_state = db.get(CollectionStateV2, state_id)
75
+ collection_state.data["status"] = CollectionStatusV2(new_status)
76
+ flag_modified(collection_state, "data")
77
+ db.commit()
120
78
 
121
- location = Path(
122
- next(
123
- line.split()[-1]
124
- for line in stdout_inspect.split("\n")
125
- if line.startswith("Location:")
126
- )
127
- )
128
79
 
129
- # NOTE
130
- # https://packaging.python.org/en/latest/specifications/recording-installed-packages/
131
- # This directory is named as {name}-{version}.dist-info, with name and
132
- # version fields corresponding to Core metadata specifications. Both
133
- # fields must be normalized (see the name normalization specification and
134
- # the version normalization specification), and replace dash (-)
135
- # characters with underscore (_) characters, so the .dist-info directory
136
- # always has exactly one dash (-) character in its stem, separating the
137
- # name and version fields.
138
- package_root = location / (task_pkg.package.replace("-", "_"))
139
- logger.debug(f"[_pip install] {location=}")
140
- logger.debug(f"[_pip install] {task_pkg.package=}")
141
- logger.debug(f"[_pip install] {package_root=}")
142
- if not package_root.exists():
143
- raise RuntimeError(
144
- "Could not determine package installation location."
145
- )
146
- return package_root
80
+ def _set_collection_state_data_log(
81
+ *,
82
+ state_id: int,
83
+ new_log: str,
84
+ logger_name: str,
85
+ db: DBSyncSession,
86
+ ):
87
+ logger = get_logger(logger_name)
88
+ logger.debug(f"{state_id=} - set state.data['log']")
89
+ collection_state = db.get(CollectionStateV2, state_id)
90
+ collection_state.data["log"] = new_log
91
+ flag_modified(collection_state, "data")
92
+ db.commit()
147
93
 
148
94
 
149
- async def _create_venv_install_package(
95
+ def _set_collection_state_data_info(
150
96
  *,
151
- task_pkg: _TaskCollectPip,
152
- path: Path,
97
+ state_id: int,
98
+ new_info: str,
153
99
  logger_name: str,
154
- ) -> tuple[Path, Path]:
155
- """Create venv and install package
100
+ db: DBSyncSession,
101
+ ):
102
+ logger = get_logger(logger_name)
103
+ logger.debug(f"{state_id=} - set state.data['info']")
104
+ collection_state = db.get(CollectionStateV2, state_id)
105
+ collection_state.data["info"] = new_info
106
+ flag_modified(collection_state, "data")
107
+ db.commit()
156
108
 
157
- Args:
158
- path: the directory in which to create the environment
159
- task_pkg: object containing the different metadata required to install
160
- the package
161
109
 
162
- Returns:
163
- python_bin: path to venv's python interpreter
164
- package_root: the location of the package manifest
110
+ def _handle_failure(
111
+ state_id: int,
112
+ log_file_path: Path,
113
+ logger_name: str,
114
+ exception: Exception,
115
+ db: DBSyncSession,
116
+ venv_path: Optional[Path] = None,
117
+ ):
118
+ """
119
+ Note: `venv_path` is only required to trigger the folder deletion.
165
120
  """
166
121
 
167
- # Normalize package name
168
- task_pkg.package_name = _normalize_package_name(task_pkg.package_name)
169
- task_pkg.package = _normalize_package_name(task_pkg.package)
122
+ logger = get_logger(logger_name)
123
+ logger.error(f"Task collection failed. Original error: {str(exception)}")
170
124
 
171
- python_bin = await _init_venv(
172
- path=path,
173
- python_version=task_pkg.python_version,
125
+ _set_collection_state_data_status(
126
+ state_id=state_id,
127
+ new_status=CollectionStatusV2.FAIL,
174
128
  logger_name=logger_name,
129
+ db=db,
175
130
  )
176
- package_root = await _pip_install(
177
- venv_path=path, task_pkg=task_pkg, logger_name=logger_name
131
+
132
+ new_log = log_file_path.open().read()
133
+ _set_collection_state_data_log(
134
+ state_id=state_id,
135
+ new_log=new_log,
136
+ logger_name=logger_name,
137
+ db=db,
178
138
  )
179
- return python_bin, package_root
139
+ # For backwards-compatibility, we also set state.data["info"]
140
+ _set_collection_state_data_info(
141
+ state_id=state_id,
142
+ new_info=f"Original error: {exception}",
143
+ logger_name=logger_name,
144
+ db=db,
145
+ )
146
+ # 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)
150
+ logger.info("Temporary folder deleted")
151
+
152
+ reset_logger_handlers(logger)
153
+ return
180
154
 
181
155
 
182
- async def create_package_environment_pip(
156
+ def _prepare_tasks_metadata(
183
157
  *,
184
- task_pkg: _TaskCollectPip,
185
- venv_path: Path,
186
- logger_name: str,
158
+ package_manifest: ManifestV2,
159
+ package_source: str,
160
+ python_bin: Path,
161
+ package_root: Path,
162
+ package_version: Optional[str] = None,
187
163
  ) -> list[TaskCreateV2]:
188
164
  """
189
- Create environment, install package, and prepare task list
190
- """
191
-
192
- logger = get_logger(logger_name)
165
+ Based on the package manifest and additional info, prepare the task list.
193
166
 
194
- # Normalize package name
195
- task_pkg.package_name = _normalize_package_name(task_pkg.package_name)
196
- task_pkg.package = _normalize_package_name(task_pkg.package)
197
-
198
- # Only proceed if package, version and manifest attributes are set
199
- task_pkg.check()
200
-
201
- try:
202
-
203
- logger.debug("Creating venv and installing package")
204
- python_bin, package_root = await _create_venv_install_package(
205
- path=venv_path,
206
- task_pkg=task_pkg,
207
- logger_name=logger_name,
208
- )
209
- logger.debug("Venv creation and package installation ended correctly.")
210
-
211
- # Prepare task_list with appropriate metadata
212
- logger.debug("Creating task list from manifest")
213
- task_list = []
214
- for t in task_pkg.package_manifest.task_list:
215
- # Fill in attributes for TaskCreate
216
- task_attributes = {}
217
- task_attributes["version"] = task_pkg.package_version
218
- task_name_slug = slugify_task_name(t.name)
167
+ Args:
168
+ package_manifest:
169
+ package_source:
170
+ python_bin:
171
+ package_root:
172
+ package_version:
173
+ """
174
+ task_list = []
175
+ for _task in package_manifest.task_list:
176
+ # Set non-command attributes
177
+ task_attributes = {}
178
+ if package_version is not None:
179
+ task_attributes["version"] = package_version
180
+ task_name_slug = slugify_task_name(_task.name)
181
+ task_attributes["source"] = f"{package_source}:{task_name_slug}"
182
+ if package_manifest.has_args_schemas:
219
183
  task_attributes[
220
- "source"
221
- ] = f"{task_pkg.package_source}:{task_name_slug}"
222
- # Executables
223
- if t.executable_non_parallel is not None:
224
- non_parallel_path = package_root / t.executable_non_parallel
225
- if not non_parallel_path.exists():
226
- raise FileNotFoundError(
227
- f"Cannot find executable `{non_parallel_path}` "
228
- f"for task `{t.name}`"
229
- )
230
- task_attributes[
231
- "command_non_parallel"
232
- ] = f"{python_bin.as_posix()} {non_parallel_path.as_posix()}"
233
- if t.executable_parallel is not None:
234
- parallel_path = package_root / t.executable_parallel
235
- if not parallel_path.exists():
236
- raise FileNotFoundError(
237
- f"Cannot find executable `{parallel_path}` "
238
- f"for task `{t.name}`"
239
- )
240
- task_attributes[
241
- "command_parallel"
242
- ] = f"{python_bin.as_posix()} {parallel_path.as_posix()}"
243
-
244
- manifest = task_pkg.package_manifest
245
- if manifest.has_args_schemas:
246
- task_attributes[
247
- "args_schema_version"
248
- ] = manifest.args_schema_version
249
-
250
- this_task = TaskCreateV2(
251
- **t.dict(
252
- exclude={"executable_non_parallel", "executable_parallel"}
253
- ),
254
- **task_attributes,
184
+ "args_schema_version"
185
+ ] = package_manifest.args_schema_version
186
+ # Set command attributes
187
+ if _task.executable_non_parallel is not None:
188
+ non_parallel_path = package_root / _task.executable_non_parallel
189
+ task_attributes["command_non_parallel"] = (
190
+ f"{python_bin.as_posix()} " f"{non_parallel_path.as_posix()}"
255
191
  )
256
- task_list.append(this_task)
257
- logger.debug("Task list created correctly")
258
- except Exception as e:
259
- logger.error("Task manifest loading failed")
260
- raise e
192
+ if _task.executable_parallel is not None:
193
+ parallel_path = package_root / _task.executable_parallel
194
+ task_attributes[
195
+ "command_parallel"
196
+ ] = f"{python_bin.as_posix()} {parallel_path.as_posix()}"
197
+ # Create object
198
+ task_obj = TaskCreateV2(
199
+ **_task.dict(
200
+ exclude={
201
+ "executable_non_parallel",
202
+ "executable_parallel",
203
+ }
204
+ ),
205
+ **task_attributes,
206
+ )
207
+ task_list.append(task_obj)
261
208
  return task_list
262
209
 
263
210
 
264
- def _get_task_type(task: TaskCreateV2) -> str:
265
- if task.command_non_parallel is None:
266
- return "parallel"
267
- elif task.command_parallel is None:
268
- return "non_parallel"
269
- else:
270
- return "compound"
271
-
272
-
273
- async def _insert_tasks(
274
- task_list: list[TaskCreateV2],
275
- db: DBSyncSession,
276
- ) -> list[TaskV2]:
277
- """
278
- Insert tasks into database
211
+ def _check_task_files_exist(task_list: list[TaskCreateV2]) -> None:
279
212
  """
213
+ Check that the modules listed in task commands point to existing files.
280
214
 
281
- task_db_list = [
282
- TaskV2(**t.dict(), type=_get_task_type(t)) for t in task_list
283
- ]
284
- db.add_all(task_db_list)
285
- db.commit()
286
- for t in task_db_list:
287
- db.refresh(t)
288
- db.close()
289
- return task_db_list
215
+ Args: task_list
216
+ """
217
+ for _task in task_list:
218
+ if _task.command_non_parallel is not None:
219
+ _task_path = _task.command_non_parallel.split()[1]
220
+ if not Path(_task_path).exists():
221
+ raise FileNotFoundError(
222
+ f"Task `{_task.name}` has `command_non_parallel` "
223
+ f"pointing to missing file `{_task_path}`."
224
+ )
225
+ if _task.command_parallel is not None:
226
+ _task_path = _task.command_parallel.split()[1]
227
+ if not Path(_task_path).exists():
228
+ raise FileNotFoundError(
229
+ f"Task `{_task.name}` has `command_parallel` "
230
+ f"pointing to missing file `{_task_path}`."
231
+ )
290
232
 
291
233
 
292
234
  async def background_collect_pip(
@@ -295,89 +237,109 @@ async def background_collect_pip(
295
237
  task_pkg: _TaskCollectPip,
296
238
  ) -> None:
297
239
  """
298
- Install package and collect tasks
299
-
300
- Install a python package and collect the tasks it provides according to
301
- the manifest.
302
-
303
- In case of error, copy the log into the state and delete the package
304
- directory.
240
+ Setup venv, install package, collect tasks.
241
+
242
+ This function (executed as background task), includes the several steps
243
+ associated to automated collection of a Python task package.
244
+ 1. Preliminary checks
245
+ 2. Create venv and run `pip install`
246
+ 3. Collect tasks into db
247
+ 4. Finalize things.
248
+ 5. Handle failures by copying the log into the state and deleting the
249
+ package directory.
305
250
  """
306
251
  logger_name = task_pkg.package.replace("/", "_")
307
252
  logger = set_logger(
308
253
  logger_name=logger_name,
309
254
  log_file_path=get_log_path(venv_path),
310
255
  )
311
- logger.debug("Start background task collection")
256
+
257
+ # Start
258
+ logger.debug("START")
312
259
  for key, value in task_pkg.dict(exclude={"package_manifest"}).items():
313
- logger.debug(f"{key}: {value}")
260
+ logger.debug(f"task_pkg.{key}: {value}")
314
261
 
315
262
  with next(get_sync_db()) as db:
316
- state: CollectionStateV2 = db.get(CollectionStateV2, state_id)
317
- data = TaskCollectStatusV2(**state.data)
318
- data.info = None
319
263
 
320
264
  try:
321
- # install
322
- logger.debug("Task-collection status: installing")
323
- data.status = "installing"
324
-
325
- state.data = data.sanitised_dict()
326
- db.merge(state)
327
- db.commit()
328
- task_list = await create_package_environment_pip(
329
- venv_path=venv_path,
265
+ # Block 1: preliminary checks (only proceed if version and
266
+ # manifest attributes are set).
267
+ # Required: task_pkg
268
+ task_pkg.check()
269
+
270
+ # Block 2: create venv and run pip install
271
+ # Required: state_id, venv_path, task_pkg
272
+ logger.debug("installing - START")
273
+ _set_collection_state_data_status(
274
+ state_id=state_id,
275
+ new_status=CollectionStatusV2.INSTALLING,
276
+ logger_name=logger_name,
277
+ db=db,
278
+ )
279
+ python_bin, package_root = await _create_venv_install_package_pip(
280
+ path=venv_path,
330
281
  task_pkg=task_pkg,
331
282
  logger_name=logger_name,
332
283
  )
284
+ logger.debug("installing - END")
285
+
286
+ # Block 3: create task metadata and create database entries
287
+ # Required: state_id, python_bin, package_root, task_pkg
288
+ logger.debug("collecting - START")
289
+ _set_collection_state_data_status(
290
+ state_id=state_id,
291
+ new_status=CollectionStatusV2.COLLECTING,
292
+ logger_name=logger_name,
293
+ db=db,
294
+ )
295
+ logger.debug("collecting - prepare tasks and update db " "- START")
296
+ 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,
300
+ package_root=package_root,
301
+ python_bin=python_bin,
302
+ )
303
+ _check_task_files_exist(task_list=task_list)
304
+ tasks = _insert_tasks(task_list=task_list, db=db)
305
+ logger.debug("collecting - prepare tasks and update db " "- END")
306
+ logger.debug("collecting - END")
333
307
 
334
- # collect
335
- logger.debug("Task-collection status: collecting")
336
- data.status = "collecting"
337
- state.data = data.sanitised_dict()
338
- db.merge(state)
339
- db.commit()
340
- tasks = await _insert_tasks(task_list=task_list, db=db)
341
-
342
- # finalise
343
- logger.debug("Task-collection status: finalising")
308
+ # Block 4: finalize (write collection files, write metadata to DB)
309
+ logger.debug("finalising - START")
344
310
  collection_path = get_collection_path(venv_path)
345
- data.task_list = [
346
- TaskReadV2(**task.model_dump()) for task in tasks
311
+ collection_state = db.get(CollectionStateV2, state_id)
312
+ task_read_list = [
313
+ TaskReadV2(**task.model_dump()).dict() for task in tasks
347
314
  ]
315
+ 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)
348
318
  with collection_path.open("w") as f:
349
- json.dump(data.sanitised_dict(), f, indent=2)
319
+ json.dump(collection_state.data, f, indent=2)
350
320
 
351
- # Update DB
352
- data.status = "OK"
353
- data.log = get_collection_log(venv_path)
354
- state.data = data.sanitised_dict()
355
- db.merge(state)
321
+ flag_modified(collection_state, "data")
356
322
  db.commit()
357
-
358
- # Write last logs to file
359
- logger.debug("Task-collection status: OK")
360
- logger.info("Background task collection completed successfully")
361
- reset_logger_handlers(logger)
362
-
363
- db.close()
323
+ logger.debug("finalising - END")
364
324
 
365
325
  except Exception as e:
366
- # Write last logs to file
367
- logger.debug("Task-collection status: fail")
368
- logger.info(f"Background collection failed. Original error: {e}")
369
-
370
- # Update db
371
- data.status = "fail"
372
- data.info = f"Original error: {e}"
373
- data.log = get_collection_log(venv_path)
374
- state.data = data.sanitised_dict()
375
- db.merge(state)
376
- db.commit()
377
- db.close()
326
+ logfile_path = get_log_path(get_absolute_venv_path(venv_path))
327
+ _handle_failure(
328
+ state_id=state_id,
329
+ log_file_path=logfile_path,
330
+ logger_name=logger_name,
331
+ exception=e,
332
+ db=db,
333
+ venv_path=venv_path,
334
+ )
335
+ return
378
336
 
379
- # Delete corrupted package dir
380
- logger.info(f"Now deleting temporary folder {venv_path}")
381
- shell_rmtree(venv_path)
382
- logger.info("Temporary folder deleted")
383
- reset_logger_handlers(logger)
337
+ logger.debug("Task-collection status: OK")
338
+ logger.info("Background task collection completed successfully")
339
+ _set_collection_state_data_status(
340
+ state_id=state_id,
341
+ new_status=CollectionStatusV2.OK,
342
+ logger_name=logger_name,
343
+ db=db,
344
+ )
345
+ reset_logger_handlers(logger)