fractal-server 2.7.0a11__py3-none-any.whl → 2.8.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 (41) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/user_settings.py +1 -0
  3. fractal_server/app/models/v2/task.py +16 -2
  4. fractal_server/app/routes/admin/v2/task_group.py +7 -0
  5. fractal_server/app/routes/api/v2/dataset.py +39 -6
  6. fractal_server/app/routes/api/v2/task.py +4 -6
  7. fractal_server/app/routes/api/v2/task_collection.py +17 -44
  8. fractal_server/app/routes/api/v2/task_collection_custom.py +5 -4
  9. fractal_server/app/schemas/user_settings.py +18 -0
  10. fractal_server/app/schemas/v2/__init__.py +1 -0
  11. fractal_server/app/schemas/v2/dataset.py +5 -3
  12. fractal_server/app/schemas/v2/task_collection.py +20 -4
  13. fractal_server/app/schemas/v2/task_group.py +8 -1
  14. fractal_server/app/security/__init__.py +8 -1
  15. fractal_server/config.py +8 -28
  16. fractal_server/migrations/versions/19eca0dd47a9_user_settings_project_dir.py +39 -0
  17. fractal_server/migrations/versions/8e8f227a3e36_update_taskv2_post_2_7_0.py +42 -0
  18. fractal_server/tasks/utils.py +0 -31
  19. fractal_server/tasks/v1/background_operations.py +11 -11
  20. fractal_server/tasks/v1/endpoint_operations.py +5 -5
  21. fractal_server/tasks/v1/utils.py +2 -2
  22. fractal_server/tasks/v2/collection_local.py +357 -0
  23. fractal_server/tasks/v2/{background_operations_ssh.py → collection_ssh.py} +108 -102
  24. fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -8
  25. fractal_server/tasks/v2/templates/{_2_upgrade_pip.sh → _2_preliminary_pip_operations.sh} +2 -1
  26. fractal_server/tasks/v2/templates/_3_pip_install.sh +22 -1
  27. fractal_server/tasks/v2/templates/_5_pip_show.sh +5 -5
  28. fractal_server/tasks/v2/utils_background.py +209 -0
  29. fractal_server/tasks/v2/utils_package_names.py +77 -0
  30. fractal_server/tasks/v2/{utils.py → utils_python_interpreter.py} +0 -26
  31. fractal_server/tasks/v2/utils_templates.py +59 -0
  32. fractal_server/utils.py +48 -3
  33. {fractal_server-2.7.0a11.dist-info → fractal_server-2.8.0.dist-info}/METADATA +14 -17
  34. {fractal_server-2.7.0a11.dist-info → fractal_server-2.8.0.dist-info}/RECORD +38 -35
  35. fractal_server/data_migrations/2_7_0.py +0 -323
  36. fractal_server/tasks/v2/_venv_pip.py +0 -193
  37. fractal_server/tasks/v2/background_operations.py +0 -456
  38. /fractal_server/{tasks/v2/endpoint_operations.py → app/routes/api/v2/_aux_functions_task_collection.py} +0 -0
  39. {fractal_server-2.7.0a11.dist-info → fractal_server-2.8.0.dist-info}/LICENSE +0 -0
  40. {fractal_server-2.7.0a11.dist-info → fractal_server-2.8.0.dist-info}/WHEEL +0 -0
  41. {fractal_server-2.7.0a11.dist-info → fractal_server-2.8.0.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,3 @@
1
- import re
2
1
  from pathlib import Path
3
2
 
4
3
  from fractal_server.config import get_settings
@@ -42,38 +41,8 @@ def get_collection_log_v1(path: Path) -> str:
42
41
  return log
43
42
 
44
43
 
45
- def get_collection_log_v2(path: Path) -> str:
46
- log_path = get_log_path(path)
47
- log = log_path.open().read()
48
- return log
49
-
50
-
51
44
  def get_collection_freeze_v1(venv_path: Path) -> str:
52
45
  package_path = get_absolute_venv_path_v1(venv_path)
53
46
  freeze_path = get_freeze_path(package_path)
54
47
  freeze = freeze_path.open().read()
55
48
  return freeze
56
-
57
-
58
- def get_collection_freeze_v2(path: Path) -> str:
59
- freeze_path = get_freeze_path(path)
60
- freeze = freeze_path.open().read()
61
- return freeze
62
-
63
-
64
- def _normalize_package_name(name: str) -> str:
65
- """
66
- Implement PyPa specifications for package-name normalization
67
-
68
- The name should be lowercased with all runs of the characters `.`, `-`,
69
- or `_` replaced with a single `-` character. This can be implemented in
70
- Python with the re module.
71
- (https://packaging.python.org/en/latest/specifications/name-normalization)
72
-
73
- Args:
74
- name: The non-normalized package name.
75
-
76
- Returns:
77
- The normalized package name.
78
- """
79
- return re.sub(r"[-_.]+", "-", name).lower()
@@ -7,10 +7,10 @@ from pathlib import Path
7
7
  from shutil import rmtree as shell_rmtree
8
8
 
9
9
  from ...string_tools import slugify_task_name_for_source_v1
10
- from ..utils import _normalize_package_name
11
10
  from ..utils import get_collection_log_v1
12
11
  from ..utils import get_collection_path
13
12
  from ..utils import get_log_path
13
+ from ..v2.utils_package_names import normalize_package_name
14
14
  from ._TaskCollectPip import _TaskCollectPip
15
15
  from .utils import _init_venv_v1
16
16
  from fractal_server.app.db import DBSyncSession
@@ -23,7 +23,7 @@ from fractal_server.app.schemas.v1 import TaskReadV1
23
23
  from fractal_server.logger import close_logger
24
24
  from fractal_server.logger import get_logger
25
25
  from fractal_server.logger import set_logger
26
- from fractal_server.utils import execute_command
26
+ from fractal_server.utils import execute_command_async
27
27
 
28
28
 
29
29
  async def _pip_install(
@@ -60,12 +60,12 @@ async def _pip_install(
60
60
  cmd_install = f"{pip} install {pip_install_str}"
61
61
  cmd_inspect = f"{pip} show {task_pkg.package}"
62
62
 
63
- await execute_command(
63
+ await execute_command_async(
64
64
  cwd=venv_path,
65
65
  command=f"{pip} install --upgrade pip",
66
66
  logger_name=logger_name,
67
67
  )
68
- await execute_command(
68
+ await execute_command_async(
69
69
  cwd=venv_path, command=cmd_install, logger_name=logger_name
70
70
  )
71
71
  if task_pkg.pinned_package_versions:
@@ -82,7 +82,7 @@ async def _pip_install(
82
82
  "Preliminary check: verify that "
83
83
  f"{pinned_pkg_version} is already installed"
84
84
  )
85
- stdout_inspect = await execute_command(
85
+ stdout_inspect = await execute_command_async(
86
86
  cwd=venv_path,
87
87
  command=f"{pip} show {pinned_pkg_name}",
88
88
  logger_name=logger_name,
@@ -99,7 +99,7 @@ async def _pip_install(
99
99
  f"({pinned_pkg_version}); "
100
100
  f"install version {pinned_pkg_version}."
101
101
  )
102
- await execute_command(
102
+ await execute_command_async(
103
103
  cwd=venv_path,
104
104
  command=(
105
105
  f"{pip} install "
@@ -114,7 +114,7 @@ async def _pip_install(
114
114
  )
115
115
 
116
116
  # Extract package installation path from `pip show`
117
- stdout_inspect = await execute_command(
117
+ stdout_inspect = await execute_command_async(
118
118
  cwd=venv_path, command=cmd_inspect, logger_name=logger_name
119
119
  )
120
120
 
@@ -165,8 +165,8 @@ async def _create_venv_install_package(
165
165
  """
166
166
 
167
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)
168
+ task_pkg.package_name = normalize_package_name(task_pkg.package_name)
169
+ task_pkg.package = normalize_package_name(task_pkg.package)
170
170
 
171
171
  python_bin = await _init_venv_v1(
172
172
  path=path,
@@ -192,8 +192,8 @@ async def create_package_environment_pip(
192
192
  logger = get_logger(logger_name)
193
193
 
194
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)
195
+ task_pkg.package_name = normalize_package_name(task_pkg.package_name)
196
+ task_pkg.package = normalize_package_name(task_pkg.package)
197
197
 
198
198
  # Only proceed if package, version and manifest attributes are set
199
199
  task_pkg.check()
@@ -4,14 +4,14 @@ from typing import Optional
4
4
  from typing import Union
5
5
  from zipfile import ZipFile
6
6
 
7
- from ..utils import _normalize_package_name
7
+ from ..v2.utils_package_names import normalize_package_name
8
8
  from ._TaskCollectPip import _TaskCollectPip as _TaskCollectPipV1
9
9
  from .utils import get_python_interpreter_v1
10
10
  from fractal_server.app.schemas.v1 import ManifestV1
11
11
  from fractal_server.config import get_settings
12
12
  from fractal_server.logger import get_logger
13
13
  from fractal_server.syringe import Inject
14
- from fractal_server.utils import execute_command
14
+ from fractal_server.utils import execute_command_async
15
15
 
16
16
 
17
17
  FRACTAL_PUBLIC_TASK_SUBDIR = ".fractal"
@@ -32,7 +32,7 @@ async def download_package(
32
32
  )
33
33
  package_and_version = f"{task_pkg.package}{version}"
34
34
  cmd = f"{pip} download --no-deps {package_and_version} -d {dest}"
35
- stdout = await execute_command(command=cmd, cwd=Path("."))
35
+ stdout = await execute_command_async(command=cmd, cwd=Path("."))
36
36
  pkg_file = next(
37
37
  line.split()[-1] for line in stdout.split("\n") if "Saved" in line
38
38
  )
@@ -123,7 +123,7 @@ def inspect_package(path: Path, logger_name: Optional[str] = None) -> dict:
123
123
  logger.debug("Package name and version read correctly.")
124
124
 
125
125
  # Normalize package name:
126
- pkg_name = _normalize_package_name(pkg_name)
126
+ pkg_name = normalize_package_name(pkg_name)
127
127
 
128
128
  info = dict(
129
129
  pkg_name=pkg_name,
@@ -148,7 +148,7 @@ def create_package_dir_pip(
148
148
  f"Cannot create venv folder for package `{task_pkg.package}` "
149
149
  "with `version=None`."
150
150
  )
151
- normalized_package = _normalize_package_name(task_pkg.package)
151
+ normalized_package = normalize_package_name(task_pkg.package)
152
152
  package_dir = f"{normalized_package}{task_pkg.package_version}"
153
153
  venv_path = settings.FRACTAL_TASKS_DIR / user / package_dir
154
154
  if create:
@@ -2,7 +2,7 @@ from pathlib import Path
2
2
  from typing import Optional
3
3
 
4
4
  from fractal_server.logger import get_logger
5
- from fractal_server.utils import execute_command
5
+ from fractal_server.utils import execute_command_async
6
6
 
7
7
 
8
8
  def get_python_interpreter_v1(version: Optional[str] = None) -> str:
@@ -57,7 +57,7 @@ async def _init_venv_v1(
57
57
  logger.debug(f"[_init_venv] {path=}")
58
58
  interpreter = get_python_interpreter_v1(version=python_version)
59
59
  logger.debug(f"[_init_venv] {interpreter=}")
60
- await execute_command(
60
+ await execute_command_async(
61
61
  cwd=path,
62
62
  command=f"{interpreter} -m venv venv",
63
63
  logger_name=logger_name,
@@ -0,0 +1,357 @@
1
+ import json
2
+ import shutil
3
+ from pathlib import Path
4
+ from tempfile import TemporaryDirectory
5
+
6
+ from sqlalchemy.orm.attributes import flag_modified
7
+
8
+ from .database_operations import create_db_tasks_and_update_task_group
9
+ from fractal_server.app.db import get_sync_db
10
+ from fractal_server.app.models.v2 import CollectionStateV2
11
+ from fractal_server.app.models.v2 import TaskGroupV2
12
+ from fractal_server.app.schemas.v2 import CollectionStatusV2
13
+ from fractal_server.app.schemas.v2 import TaskReadV2
14
+ from fractal_server.app.schemas.v2.manifest import ManifestV2
15
+ from fractal_server.config import get_settings
16
+ from fractal_server.logger import get_logger
17
+ from fractal_server.logger import set_logger
18
+ from fractal_server.syringe import Inject
19
+ from fractal_server.tasks.utils import get_log_path
20
+ from fractal_server.tasks.v2.utils_background import _handle_failure
21
+ from fractal_server.tasks.v2.utils_background import _prepare_tasks_metadata
22
+ from fractal_server.tasks.v2.utils_background import _refresh_logs
23
+ from fractal_server.tasks.v2.utils_background import (
24
+ _set_collection_state_data_status,
25
+ )
26
+ from fractal_server.tasks.v2.utils_background import check_task_files_exist
27
+ from fractal_server.tasks.v2.utils_package_names import compare_package_names
28
+ from fractal_server.tasks.v2.utils_python_interpreter import (
29
+ get_python_interpreter_v2,
30
+ )
31
+ from fractal_server.tasks.v2.utils_templates import customize_template
32
+ from fractal_server.tasks.v2.utils_templates import parse_script_5_stdout
33
+ from fractal_server.utils import execute_command_sync
34
+
35
+
36
+ def _customize_and_run_template(
37
+ script_filename: str,
38
+ replacements: list[tuple[str, str]],
39
+ script_dir: str,
40
+ logger_name: str,
41
+ ) -> str:
42
+ """
43
+ Customize one of the template bash scripts.
44
+
45
+ Args:
46
+ script_filename:
47
+ replacements:
48
+ script_dir:
49
+ logger_name:
50
+ """
51
+ logger = get_logger(logger_name)
52
+ logger.debug(f"_customize_and_run_template {script_filename} - START")
53
+
54
+ script_path_local = Path(script_dir) / script_filename
55
+ # Read template
56
+ customize_template(
57
+ template_name=script_filename,
58
+ replacements=replacements,
59
+ script_path=script_path_local,
60
+ )
61
+
62
+ cmd = f"bash {script_path_local}"
63
+ logger.debug(f"Now run '{cmd}' ")
64
+
65
+ stdout = execute_command_sync(command=cmd)
66
+
67
+ logger.debug(f"Standard output of '{cmd}':\n{stdout}")
68
+ logger.debug(f"_customize_and_run_template {script_filename} - END")
69
+
70
+ return stdout
71
+
72
+
73
+ def collect_package_local(
74
+ *,
75
+ state_id: int,
76
+ task_group: TaskGroupV2,
77
+ ) -> None:
78
+ """
79
+ Collect a task package.
80
+
81
+ This function is run as a background task, therefore exceptions must be
82
+ handled.
83
+
84
+ NOTE: by making this function sync, it will run within a thread - due to
85
+ starlette/fastapi handling of background tasks (see
86
+ https://github.com/encode/starlette/blob/master/starlette/background.py).
87
+
88
+
89
+ Arguments:
90
+ state_id:
91
+ task_group:
92
+ """
93
+
94
+ # Create the task_group path
95
+ with TemporaryDirectory() as tmpdir:
96
+
97
+ # Setup logger in tmpdir
98
+ LOGGER_NAME = "task_collection_local"
99
+ log_file_path = get_log_path(Path(tmpdir))
100
+ logger = set_logger(
101
+ logger_name=LOGGER_NAME,
102
+ log_file_path=log_file_path,
103
+ )
104
+
105
+ # Log some info
106
+ logger.debug("START")
107
+ for key, value in task_group.model_dump().items():
108
+ logger.debug(f"task_group.{key}: {value}")
109
+
110
+ # Open a DB session
111
+ with next(get_sync_db()) as db:
112
+
113
+ # Check that the task_group path does not exist
114
+ if Path(task_group.path).exists():
115
+ error_msg = f"{task_group.path} already exists."
116
+ logger.error(error_msg)
117
+ _handle_failure(
118
+ state_id=state_id,
119
+ logger_name=LOGGER_NAME,
120
+ log_file_path=log_file_path,
121
+ exception=FileExistsError(error_msg),
122
+ db=db,
123
+ task_group_id=task_group.id,
124
+ )
125
+ return
126
+
127
+ try:
128
+ # Prepare replacements for task-collection scripts
129
+ python_bin = get_python_interpreter_v2(
130
+ python_version=task_group.python_version
131
+ )
132
+ install_string = task_group.pip_install_string
133
+ settings = Inject(get_settings)
134
+ replacements = [
135
+ ("__PACKAGE_NAME__", task_group.pkg_name),
136
+ ("__TASK_GROUP_DIR__", task_group.path),
137
+ ("__PACKAGE_ENV_DIR__", task_group.venv_path),
138
+ ("__PYTHON__", python_bin),
139
+ ("__INSTALL_STRING__", install_string),
140
+ (
141
+ "__FRACTAL_MAX_PIP_VERSION__",
142
+ settings.FRACTAL_MAX_PIP_VERSION,
143
+ ),
144
+ (
145
+ "__PINNED_PACKAGE_LIST__",
146
+ task_group.pinned_package_versions_string,
147
+ ),
148
+ ]
149
+
150
+ common_args = dict(
151
+ replacements=replacements,
152
+ script_dir=task_group.path,
153
+ logger_name=LOGGER_NAME,
154
+ )
155
+
156
+ logger.debug("installing - START")
157
+ _set_collection_state_data_status(
158
+ state_id=state_id,
159
+ new_status=CollectionStatusV2.INSTALLING,
160
+ logger_name=LOGGER_NAME,
161
+ db=db,
162
+ )
163
+
164
+ # Create main path for task group
165
+ Path(task_group.path).mkdir(parents=True)
166
+ logger.debug(f"Created {task_group.path}")
167
+
168
+ # Create venv
169
+ logger.debug(
170
+ (f"START - Create python venv {task_group.venv_path}")
171
+ )
172
+ cmd = (
173
+ f"python{task_group.python_version} -m venv "
174
+ f"{task_group.venv_path} --copies"
175
+ )
176
+ stdout = execute_command_sync(command=cmd)
177
+ logger.debug(
178
+ (f"END - Create python venv folder {task_group.venv_path}")
179
+ )
180
+ _refresh_logs(
181
+ state_id=state_id,
182
+ log_file_path=log_file_path,
183
+ db=db,
184
+ )
185
+ # Close db connections before long pip related operations
186
+ # Warning this expunge all ORM objects.
187
+ # https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.close
188
+ db.close()
189
+
190
+ stdout = _customize_and_run_template(
191
+ script_filename="_2_preliminary_pip_operations.sh",
192
+ **common_args,
193
+ )
194
+ _refresh_logs(
195
+ state_id=state_id,
196
+ log_file_path=log_file_path,
197
+ db=db,
198
+ )
199
+ stdout = _customize_and_run_template(
200
+ script_filename="_3_pip_install.sh",
201
+ **common_args,
202
+ )
203
+ _refresh_logs(
204
+ state_id=state_id,
205
+ log_file_path=log_file_path,
206
+ db=db,
207
+ )
208
+ stdout_pip_freeze = _customize_and_run_template(
209
+ script_filename="_4_pip_freeze.sh",
210
+ **common_args,
211
+ )
212
+ logger.debug("installing - END")
213
+ _refresh_logs(
214
+ state_id=state_id,
215
+ log_file_path=log_file_path,
216
+ db=db,
217
+ )
218
+
219
+ logger.debug("collecting - START")
220
+ _set_collection_state_data_status(
221
+ state_id=state_id,
222
+ new_status=CollectionStatusV2.COLLECTING,
223
+ logger_name=LOGGER_NAME,
224
+ db=db,
225
+ )
226
+ _refresh_logs(
227
+ state_id=state_id,
228
+ log_file_path=log_file_path,
229
+ db=db,
230
+ )
231
+
232
+ stdout = _customize_and_run_template(
233
+ script_filename="_5_pip_show.sh",
234
+ **common_args,
235
+ )
236
+ _refresh_logs(
237
+ state_id=state_id,
238
+ log_file_path=log_file_path,
239
+ db=db,
240
+ )
241
+
242
+ pkg_attrs = parse_script_5_stdout(stdout)
243
+ for key, value in pkg_attrs.items():
244
+ logger.debug(
245
+ f"collecting - parsed from pip-show: {key}={value}"
246
+ )
247
+ # Check package_name match between pip show and task-group
248
+ package_name_pip_show = pkg_attrs.get("package_name")
249
+ package_name_task_group = task_group.pkg_name
250
+ compare_package_names(
251
+ pkg_name_pip_show=package_name_pip_show,
252
+ pkg_name_task_group=package_name_task_group,
253
+ logger_name=LOGGER_NAME,
254
+ )
255
+ # Extract/drop parsed attributes
256
+ package_name = package_name_task_group
257
+ python_bin = pkg_attrs.pop("python_bin")
258
+ package_root_parent = pkg_attrs.pop("package_root_parent")
259
+
260
+ # TODO : Use more robust logic to determine `package_root`.
261
+ # Examples: use `importlib.util.find_spec`, or parse the output
262
+ # of `pip show --files {package_name}`.
263
+ package_name_underscore = package_name.replace("-", "_")
264
+ package_root = (
265
+ Path(package_root_parent) / package_name_underscore
266
+ ).as_posix()
267
+
268
+ # Read and validate manifest file
269
+ manifest_path = pkg_attrs.pop("manifest_path")
270
+ logger.info(f"collecting - now loading {manifest_path=}")
271
+ with open(manifest_path) as json_data:
272
+ pkg_manifest_dict = json.load(json_data)
273
+ logger.info(f"collecting - loaded {manifest_path=}")
274
+ logger.info("collecting - now validating manifest content")
275
+ pkg_manifest = ManifestV2(**pkg_manifest_dict)
276
+ logger.info("collecting - validated manifest content")
277
+ _refresh_logs(
278
+ state_id=state_id,
279
+ log_file_path=log_file_path,
280
+ db=db,
281
+ )
282
+
283
+ logger.info("collecting - _prepare_tasks_metadata - start")
284
+ task_list = _prepare_tasks_metadata(
285
+ package_manifest=pkg_manifest,
286
+ package_version=task_group.version,
287
+ package_root=Path(package_root),
288
+ python_bin=Path(python_bin),
289
+ )
290
+ check_task_files_exist(task_list=task_list)
291
+ logger.info("collecting - _prepare_tasks_metadata - end")
292
+ _refresh_logs(
293
+ state_id=state_id,
294
+ log_file_path=log_file_path,
295
+ db=db,
296
+ )
297
+
298
+ logger.info(
299
+ "collecting - create_db_tasks_and_update_task_group - "
300
+ "start"
301
+ )
302
+ task_group = create_db_tasks_and_update_task_group(
303
+ task_list=task_list,
304
+ task_group_id=task_group.id,
305
+ db=db,
306
+ )
307
+ logger.info(
308
+ "collecting - create_db_tasks_and_update_task_group - end"
309
+ )
310
+
311
+ logger.debug("collecting - END")
312
+
313
+ # Finalize (write metadata to DB)
314
+ logger.debug("finalising - START")
315
+
316
+ _refresh_logs(
317
+ state_id=state_id,
318
+ log_file_path=log_file_path,
319
+ db=db,
320
+ )
321
+ collection_state = db.get(CollectionStateV2, state_id)
322
+ collection_state.data["freeze"] = stdout_pip_freeze
323
+ collection_state.data["status"] = CollectionStatusV2.OK
324
+ # FIXME: The `task_list` key is likely not used by any client,
325
+ # we should consider dropping it
326
+ task_read_list = [
327
+ TaskReadV2(**task.model_dump()).dict()
328
+ for task in task_group.task_list
329
+ ]
330
+ collection_state.data["task_list"] = task_read_list
331
+ flag_modified(collection_state, "data")
332
+ db.commit()
333
+ logger.debug("finalising - END")
334
+ logger.debug("END")
335
+
336
+ except Exception as collection_e:
337
+ # Delete corrupted package dir
338
+ try:
339
+ logger.info(f"Now delete folder {task_group.path}")
340
+
341
+ shutil.rmtree(task_group.path)
342
+ logger.info(f"Deleted folder {task_group.path}")
343
+ except Exception as rm_e:
344
+ logger.error(
345
+ "Removing folder failed.\n"
346
+ f"Original error:\n{str(rm_e)}"
347
+ )
348
+
349
+ _handle_failure(
350
+ state_id=state_id,
351
+ logger_name=LOGGER_NAME,
352
+ log_file_path=log_file_path,
353
+ exception=collection_e,
354
+ db=db,
355
+ task_group_id=task_group.id,
356
+ )
357
+ return