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.
- fractal_server/__init__.py +1 -1
- fractal_server/app/models/user_settings.py +1 -0
- fractal_server/app/models/v2/task.py +15 -0
- fractal_server/app/routes/api/v2/dataset.py +39 -6
- fractal_server/app/routes/api/v2/task.py +2 -5
- fractal_server/app/routes/api/v2/task_collection.py +14 -42
- fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
- fractal_server/app/schemas/user_settings.py +18 -0
- fractal_server/app/schemas/v2/dataset.py +5 -3
- fractal_server/app/schemas/v2/task_collection.py +20 -4
- fractal_server/migrations/versions/19eca0dd47a9_user_settings_project_dir.py +39 -0
- fractal_server/tasks/utils.py +0 -31
- fractal_server/tasks/v1/background_operations.py +11 -11
- fractal_server/tasks/v1/endpoint_operations.py +5 -5
- fractal_server/tasks/v1/utils.py +2 -2
- fractal_server/tasks/v2/collection_local.py +357 -0
- fractal_server/tasks/v2/{background_operations_ssh.py → collection_ssh.py} +108 -102
- fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -8
- fractal_server/tasks/v2/templates/_2_preliminary_pip_operations.sh +2 -2
- fractal_server/tasks/v2/templates/_3_pip_install.sh +22 -1
- fractal_server/tasks/v2/templates/_5_pip_show.sh +5 -5
- fractal_server/tasks/v2/utils_background.py +209 -0
- fractal_server/tasks/v2/utils_package_names.py +77 -0
- fractal_server/tasks/v2/{utils.py → utils_python_interpreter.py} +0 -26
- fractal_server/tasks/v2/utils_templates.py +59 -0
- fractal_server/utils.py +48 -3
- {fractal_server-2.7.1.dist-info → fractal_server-2.8.0.dist-info}/METADATA +11 -8
- {fractal_server-2.7.1.dist-info → fractal_server-2.8.0.dist-info}/RECORD +32 -29
- fractal_server/tasks/v2/_venv_pip.py +0 -198
- fractal_server/tasks/v2/background_operations.py +0 -456
- /fractal_server/{tasks/v2/endpoint_operations.py → app/routes/api/v2/_aux_functions_task_collection.py} +0 -0
- {fractal_server-2.7.1.dist-info → fractal_server-2.8.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.7.1.dist-info → fractal_server-2.8.0.dist-info}/WHEEL +0 -0
- {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.
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
54
|
-
|
31
|
+
*,
|
32
|
+
template_name: str,
|
55
33
|
replacements: list[tuple[str, str]],
|
56
|
-
|
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 {
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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(
|
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 {
|
78
|
+
logger.debug(f"_customize_and_run_template {template_name} - END")
|
106
79
|
return stdout
|
107
80
|
|
108
81
|
|
109
|
-
def
|
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
|
-
|
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
|
-
|
193
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
229
|
-
|
230
|
-
|
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
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
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 =
|
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
|
-
"
|
259
|
+
"package_root_parent"
|
259
260
|
)
|
260
|
-
manifest_path_remote = pkg_attrs.pop("
|
261
|
+
manifest_path_remote = pkg_attrs.pop("manifest_path")
|
261
262
|
|
262
|
-
# FIXME SSH: Use more robust logic to determine `package_root
|
263
|
-
#
|
264
|
-
# `
|
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
|
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
|
334
|
+
except Exception as e_rm:
|
337
335
|
logger.error(
|
338
|
-
|
339
|
-
f"Original error:\n{str(
|
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
|
-
|
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
|