fractal-server 2.7.1__py3-none-any.whl → 2.8.1__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/_validators.py +1 -1
- fractal_server/app/schemas/user_settings.py +18 -0
- fractal_server/app/schemas/v2/dataset.py +6 -4
- fractal_server/app/schemas/v2/task_collection.py +31 -12
- fractal_server/migrations/versions/19eca0dd47a9_user_settings_project_dir.py +39 -0
- fractal_server/string_tools.py +10 -3
- 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.1.dist-info}/METADATA +11 -8
- {fractal_server-2.7.1.dist-info → fractal_server-2.8.1.dist-info}/RECORD +34 -31
- 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.1.dist-info}/LICENSE +0 -0
- {fractal_server-2.7.1.dist-info → fractal_server-2.8.1.dist-info}/WHEEL +0 -0
- {fractal_server-2.7.1.dist-info → fractal_server-2.8.1.dist-info}/entry_points.txt +0 -0
@@ -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
|
@@ -33,14 +33,6 @@ write_log "START create venv in ${PACKAGE_ENV_DIR}"
|
|
33
33
|
"$PYTHON" -m venv "$PACKAGE_ENV_DIR" --copies
|
34
34
|
write_log "END create venv in ${PACKAGE_ENV_DIR}"
|
35
35
|
echo
|
36
|
-
VENVPYTHON=${PACKAGE_ENV_DIR}/bin/python
|
37
|
-
if [ -f "$VENVPYTHON" ]; then
|
38
|
-
write_log "OK: $VENVPYTHON exists."
|
39
|
-
echo
|
40
|
-
else
|
41
|
-
write_log "ERROR: $VENVPYTHON not found"
|
42
|
-
exit 2
|
43
|
-
fi
|
44
36
|
|
45
37
|
# End
|
46
38
|
TIME_END=$(date +%s)
|
@@ -14,8 +14,8 @@ VENVPYTHON=${PACKAGE_ENV_DIR}/bin/python
|
|
14
14
|
|
15
15
|
# Upgrade pip
|
16
16
|
write_log "START upgrade pip"
|
17
|
-
"$VENVPYTHON" -m pip install "pip<=__FRACTAL_MAX_PIP_VERSION__" --upgrade
|
18
|
-
"$VENVPYTHON" -m pip install setuptools
|
17
|
+
"$VENVPYTHON" -m pip install --no-cache-dir "pip<=__FRACTAL_MAX_PIP_VERSION__" --upgrade
|
18
|
+
"$VENVPYTHON" -m pip install --no-cache-dir setuptools
|
19
19
|
write_log "END upgrade pip"
|
20
20
|
echo
|
21
21
|
|
@@ -9,6 +9,7 @@ write_log(){
|
|
9
9
|
# Variables to be filled within fractal-server
|
10
10
|
PACKAGE_ENV_DIR=__PACKAGE_ENV_DIR__
|
11
11
|
INSTALL_STRING=__INSTALL_STRING__
|
12
|
+
PINNED_PACKAGE_LIST="__PINNED_PACKAGE_LIST__"
|
12
13
|
|
13
14
|
TIME_START=$(date +%s)
|
14
15
|
|
@@ -16,10 +17,30 @@ VENVPYTHON=${PACKAGE_ENV_DIR}/bin/python
|
|
16
17
|
|
17
18
|
# Install package
|
18
19
|
write_log "START install ${INSTALL_STRING}"
|
19
|
-
"$VENVPYTHON" -m pip install "$INSTALL_STRING"
|
20
|
+
"$VENVPYTHON" -m pip install --no-cache-dir "$INSTALL_STRING"
|
20
21
|
write_log "END install ${INSTALL_STRING}"
|
21
22
|
echo
|
22
23
|
|
24
|
+
|
25
|
+
# Optionally install pinned versions
|
26
|
+
if [ "$PINNED_PACKAGE_LIST" != "" ]; then
|
27
|
+
write_log "START installing pinned versions $PINNED_PACKAGE_LIST"
|
28
|
+
for PINNED_PKG_VERSION in $PINNED_PACKAGE_LIST; do
|
29
|
+
|
30
|
+
PKGNAME=$(echo "$PINNED_PKG_VERSION" | cut -d '=' -f 1)
|
31
|
+
write_log "INFO: package name $PKGNAME"
|
32
|
+
"$VENVPYTHON" -m pip show "$PKGNAME"
|
33
|
+
|
34
|
+
done
|
35
|
+
|
36
|
+
write_log "All packages in ${PINNED_PACKAGE_LIST} are already installed, proceed with specific versions."
|
37
|
+
"$VENVPYTHON" -m pip install --no-cache-dir "$PINNED_PACKAGE_LIST"
|
38
|
+
write_log "END installing pinned versions $PINNED_PACKAGE_LIST"
|
39
|
+
else
|
40
|
+
write_log "SKIP installing pinned versions $PINNED_PACKAGE_LIST (empty list)"
|
41
|
+
fi
|
42
|
+
|
43
|
+
|
23
44
|
# End
|
24
45
|
TIME_END=$(date +%s)
|
25
46
|
write_log "All good up to here."
|
@@ -26,16 +26,16 @@ write_log "START pip show"
|
|
26
26
|
$VENVPYTHON -m pip show ${PACKAGE_NAME}
|
27
27
|
write_log "END pip show"
|
28
28
|
echo
|
29
|
-
PACKAGE_NAME=$($VENVPYTHON -m pip show $
|
29
|
+
PACKAGE_NAME=$($VENVPYTHON -m pip show "$PACKAGE_NAME" | grep "Name:" | cut -d ":" -f 2 | tr -d "[:space:]")
|
30
30
|
write_log "Package name: $PACKAGE_NAME"
|
31
31
|
echo
|
32
|
-
PACKAGE_VERSION=$($VENVPYTHON -m pip show $
|
32
|
+
PACKAGE_VERSION=$($VENVPYTHON -m pip show "$PACKAGE_NAME" | grep "Version:" | cut -d ":" -f 2 | tr -d "[:space:]")
|
33
33
|
write_log "Package version: $PACKAGE_VERSION"
|
34
34
|
echo
|
35
|
-
PACKAGE_PARENT_FOLDER=$($VENVPYTHON -m pip show $
|
35
|
+
PACKAGE_PARENT_FOLDER=$($VENVPYTHON -m pip show "$PACKAGE_NAME" | grep "Location:" | cut -d ":" -f 2 | tr -d "[:space:]")
|
36
36
|
write_log "Package parent folder: $PACKAGE_PARENT_FOLDER"
|
37
37
|
echo
|
38
|
-
MANIFEST_RELATIVE_PATH=$($VENVPYTHON -m pip show $
|
38
|
+
MANIFEST_RELATIVE_PATH=$($VENVPYTHON -m pip show "$PACKAGE_NAME" --files | grep "__FRACTAL_MANIFEST__.json" | tr -d "[:space:]")
|
39
39
|
write_log "Manifest relative path: $MANIFEST_RELATIVE_PATH"
|
40
40
|
echo
|
41
41
|
MANIFEST_ABSOLUTE_PATH="${PACKAGE_PARENT_FOLDER}/${MANIFEST_RELATIVE_PATH}"
|
@@ -46,7 +46,7 @@ if [ -f "$MANIFEST_ABSOLUTE_PATH" ]; then
|
|
46
46
|
echo
|
47
47
|
else
|
48
48
|
write_log "ERROR: manifest path not found at $MANIFEST_ABSOLUTE_PATH"
|
49
|
-
exit
|
49
|
+
exit 2
|
50
50
|
fi
|
51
51
|
|
52
52
|
# End
|
@@ -0,0 +1,209 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from sqlalchemy.orm import Session as DBSyncSession
|
5
|
+
from sqlalchemy.orm.attributes import flag_modified
|
6
|
+
from sqlmodel import select
|
7
|
+
|
8
|
+
from fractal_server.app.models.v2 import CollectionStateV2
|
9
|
+
from fractal_server.app.models.v2 import TaskGroupV2
|
10
|
+
from fractal_server.app.schemas.v2 import CollectionStatusV2
|
11
|
+
from fractal_server.app.schemas.v2 import TaskCreateV2
|
12
|
+
from fractal_server.app.schemas.v2.manifest import ManifestV2
|
13
|
+
from fractal_server.logger import get_logger
|
14
|
+
from fractal_server.logger import reset_logger_handlers
|
15
|
+
|
16
|
+
|
17
|
+
def _set_collection_state_data_status(
|
18
|
+
*,
|
19
|
+
state_id: int,
|
20
|
+
new_status: CollectionStatusV2,
|
21
|
+
logger_name: str,
|
22
|
+
db: DBSyncSession,
|
23
|
+
):
|
24
|
+
logger = get_logger(logger_name)
|
25
|
+
logger.debug(f"{state_id=} - set state.data['status'] to {new_status}")
|
26
|
+
collection_state = db.get(CollectionStateV2, state_id)
|
27
|
+
collection_state.data["status"] = CollectionStatusV2(new_status)
|
28
|
+
flag_modified(collection_state, "data")
|
29
|
+
db.commit()
|
30
|
+
|
31
|
+
|
32
|
+
def _set_collection_state_data_log(
|
33
|
+
*,
|
34
|
+
state_id: int,
|
35
|
+
new_log: str,
|
36
|
+
logger_name: str,
|
37
|
+
db: DBSyncSession,
|
38
|
+
):
|
39
|
+
logger = get_logger(logger_name)
|
40
|
+
logger.debug(f"{state_id=} - set state.data['log']")
|
41
|
+
collection_state = db.get(CollectionStateV2, state_id)
|
42
|
+
collection_state.data["log"] = new_log
|
43
|
+
flag_modified(collection_state, "data")
|
44
|
+
db.commit()
|
45
|
+
|
46
|
+
|
47
|
+
def _set_collection_state_data_info(
|
48
|
+
*,
|
49
|
+
state_id: int,
|
50
|
+
new_info: str,
|
51
|
+
logger_name: str,
|
52
|
+
db: DBSyncSession,
|
53
|
+
):
|
54
|
+
logger = get_logger(logger_name)
|
55
|
+
logger.debug(f"{state_id=} - set state.data['info']")
|
56
|
+
collection_state = db.get(CollectionStateV2, state_id)
|
57
|
+
collection_state.data["info"] = new_info
|
58
|
+
flag_modified(collection_state, "data")
|
59
|
+
db.commit()
|
60
|
+
|
61
|
+
|
62
|
+
def _handle_failure(
|
63
|
+
state_id: int,
|
64
|
+
logger_name: str,
|
65
|
+
exception: Exception,
|
66
|
+
db: DBSyncSession,
|
67
|
+
task_group_id: int,
|
68
|
+
log_file_path: Path,
|
69
|
+
):
|
70
|
+
logger = get_logger(logger_name)
|
71
|
+
logger.error(f"Task collection failed. Original error: {str(exception)}")
|
72
|
+
|
73
|
+
_set_collection_state_data_status(
|
74
|
+
state_id=state_id,
|
75
|
+
new_status=CollectionStatusV2.FAIL,
|
76
|
+
logger_name=logger_name,
|
77
|
+
db=db,
|
78
|
+
)
|
79
|
+
|
80
|
+
new_log = log_file_path.open("r").read()
|
81
|
+
|
82
|
+
_set_collection_state_data_log(
|
83
|
+
state_id=state_id,
|
84
|
+
new_log=new_log,
|
85
|
+
logger_name=logger_name,
|
86
|
+
db=db,
|
87
|
+
)
|
88
|
+
# For backwards-compatibility, we also set state.data["info"]
|
89
|
+
_set_collection_state_data_info(
|
90
|
+
state_id=state_id,
|
91
|
+
new_info=f"Original error: {exception}",
|
92
|
+
logger_name=logger_name,
|
93
|
+
db=db,
|
94
|
+
)
|
95
|
+
|
96
|
+
# Delete TaskGroupV2 object / and apply cascade operation to FKs
|
97
|
+
logger.info(f"Now delete TaskGroupV2 with {task_group_id=}")
|
98
|
+
logger.info("Start of CollectionStateV2 cascade operations.")
|
99
|
+
stm = select(CollectionStateV2).where(
|
100
|
+
CollectionStateV2.taskgroupv2_id == task_group_id
|
101
|
+
)
|
102
|
+
res = db.execute(stm)
|
103
|
+
collection_states = res.scalars().all()
|
104
|
+
for collection_state in collection_states:
|
105
|
+
logger.info(
|
106
|
+
f"Setting CollectionStateV2[{collection_state.id}].taskgroupv2_id "
|
107
|
+
"to None."
|
108
|
+
)
|
109
|
+
collection_state.taskgroupv2_id = None
|
110
|
+
db.add(collection_state)
|
111
|
+
logger.info("End of CollectionStateV2 cascade operations.")
|
112
|
+
task_group = db.get(TaskGroupV2, task_group_id)
|
113
|
+
db.delete(task_group)
|
114
|
+
db.commit()
|
115
|
+
logger.info(f"TaskGroupV2 with {task_group_id=} deleted")
|
116
|
+
|
117
|
+
reset_logger_handlers(logger)
|
118
|
+
return
|
119
|
+
|
120
|
+
|
121
|
+
def _prepare_tasks_metadata(
|
122
|
+
*,
|
123
|
+
package_manifest: ManifestV2,
|
124
|
+
python_bin: Path,
|
125
|
+
package_root: Path,
|
126
|
+
package_version: Optional[str] = None,
|
127
|
+
) -> list[TaskCreateV2]:
|
128
|
+
"""
|
129
|
+
Based on the package manifest and additional info, prepare the task list.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
package_manifest:
|
133
|
+
python_bin:
|
134
|
+
package_root:
|
135
|
+
package_version:
|
136
|
+
"""
|
137
|
+
task_list = []
|
138
|
+
for _task in package_manifest.task_list:
|
139
|
+
# Set non-command attributes
|
140
|
+
task_attributes = {}
|
141
|
+
if package_version is not None:
|
142
|
+
task_attributes["version"] = package_version
|
143
|
+
if package_manifest.has_args_schemas:
|
144
|
+
task_attributes[
|
145
|
+
"args_schema_version"
|
146
|
+
] = package_manifest.args_schema_version
|
147
|
+
# Set command attributes
|
148
|
+
if _task.executable_non_parallel is not None:
|
149
|
+
non_parallel_path = package_root / _task.executable_non_parallel
|
150
|
+
task_attributes["command_non_parallel"] = (
|
151
|
+
f"{python_bin.as_posix()} " f"{non_parallel_path.as_posix()}"
|
152
|
+
)
|
153
|
+
if _task.executable_parallel is not None:
|
154
|
+
parallel_path = package_root / _task.executable_parallel
|
155
|
+
task_attributes[
|
156
|
+
"command_parallel"
|
157
|
+
] = f"{python_bin.as_posix()} {parallel_path.as_posix()}"
|
158
|
+
# Create object
|
159
|
+
task_obj = TaskCreateV2(
|
160
|
+
**_task.dict(
|
161
|
+
exclude={
|
162
|
+
"executable_non_parallel",
|
163
|
+
"executable_parallel",
|
164
|
+
}
|
165
|
+
),
|
166
|
+
**task_attributes,
|
167
|
+
authors=package_manifest.authors,
|
168
|
+
)
|
169
|
+
task_list.append(task_obj)
|
170
|
+
return task_list
|
171
|
+
|
172
|
+
|
173
|
+
def check_task_files_exist(task_list: list[TaskCreateV2]) -> None:
|
174
|
+
"""
|
175
|
+
Check that the modules listed in task commands point to existing files.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
task_list:
|
179
|
+
"""
|
180
|
+
for _task in task_list:
|
181
|
+
if _task.command_non_parallel is not None:
|
182
|
+
_task_path = _task.command_non_parallel.split()[1]
|
183
|
+
if not Path(_task_path).exists():
|
184
|
+
raise FileNotFoundError(
|
185
|
+
f"Task `{_task.name}` has `command_non_parallel` "
|
186
|
+
f"pointing to missing file `{_task_path}`."
|
187
|
+
)
|
188
|
+
if _task.command_parallel is not None:
|
189
|
+
_task_path = _task.command_parallel.split()[1]
|
190
|
+
if not Path(_task_path).exists():
|
191
|
+
raise FileNotFoundError(
|
192
|
+
f"Task `{_task.name}` has `command_parallel` "
|
193
|
+
f"pointing to missing file `{_task_path}`."
|
194
|
+
)
|
195
|
+
|
196
|
+
|
197
|
+
def _refresh_logs(
|
198
|
+
*,
|
199
|
+
state_id: int,
|
200
|
+
log_file_path: Path,
|
201
|
+
db: DBSyncSession,
|
202
|
+
) -> None:
|
203
|
+
"""
|
204
|
+
Read logs from file and update them in the db.
|
205
|
+
"""
|
206
|
+
collection_state = db.get(CollectionStateV2, state_id)
|
207
|
+
collection_state.data["log"] = log_file_path.open("r").read()
|
208
|
+
flag_modified(collection_state, "data")
|
209
|
+
db.commit()
|