fractal-server 2.7.1__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 (34) 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 +15 -0
  4. fractal_server/app/routes/api/v2/dataset.py +39 -6
  5. fractal_server/app/routes/api/v2/task.py +2 -5
  6. fractal_server/app/routes/api/v2/task_collection.py +14 -42
  7. fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
  8. fractal_server/app/schemas/user_settings.py +18 -0
  9. fractal_server/app/schemas/v2/dataset.py +5 -3
  10. fractal_server/app/schemas/v2/task_collection.py +20 -4
  11. fractal_server/migrations/versions/19eca0dd47a9_user_settings_project_dir.py +39 -0
  12. fractal_server/tasks/utils.py +0 -31
  13. fractal_server/tasks/v1/background_operations.py +11 -11
  14. fractal_server/tasks/v1/endpoint_operations.py +5 -5
  15. fractal_server/tasks/v1/utils.py +2 -2
  16. fractal_server/tasks/v2/collection_local.py +357 -0
  17. fractal_server/tasks/v2/{background_operations_ssh.py → collection_ssh.py} +108 -102
  18. fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -8
  19. fractal_server/tasks/v2/templates/_2_preliminary_pip_operations.sh +2 -2
  20. fractal_server/tasks/v2/templates/_3_pip_install.sh +22 -1
  21. fractal_server/tasks/v2/templates/_5_pip_show.sh +5 -5
  22. fractal_server/tasks/v2/utils_background.py +209 -0
  23. fractal_server/tasks/v2/utils_package_names.py +77 -0
  24. fractal_server/tasks/v2/{utils.py → utils_python_interpreter.py} +0 -26
  25. fractal_server/tasks/v2/utils_templates.py +59 -0
  26. fractal_server/utils.py +48 -3
  27. {fractal_server-2.7.1.dist-info → fractal_server-2.8.0.dist-info}/METADATA +11 -8
  28. {fractal_server-2.7.1.dist-info → fractal_server-2.8.0.dist-info}/RECORD +32 -29
  29. fractal_server/tasks/v2/_venv_pip.py +0 -198
  30. fractal_server/tasks/v2/background_operations.py +0 -456
  31. /fractal_server/{tasks/v2/endpoint_operations.py → app/routes/api/v2/_aux_functions_task_collection.py} +0 -0
  32. {fractal_server-2.7.1.dist-info → fractal_server-2.8.0.dist-info}/LICENSE +0 -0
  33. {fractal_server-2.7.1.dist-info → fractal_server-2.8.0.dist-info}/WHEEL +0 -0
  34. {fractal_server-2.7.1.dist-info → fractal_server-2.8.0.dist-info}/entry_points.txt +0 -0
@@ -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
@@ -4,10 +4,10 @@ from tempfile import TemporaryDirectory
4
4
 
5
5
  from sqlalchemy.orm.attributes import flag_modified
6
6
 
7
- from .background_operations import _handle_failure
8
- from .background_operations import _prepare_tasks_metadata
9
- from .background_operations import _set_collection_state_data_status
10
7
  from .database_operations import create_db_tasks_and_update_task_group
8
+ from .utils_background import _handle_failure
9
+ from .utils_background import _prepare_tasks_metadata
10
+ from .utils_background import _set_collection_state_data_status
11
11
  from fractal_server.app.db import get_sync_db
12
12
  from fractal_server.app.models.v2 import CollectionStateV2
13
13
  from fractal_server.app.models.v2 import TaskGroupV2
@@ -18,42 +18,20 @@ from fractal_server.logger import get_logger
18
18
  from fractal_server.logger import set_logger
19
19
  from fractal_server.ssh._fabric import FractalSSH
20
20
  from fractal_server.syringe import Inject
21
- from fractal_server.tasks.v2.utils import get_python_interpreter_v2
22
-
23
- TEMPLATES_DIR = Path(__file__).parent / "templates"
24
-
25
-
26
- def _parse_script_5_stdout(stdout: str) -> dict[str, str]:
27
- searches = [
28
- ("Python interpreter:", "python_bin"),
29
- ("Package name:", "package_name"),
30
- ("Package version:", "package_version"),
31
- ("Package parent folder:", "package_root_parent_remote"),
32
- ("Manifest absolute path:", "manifest_path_remote"),
33
- ]
34
- stdout_lines = stdout.splitlines()
35
- attributes = dict()
36
- for search, attribute_name in searches:
37
- matching_lines = [_line for _line in stdout_lines if search in _line]
38
- if len(matching_lines) == 0:
39
- raise ValueError(f"String '{search}' not found in stdout.")
40
- elif len(matching_lines) > 1:
41
- raise ValueError(
42
- f"String '{search}' found too many times "
43
- f"({len(matching_lines)})."
44
- )
45
- else:
46
- actual_line = matching_lines[0]
47
- attribute_value = actual_line.split(search)[-1].strip(" ")
48
- attributes[attribute_name] = attribute_value
49
- return attributes
21
+ from fractal_server.tasks.v2.utils_background import _refresh_logs
22
+ from fractal_server.tasks.v2.utils_package_names import compare_package_names
23
+ from fractal_server.tasks.v2.utils_python_interpreter import (
24
+ get_python_interpreter_v2,
25
+ )
26
+ from fractal_server.tasks.v2.utils_templates import customize_template
27
+ from fractal_server.tasks.v2.utils_templates import parse_script_5_stdout
50
28
 
51
29
 
52
30
  def _customize_and_run_template(
53
- script_filename: str,
54
- templates_folder: Path,
31
+ *,
32
+ template_name: str,
55
33
  replacements: list[tuple[str, str]],
56
- tmpdir: str,
34
+ script_dir: str,
57
35
  logger_name: str,
58
36
  fractal_ssh: FractalSSH,
59
37
  tasks_base_dir: str,
@@ -64,31 +42,26 @@ def _customize_and_run_template(
64
42
 
65
43
  Args:
66
44
  script_filename:
67
- templates_folder:
68
45
  replacements:
69
46
  tmpdir:
70
47
  logger_name:
71
48
  fractal_ssh:
72
49
  """
73
50
  logger = get_logger(logger_name)
74
- logger.debug(f"_customize_and_run_template {script_filename} - START")
75
-
76
- # Read template
77
- template_path = templates_folder / script_filename
78
- with template_path.open("r") as f:
79
- script_contents = f.read()
80
- # Customize template
81
- for old_new in replacements:
82
- script_contents = script_contents.replace(old_new[0], old_new[1])
83
- # Write script locally
84
- script_path_local = (Path(tmpdir) / script_filename).as_posix()
85
- with open(script_path_local, "w") as f:
86
- f.write(script_contents)
51
+ logger.debug(f"_customize_and_run_template {template_name} - START")
52
+
53
+ script_path_local = Path(script_dir) / template_name
54
+
55
+ customize_template(
56
+ template_name=template_name,
57
+ replacements=replacements,
58
+ script_path=script_path_local,
59
+ )
87
60
 
88
61
  # Transfer script to remote host
89
62
  script_path_remote = os.path.join(
90
63
  tasks_base_dir,
91
- f"script_{abs(hash(tmpdir))}{script_filename}",
64
+ f"script_{abs(hash(script_dir))}{template_name}",
92
65
  )
93
66
  logger.debug(f"Now transfer {script_path_local=} over SSH.")
94
67
  fractal_ssh.send_file(
@@ -102,11 +75,11 @@ def _customize_and_run_template(
102
75
  stdout = fractal_ssh.run_command(cmd=cmd)
103
76
  logger.debug(f"Standard output of '{cmd}':\n{stdout}")
104
77
 
105
- logger.debug(f"_customize_and_run_template {script_filename} - END")
78
+ logger.debug(f"_customize_and_run_template {template_name} - END")
106
79
  return stdout
107
80
 
108
81
 
109
- def background_collect_pip_ssh(
82
+ def collect_package_ssh(
110
83
  *,
111
84
  state_id: int,
112
85
  task_group: TaskGroupV2,
@@ -169,12 +142,15 @@ def background_collect_pip_ssh(
169
142
  "__FRACTAL_MAX_PIP_VERSION__",
170
143
  settings.FRACTAL_MAX_PIP_VERSION,
171
144
  ),
145
+ (
146
+ "__PINNED_PACKAGE_LIST__",
147
+ task_group.pinned_package_versions_string,
148
+ ),
172
149
  ]
173
150
 
174
151
  common_args = dict(
175
- templates_folder=TEMPLATES_DIR,
176
152
  replacements=replacements,
177
- tmpdir=tmpdir,
153
+ script_dir=tmpdir,
178
154
  logger_name=LOGGER_NAME,
179
155
  fractal_ssh=fractal_ssh,
180
156
  tasks_base_dir=tasks_base_dir,
@@ -189,35 +165,56 @@ def background_collect_pip_ssh(
189
165
  logger_name=LOGGER_NAME,
190
166
  db=db,
191
167
  )
192
- # Avoid keeping the db session open as we start some possibly
193
- # long operations that do not use the db
168
+ _refresh_logs(
169
+ state_id=state_id,
170
+ log_file_path=log_file_path,
171
+ db=db,
172
+ )
194
173
  db.close()
195
-
196
174
  # Create remote folder (note that because of `parents=True` we
197
175
  # are in the `no error if existing, make parent directories as
198
176
  # needed` scenario)
199
177
  fractal_ssh.mkdir(folder=tasks_base_dir, parents=True)
200
178
 
201
179
  stdout = _customize_and_run_template(
202
- script_filename="_1_create_venv.sh",
180
+ template_name="_1_create_venv.sh",
203
181
  **common_args,
204
182
  )
205
183
  remove_venv_folder_upon_failure = True
184
+ _refresh_logs(
185
+ state_id=state_id,
186
+ log_file_path=log_file_path,
187
+ db=db,
188
+ )
206
189
 
207
190
  stdout = _customize_and_run_template(
208
- script_filename="_2_preliminary_pip_operations.sh",
191
+ template_name="_2_preliminary_pip_operations.sh",
209
192
  **common_args,
210
193
  )
194
+ _refresh_logs(
195
+ state_id=state_id,
196
+ log_file_path=log_file_path,
197
+ db=db,
198
+ )
211
199
  stdout = _customize_and_run_template(
212
- script_filename="_3_pip_install.sh",
200
+ template_name="_3_pip_install.sh",
213
201
  **common_args,
214
202
  )
203
+ _refresh_logs(
204
+ state_id=state_id,
205
+ log_file_path=log_file_path,
206
+ db=db,
207
+ )
215
208
  stdout_pip_freeze = _customize_and_run_template(
216
- script_filename="_4_pip_freeze.sh",
209
+ template_name="_4_pip_freeze.sh",
217
210
  **common_args,
218
211
  )
219
212
  logger.debug("installing - END")
220
-
213
+ _refresh_logs(
214
+ state_id=state_id,
215
+ log_file_path=log_file_path,
216
+ db=db,
217
+ )
221
218
  logger.debug("collecting - START")
222
219
  _set_collection_state_data_status(
223
220
  state_id=state_id,
@@ -225,43 +222,47 @@ def background_collect_pip_ssh(
225
222
  logger_name=LOGGER_NAME,
226
223
  db=db,
227
224
  )
228
- # Avoid keeping the db session open as we start some possibly
229
- # long operations that do not use the db
230
- db.close()
225
+ _refresh_logs(
226
+ state_id=state_id,
227
+ log_file_path=log_file_path,
228
+ db=db,
229
+ )
231
230
 
232
231
  stdout = _customize_and_run_template(
233
- script_filename="_5_pip_show.sh",
232
+ template_name="_5_pip_show.sh",
234
233
  **common_args,
235
234
  )
236
-
237
- pkg_attrs = _parse_script_5_stdout(stdout)
235
+ pkg_attrs = parse_script_5_stdout(stdout)
238
236
  for key, value in pkg_attrs.items():
239
237
  logger.debug(
240
238
  f"collecting - parsed from pip-show: {key}={value}"
241
239
  )
242
- # Check package_name match
243
- # FIXME SSH: Does this work well for non-canonical names?
240
+ # Check package_name match between pip show and task-group
244
241
  package_name_pip_show = pkg_attrs.get("package_name")
245
242
  package_name_task_group = task_group.pkg_name
246
- if package_name_pip_show != package_name_task_group:
247
- error_msg = (
248
- f"`package_name` mismatch: "
249
- f"{package_name_task_group=} but "
250
- f"{package_name_pip_show=}"
251
- )
252
- logger.error(error_msg)
253
- raise ValueError(error_msg)
243
+ compare_package_names(
244
+ pkg_name_pip_show=package_name_pip_show,
245
+ pkg_name_task_group=package_name_task_group,
246
+ logger_name=LOGGER_NAME,
247
+ )
248
+
249
+ _refresh_logs(
250
+ state_id=state_id,
251
+ log_file_path=log_file_path,
252
+ db=db,
253
+ )
254
+
254
255
  # Extract/drop parsed attributes
255
- package_name = pkg_attrs.pop("package_name")
256
+ package_name = package_name_task_group
256
257
  python_bin = pkg_attrs.pop("python_bin")
257
258
  package_root_parent_remote = pkg_attrs.pop(
258
- "package_root_parent_remote"
259
+ "package_root_parent"
259
260
  )
260
- manifest_path_remote = pkg_attrs.pop("manifest_path_remote")
261
+ manifest_path_remote = pkg_attrs.pop("manifest_path")
261
262
 
262
- # FIXME SSH: Use more robust logic to determine `package_root`,
263
- # e.g. as in the custom task-collection endpoint (where we use
264
- # `importlib.util.find_spec`)
263
+ # FIXME SSH: Use more robust logic to determine `package_root`.
264
+ # Examples: use `importlib.util.find_spec`, or parse the output
265
+ # of `pip show --files {package_name}`.
265
266
  package_name_underscore = package_name.replace("-", "_")
266
267
  package_root_remote = (
267
268
  Path(package_root_parent_remote) / package_name_underscore
@@ -298,6 +299,11 @@ def background_collect_pip_ssh(
298
299
  )
299
300
 
300
301
  logger.debug("collecting - END")
302
+ _refresh_logs(
303
+ state_id=state_id,
304
+ log_file_path=log_file_path,
305
+ db=db,
306
+ )
301
307
 
302
308
  # Finalize (write metadata to DB)
303
309
  logger.debug("finalising - START")
@@ -311,16 +317,8 @@ def background_collect_pip_ssh(
311
317
  logger.debug("finalising - END")
312
318
  logger.debug("END")
313
319
 
314
- except Exception as e:
320
+ except Exception as collection_e:
315
321
  # Delete corrupted package dir
316
- _handle_failure(
317
- state_id=state_id,
318
- log_file_path=log_file_path,
319
- logger_name=LOGGER_NAME,
320
- exception=e,
321
- db=db,
322
- task_group_id=task_group.id,
323
- )
324
322
  if remove_venv_folder_upon_failure:
325
323
  try:
326
324
  logger.info(
@@ -333,14 +331,22 @@ def background_collect_pip_ssh(
333
331
  logger.info(
334
332
  f"Deleted remoted folder {task_group.path}"
335
333
  )
336
- except Exception as e:
334
+ except Exception as e_rm:
337
335
  logger.error(
338
- f"Removing remote folder failed.\n"
339
- f"Original error:\n{str(e)}"
340
- )
341
- else:
342
- logger.info(
343
- "Not trying to remove remote folder "
344
- f"{task_group.path}."
336
+ "Removing folder failed. "
337
+ f"Original error:\n{str(e_rm)}"
345
338
  )
346
- return
339
+ else:
340
+ logger.info(
341
+ "Not trying to remove remote folder "
342
+ f"{task_group.path}."
343
+ )
344
+ _handle_failure(
345
+ state_id=state_id,
346
+ log_file_path=log_file_path,
347
+ logger_name=LOGGER_NAME,
348
+ exception=collection_e,
349
+ db=db,
350
+ task_group_id=task_group.id,
351
+ )
352
+ return