fractal-server 2.14.16__py3-none-any.whl → 2.15.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/security.py +2 -2
- fractal_server/app/models/user_settings.py +2 -2
- fractal_server/app/models/v2/dataset.py +3 -3
- fractal_server/app/models/v2/job.py +6 -6
- fractal_server/app/models/v2/task.py +12 -8
- fractal_server/app/models/v2/task_group.py +19 -7
- fractal_server/app/models/v2/workflowtask.py +6 -6
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +2 -5
- fractal_server/app/routes/api/v2/__init__.py +6 -0
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +22 -0
- fractal_server/app/routes/api/v2/task_collection.py +8 -18
- fractal_server/app/routes/api/v2/task_collection_custom.py +2 -2
- fractal_server/app/routes/api/v2/task_collection_pixi.py +219 -0
- fractal_server/app/routes/api/v2/task_group.py +3 -0
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +26 -10
- fractal_server/app/runner/executors/slurm_common/_slurm_config.py +10 -0
- fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py +39 -14
- fractal_server/app/runner/executors/slurm_common/get_slurm_config.py +8 -1
- fractal_server/app/schemas/v2/__init__.py +1 -1
- fractal_server/app/schemas/v2/dumps.py +1 -1
- fractal_server/app/schemas/v2/task_collection.py +1 -1
- fractal_server/app/schemas/v2/task_group.py +7 -5
- fractal_server/config.py +70 -0
- fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py +53 -0
- fractal_server/migrations/versions/b3ffb095f973_json_to_jsonb.py +340 -0
- fractal_server/ssh/_fabric.py +26 -0
- fractal_server/tasks/v2/local/__init__.py +3 -0
- fractal_server/tasks/v2/local/_utils.py +4 -3
- fractal_server/tasks/v2/local/collect.py +26 -30
- fractal_server/tasks/v2/local/collect_pixi.py +252 -0
- fractal_server/tasks/v2/local/deactivate.py +39 -46
- fractal_server/tasks/v2/local/deactivate_pixi.py +98 -0
- fractal_server/tasks/v2/local/reactivate.py +12 -23
- fractal_server/tasks/v2/local/reactivate_pixi.py +184 -0
- fractal_server/tasks/v2/ssh/__init__.py +3 -0
- fractal_server/tasks/v2/ssh/_utils.py +50 -9
- fractal_server/tasks/v2/ssh/collect.py +46 -56
- fractal_server/tasks/v2/ssh/collect_pixi.py +315 -0
- fractal_server/tasks/v2/ssh/deactivate.py +54 -67
- fractal_server/tasks/v2/ssh/deactivate_pixi.py +122 -0
- fractal_server/tasks/v2/ssh/reactivate.py +25 -38
- fractal_server/tasks/v2/ssh/reactivate_pixi.py +233 -0
- fractal_server/tasks/v2/templates/pixi_1_extract.sh +40 -0
- fractal_server/tasks/v2/templates/pixi_2_install.sh +52 -0
- fractal_server/tasks/v2/templates/pixi_3_post_install.sh +76 -0
- fractal_server/tasks/v2/utils_background.py +50 -8
- fractal_server/tasks/v2/utils_pixi.py +38 -0
- fractal_server/tasks/v2/utils_templates.py +14 -1
- {fractal_server-2.14.16.dist-info → fractal_server-2.15.0.dist-info}/METADATA +1 -1
- {fractal_server-2.14.16.dist-info → fractal_server-2.15.0.dist-info}/RECORD +54 -41
- {fractal_server-2.14.16.dist-info → fractal_server-2.15.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.14.16.dist-info → fractal_server-2.15.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.14.16.dist-info → fractal_server-2.15.0.dist-info}/entry_points.txt +0 -0
@@ -1,15 +1,14 @@
|
|
1
|
-
import logging
|
2
1
|
import time
|
3
2
|
from pathlib import Path
|
4
3
|
from tempfile import TemporaryDirectory
|
5
4
|
|
6
5
|
from ..utils_background import add_commit_refresh
|
7
6
|
from ..utils_background import fail_and_cleanup
|
7
|
+
from ..utils_background import get_activity_and_task_group
|
8
8
|
from ..utils_templates import get_collection_replacements
|
9
9
|
from ._utils import _customize_and_run_template
|
10
|
+
from ._utils import check_ssh_or_fail_and_cleanup
|
10
11
|
from fractal_server.app.db import get_sync_db
|
11
|
-
from fractal_server.app.models.v2 import TaskGroupActivityV2
|
12
|
-
from fractal_server.app.models.v2 import TaskGroupV2
|
13
12
|
from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
|
14
13
|
from fractal_server.app.schemas.v2.task_group import TaskGroupActivityStatusV2
|
15
14
|
from fractal_server.logger import reset_logger_handlers
|
@@ -56,46 +55,34 @@ def reactivate_ssh(
|
|
56
55
|
log_file_path=log_file_path,
|
57
56
|
)
|
58
57
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
f"{task_group=}\n{activity=}. Exit."
|
75
|
-
)
|
76
|
-
return
|
77
|
-
|
78
|
-
# Log some info
|
79
|
-
logger.info("START")
|
80
|
-
for key, value in task_group.model_dump().items():
|
81
|
-
logger.debug(f"task_group.{key}: {value}")
|
82
|
-
|
83
|
-
# Check that SSH connection works
|
58
|
+
logger.info("START")
|
59
|
+
with next(get_sync_db()) as db:
|
60
|
+
db_objects_ok, task_group, activity = get_activity_and_task_group(
|
61
|
+
task_group_activity_id=task_group_activity_id,
|
62
|
+
task_group_id=task_group_id,
|
63
|
+
db=db,
|
64
|
+
logger_name=LOGGER_NAME,
|
65
|
+
)
|
66
|
+
if not db_objects_ok:
|
67
|
+
return
|
68
|
+
|
69
|
+
with SingleUseFractalSSH(
|
70
|
+
ssh_config=ssh_config,
|
71
|
+
logger_name=LOGGER_NAME,
|
72
|
+
) as fractal_ssh:
|
84
73
|
try:
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
fail_and_cleanup(
|
74
|
+
# Check SSH connection
|
75
|
+
ssh_ok = check_ssh_or_fail_and_cleanup(
|
76
|
+
fractal_ssh=fractal_ssh,
|
89
77
|
task_group=task_group,
|
90
78
|
task_group_activity=activity,
|
91
79
|
logger_name=LOGGER_NAME,
|
92
80
|
log_file_path=log_file_path,
|
93
|
-
exception=e,
|
94
81
|
db=db,
|
95
82
|
)
|
96
|
-
|
83
|
+
if not ssh_ok:
|
84
|
+
return
|
97
85
|
|
98
|
-
try:
|
99
86
|
# Check that the (remote) task_group venv_path does not
|
100
87
|
# exist
|
101
88
|
if fractal_ssh.remote_exists(task_group.venv_path):
|
@@ -128,7 +115,7 @@ def reactivate_ssh(
|
|
128
115
|
Path(task_group.path) / "_tmp_pip_freeze.txt"
|
129
116
|
).as_posix()
|
130
117
|
with open(pip_freeze_file_local, "w") as f:
|
131
|
-
f.write(task_group.
|
118
|
+
f.write(task_group.env_info)
|
132
119
|
fractal_ssh.send_file(
|
133
120
|
local=pip_freeze_file_local,
|
134
121
|
remote=pip_freeze_file_remote,
|
@@ -199,8 +186,8 @@ def reactivate_ssh(
|
|
199
186
|
logger.info(f"Deleted folder {task_group.venv_path}")
|
200
187
|
except Exception as rm_e:
|
201
188
|
logger.error(
|
202
|
-
"Removing folder failed
|
203
|
-
f"Original error
|
189
|
+
"Removing folder failed. "
|
190
|
+
f"Original error: {str(rm_e)}"
|
204
191
|
)
|
205
192
|
|
206
193
|
fail_and_cleanup(
|
@@ -0,0 +1,233 @@
|
|
1
|
+
import time
|
2
|
+
from pathlib import Path
|
3
|
+
from tempfile import TemporaryDirectory
|
4
|
+
|
5
|
+
from ..utils_background import fail_and_cleanup
|
6
|
+
from ..utils_background import get_activity_and_task_group
|
7
|
+
from ..utils_pixi import SOURCE_DIR_NAME
|
8
|
+
from ._utils import check_ssh_or_fail_and_cleanup
|
9
|
+
from fractal_server.app.db import get_sync_db
|
10
|
+
from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
|
11
|
+
from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
|
12
|
+
from fractal_server.config import get_settings
|
13
|
+
from fractal_server.logger import reset_logger_handlers
|
14
|
+
from fractal_server.logger import set_logger
|
15
|
+
from fractal_server.ssh._fabric import SingleUseFractalSSH
|
16
|
+
from fractal_server.ssh._fabric import SSHConfig
|
17
|
+
from fractal_server.syringe import Inject
|
18
|
+
from fractal_server.tasks.utils import get_log_path
|
19
|
+
from fractal_server.tasks.v2.ssh._utils import _customize_and_run_template
|
20
|
+
from fractal_server.tasks.v2.utils_background import add_commit_refresh
|
21
|
+
from fractal_server.tasks.v2.utils_background import get_current_log
|
22
|
+
from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
|
23
|
+
from fractal_server.utils import get_timestamp
|
24
|
+
|
25
|
+
|
26
|
+
def reactivate_ssh_pixi(
|
27
|
+
*,
|
28
|
+
task_group_activity_id: int,
|
29
|
+
task_group_id: int,
|
30
|
+
ssh_config: SSHConfig,
|
31
|
+
tasks_base_dir: str,
|
32
|
+
) -> None:
|
33
|
+
"""
|
34
|
+
Reactivate a task group venv.
|
35
|
+
|
36
|
+
This function is run as a background task, therefore exceptions must be
|
37
|
+
handled.
|
38
|
+
|
39
|
+
Arguments:
|
40
|
+
task_group_id:
|
41
|
+
task_group_activity_id:
|
42
|
+
ssh_config:
|
43
|
+
tasks_base_dir:
|
44
|
+
Only used as a `safe_root` in `remove_dir`, and typically set to
|
45
|
+
`user_settings.ssh_tasks_dir`.
|
46
|
+
"""
|
47
|
+
|
48
|
+
LOGGER_NAME = f"{__name__}.ID{task_group_activity_id}"
|
49
|
+
|
50
|
+
with TemporaryDirectory() as tmpdir:
|
51
|
+
log_file_path = get_log_path(Path(tmpdir))
|
52
|
+
logger = set_logger(
|
53
|
+
logger_name=LOGGER_NAME,
|
54
|
+
log_file_path=log_file_path,
|
55
|
+
)
|
56
|
+
|
57
|
+
logger.info("START")
|
58
|
+
with next(get_sync_db()) as db:
|
59
|
+
db_objects_ok, task_group, activity = get_activity_and_task_group(
|
60
|
+
task_group_activity_id=task_group_activity_id,
|
61
|
+
task_group_id=task_group_id,
|
62
|
+
db=db,
|
63
|
+
logger_name=LOGGER_NAME,
|
64
|
+
)
|
65
|
+
if not db_objects_ok:
|
66
|
+
return
|
67
|
+
|
68
|
+
with SingleUseFractalSSH(
|
69
|
+
ssh_config=ssh_config,
|
70
|
+
logger_name=LOGGER_NAME,
|
71
|
+
) as fractal_ssh:
|
72
|
+
try:
|
73
|
+
# Check SSH connection
|
74
|
+
ssh_ok = check_ssh_or_fail_and_cleanup(
|
75
|
+
fractal_ssh=fractal_ssh,
|
76
|
+
task_group=task_group,
|
77
|
+
task_group_activity=activity,
|
78
|
+
logger_name=LOGGER_NAME,
|
79
|
+
log_file_path=log_file_path,
|
80
|
+
db=db,
|
81
|
+
)
|
82
|
+
if not ssh_ok:
|
83
|
+
return
|
84
|
+
|
85
|
+
# Check that the (remote) task_group source_dir does not
|
86
|
+
# exist
|
87
|
+
source_dir = Path(
|
88
|
+
task_group.path, SOURCE_DIR_NAME
|
89
|
+
).as_posix()
|
90
|
+
if fractal_ssh.remote_exists(source_dir):
|
91
|
+
error_msg = f"{source_dir} already exists."
|
92
|
+
logger.error(error_msg)
|
93
|
+
fail_and_cleanup(
|
94
|
+
task_group=task_group,
|
95
|
+
task_group_activity=activity,
|
96
|
+
logger_name=LOGGER_NAME,
|
97
|
+
log_file_path=log_file_path,
|
98
|
+
exception=FileExistsError(error_msg),
|
99
|
+
db=db,
|
100
|
+
)
|
101
|
+
return
|
102
|
+
|
103
|
+
settings = Inject(get_settings)
|
104
|
+
replacements = {
|
105
|
+
(
|
106
|
+
"__PIXI_HOME__",
|
107
|
+
settings.pixi.versions[task_group.pixi_version],
|
108
|
+
),
|
109
|
+
("__PACKAGE_DIR__", task_group.path),
|
110
|
+
("__TAR_GZ_PATH__", task_group.archive_path),
|
111
|
+
(
|
112
|
+
"__IMPORT_PACKAGE_NAME__",
|
113
|
+
task_group.pkg_name.replace("-", "_"),
|
114
|
+
),
|
115
|
+
("__SOURCE_DIR_NAME__", SOURCE_DIR_NAME),
|
116
|
+
("__FROZEN_OPTION__", "--frozen"),
|
117
|
+
(
|
118
|
+
"__TOKIO_WORKER_THREADS__",
|
119
|
+
str(settings.pixi.TOKIO_WORKER_THREADS),
|
120
|
+
),
|
121
|
+
(
|
122
|
+
"__PIXI_CONCURRENT_SOLVES__",
|
123
|
+
str(settings.pixi.PIXI_CONCURRENT_SOLVES),
|
124
|
+
),
|
125
|
+
(
|
126
|
+
"__PIXI_CONCURRENT_DOWNLOADS__",
|
127
|
+
str(settings.pixi.PIXI_CONCURRENT_DOWNLOADS),
|
128
|
+
),
|
129
|
+
}
|
130
|
+
|
131
|
+
logger.info("installing - START")
|
132
|
+
|
133
|
+
# Set status to ONGOING and refresh logs
|
134
|
+
activity.status = TaskGroupActivityStatusV2.ONGOING
|
135
|
+
activity.log = get_current_log(log_file_path)
|
136
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
137
|
+
|
138
|
+
script_dir_remote = Path(
|
139
|
+
task_group.path, SCRIPTS_SUBFOLDER
|
140
|
+
).as_posix()
|
141
|
+
common_args = dict(
|
142
|
+
script_dir_local=(
|
143
|
+
Path(tmpdir) / SCRIPTS_SUBFOLDER
|
144
|
+
).as_posix(),
|
145
|
+
script_dir_remote=script_dir_remote,
|
146
|
+
prefix=(
|
147
|
+
f"{int(time.time())}_"
|
148
|
+
f"{TaskGroupActivityActionV2.REACTIVATE}"
|
149
|
+
),
|
150
|
+
logger_name=LOGGER_NAME,
|
151
|
+
fractal_ssh=fractal_ssh,
|
152
|
+
)
|
153
|
+
|
154
|
+
# Run script 1 - extract tar.gz into `source_dir`
|
155
|
+
stdout = _customize_and_run_template(
|
156
|
+
template_filename="pixi_1_extract.sh",
|
157
|
+
replacements=replacements,
|
158
|
+
**common_args,
|
159
|
+
)
|
160
|
+
logger.debug(f"STDOUT: {stdout}")
|
161
|
+
activity.log = get_current_log(log_file_path)
|
162
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
163
|
+
|
164
|
+
# Write pixi.lock into `source_dir`
|
165
|
+
pixi_lock_local = Path(tmpdir, "pixi.lock").as_posix()
|
166
|
+
pixi_lock_remote = Path(
|
167
|
+
task_group.path, SOURCE_DIR_NAME, "pixi.lock"
|
168
|
+
).as_posix()
|
169
|
+
logger.info(
|
170
|
+
f"Write `env_info` contents into {pixi_lock_local}"
|
171
|
+
)
|
172
|
+
with open(pixi_lock_local, "w") as f:
|
173
|
+
f.write(task_group.env_info)
|
174
|
+
fractal_ssh.send_file(
|
175
|
+
local=pixi_lock_local,
|
176
|
+
remote=pixi_lock_remote,
|
177
|
+
)
|
178
|
+
|
179
|
+
# Run script 2 - run pixi-install command
|
180
|
+
stdout = _customize_and_run_template(
|
181
|
+
template_filename="pixi_2_install.sh",
|
182
|
+
replacements=replacements,
|
183
|
+
**common_args,
|
184
|
+
)
|
185
|
+
logger.debug(f"STDOUT: {stdout}")
|
186
|
+
activity.log = get_current_log(log_file_path)
|
187
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
188
|
+
|
189
|
+
# Run script 3 - post-install
|
190
|
+
stdout = _customize_and_run_template(
|
191
|
+
template_filename="pixi_3_post_install.sh",
|
192
|
+
replacements=replacements,
|
193
|
+
**common_args,
|
194
|
+
)
|
195
|
+
logger.debug(f"STDOUT: {stdout}")
|
196
|
+
activity.log = get_current_log(log_file_path)
|
197
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
198
|
+
|
199
|
+
fractal_ssh.run_command(cmd=f"chmod 755 {source_dir} -R")
|
200
|
+
|
201
|
+
# Finalize (write metadata to DB)
|
202
|
+
activity.status = TaskGroupActivityStatusV2.OK
|
203
|
+
activity.timestamp_ended = get_timestamp()
|
204
|
+
activity = add_commit_refresh(obj=activity, db=db)
|
205
|
+
task_group.active = True
|
206
|
+
task_group = add_commit_refresh(obj=task_group, db=db)
|
207
|
+
logger.info("END")
|
208
|
+
|
209
|
+
reset_logger_handlers(logger)
|
210
|
+
|
211
|
+
except Exception as reactivate_e:
|
212
|
+
# Delete corrupted source_dir
|
213
|
+
try:
|
214
|
+
logger.info(f"Now delete folder {source_dir}")
|
215
|
+
fractal_ssh.remove_folder(
|
216
|
+
folder=source_dir,
|
217
|
+
safe_root=tasks_base_dir,
|
218
|
+
)
|
219
|
+
logger.info(f"Deleted folder {source_dir}")
|
220
|
+
except Exception as rm_e:
|
221
|
+
logger.error(
|
222
|
+
"Removing folder failed. "
|
223
|
+
f"Original error: {str(rm_e)}"
|
224
|
+
)
|
225
|
+
|
226
|
+
fail_and_cleanup(
|
227
|
+
task_group=task_group,
|
228
|
+
task_group_activity=activity,
|
229
|
+
logger_name=LOGGER_NAME,
|
230
|
+
log_file_path=log_file_path,
|
231
|
+
exception=reactivate_e,
|
232
|
+
db=db,
|
233
|
+
)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
set -e
|
2
|
+
|
3
|
+
write_log(){
|
4
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
5
|
+
echo "[extract-tar-gz-pixi, ${TIMESTAMP}] ${1}"
|
6
|
+
}
|
7
|
+
|
8
|
+
# Replacements
|
9
|
+
PACKAGE_DIR="__PACKAGE_DIR__"
|
10
|
+
TAR_GZ_PATH="__TAR_GZ_PATH__"
|
11
|
+
SOURCE_DIR_NAME="__SOURCE_DIR_NAME__"
|
12
|
+
|
13
|
+
# Strip trailing `/` from `PACKAGE_DIR`
|
14
|
+
PACKAGE_DIR=${PACKAGE_DIR%/}
|
15
|
+
|
16
|
+
# Known paths
|
17
|
+
SOURCE_DIR="${PACKAGE_DIR}/${SOURCE_DIR_NAME}"
|
18
|
+
TAR_GZ_BASENAME=$(basename "${TAR_GZ_PATH}" ".tar.gz")
|
19
|
+
|
20
|
+
TIME_START=$(date +%s)
|
21
|
+
|
22
|
+
cd "${PACKAGE_DIR}"
|
23
|
+
write_log "Changed working directory to ${PACKAGE_DIR}"
|
24
|
+
|
25
|
+
# -----------------------------------------------------------------------------
|
26
|
+
|
27
|
+
write_log "START 'tar xz -f ${TAR_GZ_PATH} ${TAR_GZ_BASENAME}'"
|
28
|
+
tar xz -f "${TAR_GZ_PATH}" "${TAR_GZ_BASENAME}"
|
29
|
+
write_log "END 'tar xz -f ${TAR_GZ_PATH} ${TAR_GZ_BASENAME}'"
|
30
|
+
echo
|
31
|
+
|
32
|
+
write_log "START 'mv ${PACKAGE_DIR}/${TAR_GZ_BASENAME} ${SOURCE_DIR}'"
|
33
|
+
mv "${PACKAGE_DIR}/${TAR_GZ_BASENAME}" "${SOURCE_DIR}"
|
34
|
+
write_log "END 'mv ${PACKAGE_DIR}/${TAR_GZ_BASENAME} ${SOURCE_DIR}'"
|
35
|
+
echo
|
36
|
+
|
37
|
+
TIME_END=$(date +%s)
|
38
|
+
write_log "Elapsed: $((TIME_END - TIME_START)) seconds"
|
39
|
+
write_log "All ok, exit."
|
40
|
+
echo
|
@@ -0,0 +1,52 @@
|
|
1
|
+
set -e
|
2
|
+
|
3
|
+
write_log(){
|
4
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
5
|
+
echo "[install-tasks-pixi, ${TIMESTAMP}] ${1}"
|
6
|
+
}
|
7
|
+
|
8
|
+
# Replacements
|
9
|
+
PIXI_HOME="__PIXI_HOME__"
|
10
|
+
PACKAGE_DIR="__PACKAGE_DIR__"
|
11
|
+
SOURCE_DIR_NAME="__SOURCE_DIR_NAME__"
|
12
|
+
FROZEN_OPTION="__FROZEN_OPTION__"
|
13
|
+
TOKIO_WORKER_THREADS="__TOKIO_WORKER_THREADS__"
|
14
|
+
PIXI_CONCURRENT_SOLVES="__PIXI_CONCURRENT_SOLVES__"
|
15
|
+
PIXI_CONCURRENT_DOWNLOADS="__PIXI_CONCURRENT_DOWNLOADS__"
|
16
|
+
|
17
|
+
# Strip trailing `/` from `PACKAGE_DIR`
|
18
|
+
PIXI_HOME=${PIXI_HOME%/}
|
19
|
+
PACKAGE_DIR=${PACKAGE_DIR%/}
|
20
|
+
|
21
|
+
# Known paths
|
22
|
+
PIXI_EXECUTABLE="${PIXI_HOME}/bin/pixi"
|
23
|
+
SOURCE_DIR="${PACKAGE_DIR}/${SOURCE_DIR_NAME}"
|
24
|
+
PYPROJECT_TOML="${SOURCE_DIR}/pyproject.toml"
|
25
|
+
|
26
|
+
# Pixi env variable
|
27
|
+
export PIXI_HOME="${PIXI_HOME}"
|
28
|
+
export PIXI_CACHE_DIR="${PIXI_HOME}/cache"
|
29
|
+
export RATTLER_AUTH_FILE="${PIXI_HOME}/credentials.json"
|
30
|
+
export TOKIO_WORKER_THREADS="${TOKIO_WORKER_THREADS}"
|
31
|
+
|
32
|
+
TIME_START=$(date +%s)
|
33
|
+
|
34
|
+
write_log "Hostname: $(hostname)"
|
35
|
+
|
36
|
+
cd "${PACKAGE_DIR}"
|
37
|
+
write_log "Changed working directory to ${PACKAGE_DIR}"
|
38
|
+
|
39
|
+
# -----------------------------------------------------------------------------
|
40
|
+
|
41
|
+
write_log "START '${PIXI_EXECUTABLE} install ${FROZEN_OPTION} --manifest-path ${PYPROJECT_TOML}'"
|
42
|
+
${PIXI_EXECUTABLE} install \
|
43
|
+
--concurrent-solves "${PIXI_CONCURRENT_SOLVES}" \
|
44
|
+
--concurrent-downloads "${PIXI_CONCURRENT_DOWNLOADS}" \
|
45
|
+
${FROZEN_OPTION} --manifest-path "${PYPROJECT_TOML}"
|
46
|
+
write_log "END '${PIXI_EXECUTABLE} install ${FROZEN_OPTION} --manifest-path ${PYPROJECT_TOML}'"
|
47
|
+
echo
|
48
|
+
|
49
|
+
TIME_END=$(date +%s)
|
50
|
+
write_log "Elapsed: $((TIME_END - TIME_START)) seconds"
|
51
|
+
write_log "All ok, exit."
|
52
|
+
echo
|
@@ -0,0 +1,76 @@
|
|
1
|
+
set -e
|
2
|
+
|
3
|
+
write_log(){
|
4
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
5
|
+
echo "[after-install-pixi, ${TIMESTAMP}] ${1}"
|
6
|
+
}
|
7
|
+
|
8
|
+
# Replacements
|
9
|
+
PIXI_HOME="__PIXI_HOME__"
|
10
|
+
PACKAGE_DIR="__PACKAGE_DIR__"
|
11
|
+
SOURCE_DIR_NAME="__SOURCE_DIR_NAME__"
|
12
|
+
IMPORT_PACKAGE_NAME="__IMPORT_PACKAGE_NAME__"
|
13
|
+
|
14
|
+
# Strip trailing `/` from `PACKAGE_DIR`
|
15
|
+
PIXI_HOME=${PIXI_HOME%/}
|
16
|
+
PACKAGE_DIR=${PACKAGE_DIR%/}
|
17
|
+
|
18
|
+
# Known paths
|
19
|
+
PIXI_EXECUTABLE="${PIXI_HOME}/bin/pixi"
|
20
|
+
SOURCE_DIR="${PACKAGE_DIR}/${SOURCE_DIR_NAME}"
|
21
|
+
PYPROJECT_TOML="${SOURCE_DIR}/pyproject.toml"
|
22
|
+
ACTIVATION_FILE="${SOURCE_DIR}/activate_project.sh"
|
23
|
+
PROJECT_PYTHON_WRAPPER="${SOURCE_DIR}/project_python.sh"
|
24
|
+
|
25
|
+
# Pixi env variable
|
26
|
+
export PIXI_HOME="${PIXI_HOME}"
|
27
|
+
export PIXI_CACHE_DIR="${PIXI_HOME}/cache"
|
28
|
+
export RATTLER_AUTH_FILE="${PIXI_HOME}/credentials.json"
|
29
|
+
|
30
|
+
|
31
|
+
TIME_START=$(date +%s)
|
32
|
+
|
33
|
+
cd "${PACKAGE_DIR}"
|
34
|
+
write_log "Changed working directory to ${PACKAGE_DIR}"
|
35
|
+
|
36
|
+
# -----------------------------------------------------------------------------
|
37
|
+
|
38
|
+
write_log "START '${PIXI_EXECUTABLE} shell-hook --manifest-path ${PYPROJECT_TOML}'"
|
39
|
+
${PIXI_EXECUTABLE} shell-hook --manifest-path "${PYPROJECT_TOML}" > "${ACTIVATION_FILE}"
|
40
|
+
write_log "END '${PIXI_EXECUTABLE} shell-hook --manifest-path ${PYPROJECT_TOML}'"
|
41
|
+
echo
|
42
|
+
|
43
|
+
PROJECT_PYTHON_BIN=$(${PIXI_EXECUTABLE} run --manifest-path "${PYPROJECT_TOML}" which python)
|
44
|
+
write_log "Found PROJECT_PYTHON_BIN=${PROJECT_PYTHON_BIN}"
|
45
|
+
|
46
|
+
# Write project-scoped Python wrapper
|
47
|
+
cat <<EOF > "${PROJECT_PYTHON_WRAPPER}"
|
48
|
+
#!/bin/bash
|
49
|
+
source ${ACTIVATION_FILE}
|
50
|
+
${PROJECT_PYTHON_BIN} "\$@"
|
51
|
+
EOF
|
52
|
+
|
53
|
+
chmod 755 "${PROJECT_PYTHON_WRAPPER}"
|
54
|
+
write_log "Written ${PROJECT_PYTHON_WRAPPER} with 755 permissions"
|
55
|
+
write_log "Project Python wrapper: ${PROJECT_PYTHON_WRAPPER}"
|
56
|
+
write_log "Project-Python version: $(${PROJECT_PYTHON_WRAPPER} --version)"
|
57
|
+
echo
|
58
|
+
|
59
|
+
# Find PACKAGE_FOLDER
|
60
|
+
FIND_PACKAGE_FOLDER_SCRIPT="${SOURCE_DIR}/find_package_folder.sh"
|
61
|
+
echo "source ${ACTIVATION_FILE}" > "${FIND_PACKAGE_FOLDER_SCRIPT}"
|
62
|
+
echo "${PROJECT_PYTHON_BIN} -c \"import ${IMPORT_PACKAGE_NAME} as p, os; print(os.path.dirname(p.__file__))\"" >> "${FIND_PACKAGE_FOLDER_SCRIPT}"
|
63
|
+
PACKAGE_FOLDER=$(bash "${FIND_PACKAGE_FOLDER_SCRIPT}")
|
64
|
+
write_log "Package folder: ${PACKAGE_FOLDER}"
|
65
|
+
echo
|
66
|
+
|
67
|
+
ENV_DISK_USAGE=$(du -sk "${PACKAGE_DIR}" | cut -f1)
|
68
|
+
ENV_FILE_NUMBER=$(find "${PACKAGE_DIR}" -type f | wc -l)
|
69
|
+
write_log "Disk usage: ${ENV_DISK_USAGE}"
|
70
|
+
write_log "Number of files: ${ENV_FILE_NUMBER}"
|
71
|
+
echo
|
72
|
+
|
73
|
+
TIME_END=$(date +%s)
|
74
|
+
write_log "Elapsed: $((TIME_END - TIME_START)) seconds"
|
75
|
+
write_log "All ok, exit."
|
76
|
+
echo
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import logging
|
1
2
|
from pathlib import Path
|
2
3
|
from typing import TypeVar
|
3
4
|
|
@@ -9,6 +10,7 @@ from fractal_server.app.schemas.v2 import TaskCreateV2
|
|
9
10
|
from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
|
10
11
|
from fractal_server.app.schemas.v2.manifest import ManifestV2
|
11
12
|
from fractal_server.app.schemas.v2.task_group import TaskGroupActivityActionV2
|
13
|
+
from fractal_server.exceptions import UnreachableBranchError
|
12
14
|
from fractal_server.logger import get_logger
|
13
15
|
from fractal_server.logger import reset_logger_handlers
|
14
16
|
from fractal_server.utils import get_timestamp
|
@@ -23,6 +25,31 @@ def add_commit_refresh(*, obj: T, db: DBSyncSession) -> T:
|
|
23
25
|
return obj
|
24
26
|
|
25
27
|
|
28
|
+
def get_activity_and_task_group(
|
29
|
+
*,
|
30
|
+
task_group_activity_id: int,
|
31
|
+
task_group_id: int,
|
32
|
+
db: DBSyncSession,
|
33
|
+
logger_name: str,
|
34
|
+
) -> tuple[bool, TaskGroupV2, TaskGroupActivityV2]:
|
35
|
+
task_group = db.get(TaskGroupV2, task_group_id)
|
36
|
+
activity = db.get(TaskGroupActivityV2, task_group_activity_id)
|
37
|
+
if activity is None or task_group is None:
|
38
|
+
logging.error(
|
39
|
+
"Cannot find database rows with "
|
40
|
+
f"{task_group_id=} and {task_group_activity_id=}:\n"
|
41
|
+
f"{task_group=}\n{activity=}. Exit."
|
42
|
+
)
|
43
|
+
return False, None, None
|
44
|
+
|
45
|
+
# Log some info about task group
|
46
|
+
logger = get_logger(logger_name=logger_name)
|
47
|
+
for key, value in task_group.model_dump(exclude={"env_info"}).items():
|
48
|
+
logger.debug(f"task_group.{key}: {value}")
|
49
|
+
|
50
|
+
return True, task_group, activity
|
51
|
+
|
52
|
+
|
26
53
|
def fail_and_cleanup(
|
27
54
|
task_group: TaskGroupV2,
|
28
55
|
task_group_activity: TaskGroupActivityV2,
|
@@ -47,11 +74,12 @@ def fail_and_cleanup(
|
|
47
74
|
reset_logger_handlers(logger)
|
48
75
|
|
49
76
|
|
50
|
-
def
|
77
|
+
def prepare_tasks_metadata(
|
51
78
|
*,
|
52
79
|
package_manifest: ManifestV2,
|
53
|
-
python_bin: Path,
|
54
80
|
package_root: Path,
|
81
|
+
python_bin: Path | None = None,
|
82
|
+
project_python_wrapper: Path | None = None,
|
55
83
|
package_version: str | None = None,
|
56
84
|
) -> list[TaskCreateV2]:
|
57
85
|
"""
|
@@ -59,10 +87,22 @@ def _prepare_tasks_metadata(
|
|
59
87
|
|
60
88
|
Args:
|
61
89
|
package_manifest:
|
62
|
-
python_bin:
|
63
90
|
package_root:
|
64
91
|
package_version:
|
92
|
+
python_bin:
|
93
|
+
project_python_wrapper:
|
65
94
|
"""
|
95
|
+
|
96
|
+
if bool(project_python_wrapper is None) == bool(python_bin is None):
|
97
|
+
raise UnreachableBranchError(
|
98
|
+
f"Either {project_python_wrapper} or {python_bin} must be set."
|
99
|
+
)
|
100
|
+
|
101
|
+
if python_bin is not None:
|
102
|
+
actual_python = python_bin
|
103
|
+
else:
|
104
|
+
actual_python = project_python_wrapper
|
105
|
+
|
66
106
|
task_list = []
|
67
107
|
for _task in package_manifest.task_list:
|
68
108
|
# Set non-command attributes
|
@@ -76,14 +116,16 @@ def _prepare_tasks_metadata(
|
|
76
116
|
# Set command attributes
|
77
117
|
if _task.executable_non_parallel is not None:
|
78
118
|
non_parallel_path = package_root / _task.executable_non_parallel
|
79
|
-
|
80
|
-
f"{
|
119
|
+
cmd_non_parallel = (
|
120
|
+
f"{actual_python.as_posix()} {non_parallel_path.as_posix()}"
|
81
121
|
)
|
122
|
+
task_attributes["command_non_parallel"] = cmd_non_parallel
|
82
123
|
if _task.executable_parallel is not None:
|
83
124
|
parallel_path = package_root / _task.executable_parallel
|
84
|
-
|
85
|
-
"
|
86
|
-
|
125
|
+
cmd_parallel = (
|
126
|
+
f"{actual_python.as_posix()} {parallel_path.as_posix()}"
|
127
|
+
)
|
128
|
+
task_attributes["command_parallel"] = cmd_parallel
|
87
129
|
# Create object
|
88
130
|
task_obj = TaskCreateV2(
|
89
131
|
**_task.model_dump(
|
@@ -0,0 +1,38 @@
|
|
1
|
+
from typing import TypedDict
|
2
|
+
|
3
|
+
SOURCE_DIR_NAME = "source_dir"
|
4
|
+
|
5
|
+
|
6
|
+
class ParsedOutput(TypedDict):
|
7
|
+
package_root: str
|
8
|
+
venv_size: str
|
9
|
+
venv_file_number: str
|
10
|
+
project_python_wrapper: str
|
11
|
+
|
12
|
+
|
13
|
+
def parse_collect_stdout(stdout: str) -> ParsedOutput:
|
14
|
+
"""
|
15
|
+
Parse standard output of `pixi/1_collect.sh`
|
16
|
+
"""
|
17
|
+
searches = [
|
18
|
+
("Package folder:", "package_root"),
|
19
|
+
("Disk usage:", "venv_size"),
|
20
|
+
("Number of files:", "venv_file_number"),
|
21
|
+
("Project Python wrapper:", "project_python_wrapper"),
|
22
|
+
]
|
23
|
+
stdout_lines = stdout.splitlines()
|
24
|
+
attributes = dict()
|
25
|
+
for search, attribute_name in searches:
|
26
|
+
matching_lines = [_line for _line in stdout_lines if search in _line]
|
27
|
+
if len(matching_lines) == 0:
|
28
|
+
raise ValueError(f"String '{search}' not found in stdout.")
|
29
|
+
elif len(matching_lines) > 1:
|
30
|
+
raise ValueError(
|
31
|
+
f"String '{search}' found too many times "
|
32
|
+
f"({len(matching_lines)})."
|
33
|
+
)
|
34
|
+
else:
|
35
|
+
actual_line = matching_lines[0]
|
36
|
+
attribute_value = actual_line.split(search)[-1].strip(" ")
|
37
|
+
attributes[attribute_name] = attribute_value
|
38
|
+
return attributes
|
@@ -12,10 +12,21 @@ SCRIPTS_SUBFOLDER = "scripts"
|
|
12
12
|
logger = set_logger(__name__)
|
13
13
|
|
14
14
|
|
15
|
+
def _check_pixi_frozen_option(replacements: list[tuple[str, str]]):
|
16
|
+
try:
|
17
|
+
replacement = next(
|
18
|
+
rep for rep in replacements if rep[0] == "__FROZEN_OPTION__"
|
19
|
+
)
|
20
|
+
if replacement[1] not in ["", "--frozen"]:
|
21
|
+
raise ValueError(f"Invalid {replacement=}.")
|
22
|
+
except StopIteration:
|
23
|
+
pass
|
24
|
+
|
25
|
+
|
15
26
|
def customize_template(
|
16
27
|
*,
|
17
28
|
template_name: str,
|
18
|
-
replacements:
|
29
|
+
replacements: set[tuple[str, str]],
|
19
30
|
script_path: str,
|
20
31
|
) -> str:
|
21
32
|
"""
|
@@ -26,6 +37,8 @@ def customize_template(
|
|
26
37
|
replacements: List of replacements for template customization.
|
27
38
|
script_path: Local path where the customized template will be written.
|
28
39
|
"""
|
40
|
+
_check_pixi_frozen_option(replacements=replacements)
|
41
|
+
|
29
42
|
# Read template
|
30
43
|
template_path = TEMPLATES_DIR / template_name
|
31
44
|
with template_path.open("r") as f:
|