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.
Files changed (54) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/security.py +2 -2
  3. fractal_server/app/models/user_settings.py +2 -2
  4. fractal_server/app/models/v2/dataset.py +3 -3
  5. fractal_server/app/models/v2/job.py +6 -6
  6. fractal_server/app/models/v2/task.py +12 -8
  7. fractal_server/app/models/v2/task_group.py +19 -7
  8. fractal_server/app/models/v2/workflowtask.py +6 -6
  9. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +2 -5
  10. fractal_server/app/routes/api/v2/__init__.py +6 -0
  11. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +22 -0
  12. fractal_server/app/routes/api/v2/task_collection.py +8 -18
  13. fractal_server/app/routes/api/v2/task_collection_custom.py +2 -2
  14. fractal_server/app/routes/api/v2/task_collection_pixi.py +219 -0
  15. fractal_server/app/routes/api/v2/task_group.py +3 -0
  16. fractal_server/app/routes/api/v2/task_group_lifecycle.py +26 -10
  17. fractal_server/app/runner/executors/slurm_common/_slurm_config.py +10 -0
  18. fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py +39 -14
  19. fractal_server/app/runner/executors/slurm_common/get_slurm_config.py +8 -1
  20. fractal_server/app/schemas/v2/__init__.py +1 -1
  21. fractal_server/app/schemas/v2/dumps.py +1 -1
  22. fractal_server/app/schemas/v2/task_collection.py +1 -1
  23. fractal_server/app/schemas/v2/task_group.py +7 -5
  24. fractal_server/config.py +70 -0
  25. fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py +53 -0
  26. fractal_server/migrations/versions/b3ffb095f973_json_to_jsonb.py +340 -0
  27. fractal_server/ssh/_fabric.py +26 -0
  28. fractal_server/tasks/v2/local/__init__.py +3 -0
  29. fractal_server/tasks/v2/local/_utils.py +4 -3
  30. fractal_server/tasks/v2/local/collect.py +26 -30
  31. fractal_server/tasks/v2/local/collect_pixi.py +252 -0
  32. fractal_server/tasks/v2/local/deactivate.py +39 -46
  33. fractal_server/tasks/v2/local/deactivate_pixi.py +98 -0
  34. fractal_server/tasks/v2/local/reactivate.py +12 -23
  35. fractal_server/tasks/v2/local/reactivate_pixi.py +184 -0
  36. fractal_server/tasks/v2/ssh/__init__.py +3 -0
  37. fractal_server/tasks/v2/ssh/_utils.py +50 -9
  38. fractal_server/tasks/v2/ssh/collect.py +46 -56
  39. fractal_server/tasks/v2/ssh/collect_pixi.py +315 -0
  40. fractal_server/tasks/v2/ssh/deactivate.py +54 -67
  41. fractal_server/tasks/v2/ssh/deactivate_pixi.py +122 -0
  42. fractal_server/tasks/v2/ssh/reactivate.py +25 -38
  43. fractal_server/tasks/v2/ssh/reactivate_pixi.py +233 -0
  44. fractal_server/tasks/v2/templates/pixi_1_extract.sh +40 -0
  45. fractal_server/tasks/v2/templates/pixi_2_install.sh +52 -0
  46. fractal_server/tasks/v2/templates/pixi_3_post_install.sh +76 -0
  47. fractal_server/tasks/v2/utils_background.py +50 -8
  48. fractal_server/tasks/v2/utils_pixi.py +38 -0
  49. fractal_server/tasks/v2/utils_templates.py +14 -1
  50. {fractal_server-2.14.16.dist-info → fractal_server-2.15.0.dist-info}/METADATA +1 -1
  51. {fractal_server-2.14.16.dist-info → fractal_server-2.15.0.dist-info}/RECORD +54 -41
  52. {fractal_server-2.14.16.dist-info → fractal_server-2.15.0.dist-info}/LICENSE +0 -0
  53. {fractal_server-2.14.16.dist-info → fractal_server-2.15.0.dist-info}/WHEEL +0 -0
  54. {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
- with SingleUseFractalSSH(
60
- ssh_config=ssh_config,
61
- logger_name=LOGGER_NAME,
62
- ) as fractal_ssh:
63
-
64
- with next(get_sync_db()) as db:
65
-
66
- # Get main objects from db
67
- activity = db.get(TaskGroupActivityV2, task_group_activity_id)
68
- task_group = db.get(TaskGroupV2, task_group_id)
69
- if activity is None or task_group is None:
70
- # Use `logging` directly
71
- logging.error(
72
- "Cannot find database rows with "
73
- f"{task_group_id=} and {task_group_activity_id=}:\n"
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
- fractal_ssh.check_connection()
86
- except Exception as e:
87
- logger.error("Cannot establish SSH connection.")
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
- return
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.pip_freeze)
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.\n"
203
- f"Original error:\n{str(rm_e)}"
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 _prepare_tasks_metadata(
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
- task_attributes["command_non_parallel"] = (
80
- f"{python_bin.as_posix()} " f"{non_parallel_path.as_posix()}"
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
- task_attributes[
85
- "command_parallel"
86
- ] = f"{python_bin.as_posix()} {parallel_path.as_posix()}"
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: list[tuple[str, str]],
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: