fractal-server 2.14.16__py3-none-any.whl → 2.15.0a1__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 (41) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/v2/task_group.py +17 -5
  3. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +2 -2
  4. fractal_server/app/routes/api/v2/__init__.py +6 -0
  5. fractal_server/app/routes/api/v2/task_collection.py +3 -3
  6. fractal_server/app/routes/api/v2/task_collection_custom.py +2 -2
  7. fractal_server/app/routes/api/v2/task_collection_pixi.py +236 -0
  8. fractal_server/app/routes/api/v2/task_group_lifecycle.py +26 -7
  9. fractal_server/app/schemas/v2/__init__.py +2 -1
  10. fractal_server/app/schemas/v2/dumps.py +1 -1
  11. fractal_server/app/schemas/v2/task_collection.py +1 -1
  12. fractal_server/app/schemas/v2/task_group.py +16 -5
  13. fractal_server/config.py +42 -0
  14. fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py +53 -0
  15. fractal_server/ssh/_fabric.py +26 -0
  16. fractal_server/tasks/v2/local/__init__.py +3 -0
  17. fractal_server/tasks/v2/local/_utils.py +7 -2
  18. fractal_server/tasks/v2/local/collect.py +23 -24
  19. fractal_server/tasks/v2/local/collect_pixi.py +234 -0
  20. fractal_server/tasks/v2/local/deactivate.py +36 -39
  21. fractal_server/tasks/v2/local/deactivate_pixi.py +102 -0
  22. fractal_server/tasks/v2/local/reactivate.py +9 -16
  23. fractal_server/tasks/v2/local/reactivate_pixi.py +146 -0
  24. fractal_server/tasks/v2/ssh/__init__.py +3 -0
  25. fractal_server/tasks/v2/ssh/_utils.py +5 -5
  26. fractal_server/tasks/v2/ssh/collect.py +23 -28
  27. fractal_server/tasks/v2/ssh/collect_pixi.py +306 -0
  28. fractal_server/tasks/v2/ssh/deactivate.py +39 -45
  29. fractal_server/tasks/v2/ssh/deactivate_pixi.py +128 -0
  30. fractal_server/tasks/v2/ssh/reactivate.py +8 -15
  31. fractal_server/tasks/v2/ssh/reactivate_pixi.py +108 -0
  32. fractal_server/tasks/v2/templates/pixi_1_extract.sh +40 -0
  33. fractal_server/tasks/v2/templates/pixi_2_install.sh +48 -0
  34. fractal_server/tasks/v2/templates/pixi_3_post_install.sh +80 -0
  35. fractal_server/tasks/v2/utils_background.py +43 -8
  36. fractal_server/tasks/v2/utils_pixi.py +38 -0
  37. {fractal_server-2.14.16.dist-info → fractal_server-2.15.0a1.dist-info}/METADATA +1 -1
  38. {fractal_server-2.14.16.dist-info → fractal_server-2.15.0a1.dist-info}/RECORD +41 -29
  39. {fractal_server-2.14.16.dist-info → fractal_server-2.15.0a1.dist-info}/LICENSE +0 -0
  40. {fractal_server-2.14.16.dist-info → fractal_server-2.15.0a1.dist-info}/WHEEL +0 -0
  41. {fractal_server-2.14.16.dist-info → fractal_server-2.15.0a1.dist-info}/entry_points.txt +0 -0
@@ -204,6 +204,32 @@ class FractalSSH:
204
204
  self.logger.info(f"END reading remote JSON file {filepath}.")
205
205
  return data
206
206
 
207
+ def read_remote_text_file(self, filepath: str) -> dict[str, Any]:
208
+ """
209
+ Read a remote text file into a string.
210
+
211
+ Note from paramiko docs:
212
+ > The Python 'b' flag is ignored, since SSH treats all files as binary.
213
+ """
214
+ self.logger.info(f"START reading remote text file {filepath}.")
215
+ with _acquire_lock_with_timeout(
216
+ lock=self._lock,
217
+ label="read_remote_text_file",
218
+ timeout=self.default_lock_timeout,
219
+ ):
220
+ try:
221
+ with self._sftp_unsafe().open(filepath, "r") as f:
222
+ data = f.read().decode()
223
+ except Exception as e:
224
+ self.log_and_raise(
225
+ e=e,
226
+ message=(
227
+ f"Error in `read_remote_text_file`, for {filepath=}."
228
+ ),
229
+ )
230
+ self.logger.info(f"END reading remote text file {filepath}.")
231
+ return data
232
+
207
233
  def check_connection(self) -> None:
208
234
  """
209
235
  Open the SSH connection and handle exceptions.
@@ -1,3 +1,6 @@
1
1
  from .collect import collect_local # noqa
2
+ from .collect_pixi import collect_local_pixi # noqa
2
3
  from .deactivate import deactivate_local # noqa
4
+ from .deactivate_pixi import deactivate_local_pixi # noqa
3
5
  from .reactivate import reactivate_local # noqa
6
+ from .reactivate_pixi import reactivate_local_pixi # noqa
@@ -50,19 +50,24 @@ def check_task_files_exist(task_list: list[TaskCreateV2]) -> None:
50
50
  """
51
51
  Check that the modules listed in task commands point to existing files.
52
52
 
53
+ Note: commands may be like `/one/python /another/task.py` or
54
+ `/one/pixi [...] /another/task.py`, and in both cases `split()[-1]`
55
+ returns `/another/task.py`.
56
+
53
57
  Args:
54
58
  task_list:
55
59
  """
60
+
56
61
  for _task in task_list:
57
62
  if _task.command_non_parallel is not None:
58
- _task_path = _task.command_non_parallel.split()[1]
63
+ _task_path = _task.command_non_parallel.split()[-1]
59
64
  if not Path(_task_path).exists():
60
65
  raise FileNotFoundError(
61
66
  f"Task `{_task.name}` has `command_non_parallel` "
62
67
  f"pointing to missing file `{_task_path}`."
63
68
  )
64
69
  if _task.command_parallel is not None:
65
- _task_path = _task.command_parallel.split()[1]
70
+ _task_path = _task.command_parallel.split()[-1]
66
71
  if not Path(_task_path).exists():
67
72
  raise FileNotFoundError(
68
73
  f"Task `{_task.name}` has `command_parallel` "
@@ -1,5 +1,4 @@
1
1
  import json
2
- import logging
3
2
  import shutil
4
3
  import time
5
4
  from pathlib import Path
@@ -8,20 +7,22 @@ from tempfile import TemporaryDirectory
8
7
  from ..utils_database import create_db_tasks_and_update_task_group_sync
9
8
  from ._utils import _customize_and_run_template
10
9
  from fractal_server.app.db import get_sync_db
11
- from fractal_server.app.models.v2 import TaskGroupActivityV2
12
10
  from fractal_server.app.models.v2 import TaskGroupV2
11
+ from fractal_server.app.schemas.v2 import FractalUploadedFile
13
12
  from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
14
13
  from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
15
- from fractal_server.app.schemas.v2 import WheelFile
16
14
  from fractal_server.app.schemas.v2.manifest import ManifestV2
17
15
  from fractal_server.logger import reset_logger_handlers
18
16
  from fractal_server.logger import set_logger
19
17
  from fractal_server.tasks.utils import get_log_path
20
18
  from fractal_server.tasks.v2.local._utils import check_task_files_exist
21
- from fractal_server.tasks.v2.utils_background import _prepare_tasks_metadata
22
19
  from fractal_server.tasks.v2.utils_background import add_commit_refresh
23
20
  from fractal_server.tasks.v2.utils_background import fail_and_cleanup
21
+ from fractal_server.tasks.v2.utils_background import (
22
+ get_activity_and_task_group,
23
+ )
24
24
  from fractal_server.tasks.v2.utils_background import get_current_log
25
+ from fractal_server.tasks.v2.utils_background import prepare_tasks_metadata
25
26
  from fractal_server.tasks.v2.utils_package_names import compare_package_names
26
27
  from fractal_server.tasks.v2.utils_python_interpreter import (
27
28
  get_python_interpreter_v2,
@@ -38,7 +39,7 @@ def collect_local(
38
39
  *,
39
40
  task_group_activity_id: int,
40
41
  task_group_id: int,
41
- wheel_file: WheelFile | None = None,
42
+ wheel_file: FractalUploadedFile | None = None,
42
43
  ) -> None:
43
44
  """
44
45
  Collect a task package.
@@ -67,16 +68,12 @@ def collect_local(
67
68
  )
68
69
 
69
70
  with next(get_sync_db()) as db:
70
- # Get main objects from db
71
- activity = db.get(TaskGroupActivityV2, task_group_activity_id)
72
- task_group = db.get(TaskGroupV2, task_group_id)
73
- if activity is None or task_group is None:
74
- # Use `logging` directly
75
- logging.error(
76
- "Cannot find database rows with "
77
- f"{task_group_id=} and {task_group_activity_id=}:\n"
78
- f"{task_group=}\n{activity=}. Exit."
79
- )
71
+ success, task_group, activity = get_activity_and_task_group(
72
+ task_group_activity_id=task_group_activity_id,
73
+ task_group_id=task_group_id,
74
+ db=db,
75
+ )
76
+ if not success:
80
77
  return
81
78
 
82
79
  # Log some info
@@ -103,16 +100,18 @@ def collect_local(
103
100
  Path(task_group.path).mkdir(parents=True)
104
101
  logger.info(f"Created {task_group.path}")
105
102
 
106
- # Write wheel file and set task_group.wheel_path
103
+ # Write wheel file and set task_group.archive_path
107
104
  if wheel_file is not None:
108
105
 
109
- wheel_path = (
106
+ archive_path = (
110
107
  Path(task_group.path) / wheel_file.filename
111
108
  ).as_posix()
112
- logger.info(f"Write wheel-file contents into {wheel_path}")
113
- with open(wheel_path, "wb") as f:
109
+ logger.info(
110
+ f"Write wheel-file contents into {archive_path}"
111
+ )
112
+ with open(archive_path, "wb") as f:
114
113
  f.write(wheel_file.contents)
115
- task_group.wheel_path = wheel_path
114
+ task_group.archive_path = archive_path
116
115
  task_group = add_commit_refresh(obj=task_group, db=db)
117
116
 
118
117
  # Prepare replacements for templates
@@ -220,7 +219,7 @@ def collect_local(
220
219
  activity = add_commit_refresh(obj=activity, db=db)
221
220
 
222
221
  logger.info("_prepare_tasks_metadata - start")
223
- task_list = _prepare_tasks_metadata(
222
+ task_list = prepare_tasks_metadata(
224
223
  package_manifest=pkg_manifest,
225
224
  package_version=task_group.version,
226
225
  package_root=Path(package_root),
@@ -241,15 +240,15 @@ def collect_local(
241
240
 
242
241
  # Update task_group data
243
242
  logger.info(
244
- "Add pip_freeze, venv_size and venv_file_number "
243
+ "Add env_info, venv_size and venv_file_number "
245
244
  "to TaskGroupV2 - start"
246
245
  )
247
- task_group.pip_freeze = pip_freeze_stdout
246
+ task_group.env_info = pip_freeze_stdout
248
247
  task_group.venv_size_in_kB = int(venv_size)
249
248
  task_group.venv_file_number = int(venv_file_number)
250
249
  task_group = add_commit_refresh(obj=task_group, db=db)
251
250
  logger.info(
252
- "Add pip_freeze, venv_size and venv_file_number "
251
+ "Add env_info, venv_size and venv_file_number "
253
252
  "to TaskGroupV2 - end"
254
253
  )
255
254
 
@@ -0,0 +1,234 @@
1
+ import json
2
+ import shutil
3
+ import time
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+
7
+ from ..utils_database import create_db_tasks_and_update_task_group_sync
8
+ from ..utils_pixi import parse_collect_stdout
9
+ from ..utils_pixi import SOURCE_DIR_NAME
10
+ from fractal_server.app.db import get_sync_db
11
+ from fractal_server.app.schemas.v2 import FractalUploadedFile
12
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
13
+ from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
14
+ from fractal_server.app.schemas.v2.manifest import ManifestV2
15
+ from fractal_server.config import get_settings
16
+ from fractal_server.logger import reset_logger_handlers
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.local._utils import _customize_and_run_template
21
+ from fractal_server.tasks.v2.local._utils import check_task_files_exist
22
+ from fractal_server.tasks.v2.utils_background import add_commit_refresh
23
+ from fractal_server.tasks.v2.utils_background import fail_and_cleanup
24
+ from fractal_server.tasks.v2.utils_background import (
25
+ get_activity_and_task_group,
26
+ )
27
+ from fractal_server.tasks.v2.utils_background import get_current_log
28
+ from fractal_server.tasks.v2.utils_background import prepare_tasks_metadata
29
+ from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
30
+ from fractal_server.utils import get_timestamp
31
+
32
+
33
+ def collect_local_pixi(
34
+ *,
35
+ task_group_activity_id: int,
36
+ task_group_id: int,
37
+ tar_gz_file: FractalUploadedFile,
38
+ ) -> None:
39
+ settings = Inject(get_settings)
40
+
41
+ LOGGER_NAME = f"{__name__}.ID{task_group_activity_id}"
42
+
43
+ with TemporaryDirectory() as tmpdir:
44
+ log_file_path = get_log_path(Path(tmpdir))
45
+ logger = set_logger(
46
+ logger_name=LOGGER_NAME,
47
+ log_file_path=log_file_path,
48
+ )
49
+
50
+ with next(get_sync_db()) as db:
51
+ success, task_group, activity = get_activity_and_task_group(
52
+ task_group_activity_id=task_group_activity_id,
53
+ task_group_id=task_group_id,
54
+ db=db,
55
+ )
56
+ if not success:
57
+ return
58
+
59
+ logger.info("START")
60
+ for key, value in task_group.model_dump().items():
61
+ logger.debug(f"task_group.{key}: {value}")
62
+
63
+ if Path(task_group.path).exists():
64
+ error_msg = f"{task_group.path} already exists."
65
+ logger.error(error_msg)
66
+ fail_and_cleanup(
67
+ task_group=task_group,
68
+ task_group_activity=activity,
69
+ logger_name=LOGGER_NAME,
70
+ log_file_path=log_file_path,
71
+ exception=FileExistsError(error_msg),
72
+ db=db,
73
+ )
74
+ return
75
+
76
+ # Set `pixi_home`
77
+ pixi_home = settings.pixi.versions[task_group.pixi_version]
78
+
79
+ try:
80
+ Path(task_group.path).mkdir(parents=True)
81
+ logger.info(f"Created {task_group.path}")
82
+ archive_path = Path(
83
+ task_group.path, tar_gz_file.filename
84
+ ).as_posix()
85
+ logger.info(f"Write tar.gz-file contents into {archive_path}.")
86
+ with open(archive_path, "wb") as f:
87
+ f.write(tar_gz_file.contents)
88
+ task_group.archive_path = archive_path
89
+ task_group = add_commit_refresh(obj=task_group, db=db)
90
+
91
+ common_args = dict(
92
+ replacements={
93
+ ("__PIXI_HOME__", pixi_home),
94
+ ("__PACKAGE_DIR__", task_group.path),
95
+ ("__TAR_GZ_PATH__", archive_path),
96
+ (
97
+ "__IMPORT_PACKAGE_NAME__",
98
+ task_group.pkg_name.replace("-", "_"),
99
+ ),
100
+ ("__SOURCE_DIR_NAME__", SOURCE_DIR_NAME),
101
+ },
102
+ script_dir=Path(
103
+ task_group.path, SCRIPTS_SUBFOLDER
104
+ ).as_posix(),
105
+ prefix=(
106
+ f"{int(time.time())}_"
107
+ f"{TaskGroupActivityActionV2.COLLECT}_"
108
+ ),
109
+ logger_name=LOGGER_NAME,
110
+ )
111
+
112
+ activity.status = TaskGroupActivityStatusV2.ONGOING
113
+ activity.log = get_current_log(log_file_path)
114
+ activity = add_commit_refresh(obj=activity, db=db)
115
+
116
+ # Run script 1
117
+ _customize_and_run_template(
118
+ template_filename="pixi_1_extract.sh",
119
+ **common_args,
120
+ )
121
+ activity.log = get_current_log(log_file_path)
122
+ activity = add_commit_refresh(obj=activity, db=db)
123
+
124
+ # Run script 2
125
+ _customize_and_run_template(
126
+ template_filename="pixi_2_install.sh",
127
+ **common_args,
128
+ )
129
+ activity.log = get_current_log(log_file_path)
130
+ activity = add_commit_refresh(obj=activity, db=db)
131
+
132
+ # Run script 3
133
+ stdout = _customize_and_run_template(
134
+ template_filename="pixi_3_post_install.sh",
135
+ **common_args,
136
+ )
137
+ activity.log = get_current_log(log_file_path)
138
+ activity = add_commit_refresh(obj=activity, db=db)
139
+
140
+ # Parse stdout
141
+ parsed_output = parse_collect_stdout(stdout)
142
+ package_root = parsed_output["package_root"]
143
+ venv_size = parsed_output["venv_size"]
144
+ venv_file_number = parsed_output["venv_file_number"]
145
+ project_python_wrapper = parsed_output[
146
+ "project_python_wrapper"
147
+ ]
148
+
149
+ # Read and validate manifest
150
+ # NOTE: we are only supporting the manifest path being relative
151
+ # to the top-level folder
152
+ manifest_path = f"{package_root}/__FRACTAL_MANIFEST__.json"
153
+ with open(manifest_path) as json_data:
154
+ pkg_manifest_dict = json.load(json_data)
155
+ logger.info(f"loaded {manifest_path=}")
156
+ logger.info("now validating manifest content")
157
+ pkg_manifest = ManifestV2(**pkg_manifest_dict)
158
+ logger.info("validated manifest content")
159
+ activity.log = get_current_log(log_file_path)
160
+ activity = add_commit_refresh(obj=activity, db=db)
161
+
162
+ logger.info("_prepare_tasks_metadata - start")
163
+ task_list = prepare_tasks_metadata(
164
+ package_manifest=pkg_manifest,
165
+ package_version=task_group.version,
166
+ package_root=Path(package_root),
167
+ project_python_wrapper=Path(project_python_wrapper),
168
+ )
169
+ check_task_files_exist(task_list=task_list)
170
+ logger.info("_prepare_tasks_metadata - end")
171
+ activity.log = get_current_log(log_file_path)
172
+ activity = add_commit_refresh(obj=activity, db=db)
173
+
174
+ logger.info("create_db_tasks_and_update_task_group - start")
175
+ create_db_tasks_and_update_task_group_sync(
176
+ task_list=task_list,
177
+ task_group_id=task_group.id,
178
+ db=db,
179
+ )
180
+ logger.info("create_db_tasks_and_update_task_group - end")
181
+
182
+ # Update task_group data
183
+ logger.info(
184
+ "Add env_info, venv_size and venv_file_number "
185
+ "to TaskGroupV2 - start"
186
+ )
187
+ with Path(
188
+ task_group.path,
189
+ SOURCE_DIR_NAME,
190
+ "pixi.lock",
191
+ ).open() as f:
192
+ pixi_lock_contents = f.read()
193
+
194
+ # NOTE: see issue 2626 about whether to keep `pixi.lock` files
195
+ # in the database
196
+ task_group.env_info = pixi_lock_contents
197
+ task_group.venv_size_in_kB = int(venv_size)
198
+ task_group.venv_file_number = int(venv_file_number)
199
+ task_group = add_commit_refresh(obj=task_group, db=db)
200
+ logger.info(
201
+ "Add env_info, venv_size and venv_file_number "
202
+ "to TaskGroupV2 - end"
203
+ )
204
+
205
+ # Finalize (write metadata to DB)
206
+ logger.info("finalising - START")
207
+ activity.status = TaskGroupActivityStatusV2.OK
208
+ activity.timestamp_ended = get_timestamp()
209
+ activity = add_commit_refresh(obj=activity, db=db)
210
+ logger.info("finalising - END")
211
+ logger.info("END")
212
+
213
+ reset_logger_handlers(logger)
214
+
215
+ except Exception as collection_e:
216
+ # Delete corrupted package dir
217
+ try:
218
+ logger.info(f"Now delete folder {task_group.path}")
219
+ shutil.rmtree(task_group.path)
220
+ logger.info(f"Deleted folder {task_group.path}")
221
+ except Exception as rm_e:
222
+ logger.error(
223
+ "Removing folder failed.\n"
224
+ f"Original error:\n{str(rm_e)}"
225
+ )
226
+
227
+ fail_and_cleanup(
228
+ task_group=task_group,
229
+ task_group_activity=activity,
230
+ logger_name=LOGGER_NAME,
231
+ log_file_path=log_file_path,
232
+ exception=collection_e,
233
+ db=db,
234
+ )
@@ -1,4 +1,3 @@
1
- import logging
2
1
  import shutil
3
2
  import time
4
3
  from pathlib import Path
@@ -6,11 +5,10 @@ from tempfile import TemporaryDirectory
6
5
 
7
6
  from ..utils_background import add_commit_refresh
8
7
  from ..utils_background import fail_and_cleanup
8
+ from ..utils_background import get_activity_and_task_group
9
9
  from ..utils_templates import get_collection_replacements
10
10
  from ._utils import _customize_and_run_template
11
11
  from fractal_server.app.db import get_sync_db
12
- from fractal_server.app.models.v2 import TaskGroupActivityV2
13
- from fractal_server.app.models.v2 import TaskGroupV2
14
12
  from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
15
13
  from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
16
14
  from fractal_server.app.schemas.v2.task_group import TaskGroupActivityStatusV2
@@ -49,17 +47,12 @@ def deactivate_local(
49
47
  )
50
48
 
51
49
  with next(get_sync_db()) as db:
52
-
53
- # Get main objects from db
54
- activity = db.get(TaskGroupActivityV2, task_group_activity_id)
55
- task_group = db.get(TaskGroupV2, task_group_id)
56
- if activity is None or task_group is None:
57
- # Use `logging` directly
58
- logging.error(
59
- "Cannot find database rows with "
60
- f"{task_group_id=} and {task_group_activity_id=}:\n"
61
- f"{task_group=}\n{activity=}. Exit."
62
- )
50
+ success, task_group, activity = get_activity_and_task_group(
51
+ task_group_activity_id=task_group_activity_id,
52
+ task_group_id=task_group_id,
53
+ db=db,
54
+ )
55
+ if not success:
63
56
  return
64
57
 
65
58
  # Log some info
@@ -87,10 +80,10 @@ def deactivate_local(
87
80
  activity.status = TaskGroupActivityStatusV2.ONGOING
88
81
  activity = add_commit_refresh(obj=activity, db=db)
89
82
 
90
- if task_group.pip_freeze is None:
83
+ if task_group.env_info is None:
91
84
  logger.warning(
92
85
  "Recreate pip-freeze information, since "
93
- f"{task_group.pip_freeze=}. NOTE: this should only "
86
+ f"{task_group.env_info=}. NOTE: this should only "
94
87
  "happen for task groups created before 2.9.0."
95
88
  )
96
89
  # Prepare replacements for templates
@@ -120,7 +113,7 @@ def deactivate_local(
120
113
  logger.info("Add pip freeze stdout to TaskGroupV2 - start")
121
114
  activity.log = get_current_log(log_file_path)
122
115
  activity = add_commit_refresh(obj=activity, db=db)
123
- task_group.pip_freeze = pip_freeze_stdout
116
+ task_group.env_info = pip_freeze_stdout
124
117
  task_group = add_commit_refresh(obj=task_group, db=db)
125
118
  logger.info("Add pip freeze stdout to TaskGroupV2 - end")
126
119
 
@@ -131,15 +124,15 @@ def deactivate_local(
131
124
  f"Handle specific cases for {task_group.origin=}."
132
125
  )
133
126
 
134
- # Blocking situation: `wheel_path` is not set or points
127
+ # Blocking situation: `archive_path` is not set or points
135
128
  # to a missing path
136
129
  if (
137
- task_group.wheel_path is None
138
- or not Path(task_group.wheel_path).exists()
130
+ task_group.archive_path is None
131
+ or not Path(task_group.archive_path).exists()
139
132
  ):
140
133
  error_msg = (
141
134
  "Invalid wheel path for task group with "
142
- f"{task_group_id=}. {task_group.wheel_path=} is "
135
+ f"{task_group_id=}. {task_group.archive_path=} is "
143
136
  "unset or does not exist."
144
137
  )
145
138
  logger.error(error_msg)
@@ -153,48 +146,52 @@ def deactivate_local(
153
146
  )
154
147
  return
155
148
 
156
- # Recoverable situation: `wheel_path` was not yet copied
149
+ # Recoverable situation: `archive_path` was not yet copied
157
150
  # over to the correct server-side folder
158
- wheel_path_parent_dir = Path(task_group.wheel_path).parent
159
- if wheel_path_parent_dir != Path(task_group.path):
151
+ archive_path_parent_dir = Path(
152
+ task_group.archive_path
153
+ ).parent
154
+ if archive_path_parent_dir != Path(task_group.path):
160
155
  logger.warning(
161
- f"{wheel_path_parent_dir.as_posix()} differs from "
162
- f"{task_group.path}. NOTE: this should only "
156
+ f"{archive_path_parent_dir.as_posix()} differs "
157
+ f"from {task_group.path}. NOTE: this should only "
163
158
  "happen for task groups created before 2.9.0."
164
159
  )
165
160
 
166
- if task_group.wheel_path not in task_group.pip_freeze:
161
+ if task_group.archive_path not in task_group.env_info:
167
162
  raise ValueError(
168
- f"Cannot find {task_group.wheel_path=} in "
163
+ f"Cannot find {task_group.archive_path=} in "
169
164
  "pip-freeze data. Exit."
170
165
  )
171
166
 
172
167
  logger.info(
173
168
  f"Now copy wheel file into {task_group.path}."
174
169
  )
175
- new_wheel_path = (
170
+ new_archive_path = (
176
171
  Path(task_group.path)
177
- / Path(task_group.wheel_path).name
172
+ / Path(task_group.archive_path).name
178
173
  ).as_posix()
179
- shutil.copy(task_group.wheel_path, new_wheel_path)
180
- logger.info(f"Copied wheel file to {new_wheel_path}.")
174
+ shutil.copy(task_group.archive_path, new_archive_path)
175
+ logger.info(
176
+ f"Copied wheel file to {new_archive_path}."
177
+ )
181
178
 
182
- task_group.wheel_path = new_wheel_path
183
- new_pip_freeze = task_group.pip_freeze.replace(
184
- task_group.wheel_path,
185
- new_wheel_path,
179
+ task_group.archive_path = new_archive_path
180
+ new_pip_freeze = task_group.env_info.replace(
181
+ task_group.archive_path,
182
+ new_archive_path,
186
183
  )
187
- task_group.pip_freeze = new_pip_freeze
184
+ task_group.env_info = new_pip_freeze
188
185
  task_group = add_commit_refresh(obj=task_group, db=db)
189
186
  logger.info(
190
- "Updated `wheel_path` and `pip_freeze` "
187
+ "Updated `archive_path` and `env_info` "
191
188
  "task-group attributes."
192
189
  )
193
190
 
194
191
  # Fail if `pip_freeze` includes "github.com", see
195
192
  # https://github.com/fractal-analytics-platform/fractal-server/issues/2142
196
193
  for forbidden_string in FORBIDDEN_DEPENDENCY_STRINGS:
197
- if forbidden_string in task_group.pip_freeze:
194
+ if forbidden_string in task_group.env_info:
198
195
  raise ValueError(
199
196
  "Deactivation and reactivation of task packages "
200
197
  f"with direct {forbidden_string} dependencies "
@@ -0,0 +1,102 @@
1
+ import shutil
2
+ from pathlib import Path
3
+ from tempfile import TemporaryDirectory
4
+
5
+ from ..utils_background import add_commit_refresh
6
+ from ..utils_background import fail_and_cleanup
7
+ from ..utils_background import get_activity_and_task_group
8
+ from ..utils_pixi import SOURCE_DIR_NAME
9
+ from fractal_server.app.db import get_sync_db
10
+ from fractal_server.app.schemas.v2.task_group import TaskGroupActivityStatusV2
11
+ from fractal_server.logger import reset_logger_handlers
12
+ from fractal_server.logger import set_logger
13
+ from fractal_server.tasks.utils import get_log_path
14
+ from fractal_server.tasks.v2.utils_background import get_current_log
15
+ from fractal_server.utils import get_timestamp
16
+
17
+
18
+ def deactivate_local_pixi(
19
+ *,
20
+ task_group_activity_id: int,
21
+ task_group_id: int,
22
+ ) -> None:
23
+ """
24
+ Deactivate a pixi task group venv.
25
+
26
+ This function is run as a background task, therefore exceptions must be
27
+ handled.
28
+
29
+ Arguments:
30
+ task_group_id:
31
+ task_group_activity_id:
32
+ """
33
+
34
+ LOGGER_NAME = f"{__name__}.ID{task_group_activity_id}"
35
+
36
+ with TemporaryDirectory() as tmpdir:
37
+ log_file_path = get_log_path(Path(tmpdir))
38
+ logger = set_logger(
39
+ logger_name=LOGGER_NAME,
40
+ log_file_path=log_file_path,
41
+ )
42
+
43
+ with next(get_sync_db()) as db:
44
+ success, task_group, activity = get_activity_and_task_group(
45
+ task_group_activity_id=task_group_activity_id,
46
+ task_group_id=task_group_id,
47
+ db=db,
48
+ )
49
+ if not success:
50
+ return
51
+
52
+ # Log some info
53
+ logger.debug("START")
54
+
55
+ for key, value in task_group.model_dump().items():
56
+ logger.debug(f"task_group.{key}: {value}")
57
+
58
+ source_dir = Path(task_group.path, SOURCE_DIR_NAME)
59
+ if not source_dir.exists():
60
+ error_msg = f"'{source_dir.as_posix()}' does not exist."
61
+ logger.error(error_msg)
62
+ fail_and_cleanup(
63
+ task_group=task_group,
64
+ task_group_activity=activity,
65
+ logger_name=LOGGER_NAME,
66
+ log_file_path=log_file_path,
67
+ exception=FileNotFoundError(error_msg),
68
+ db=db,
69
+ )
70
+ return
71
+
72
+ try:
73
+
74
+ activity.status = TaskGroupActivityStatusV2.ONGOING
75
+ activity = add_commit_refresh(obj=activity, db=db)
76
+
77
+ # Actually mark the task group as non-active
78
+ logger.info("Now setting `active=False`.")
79
+ task_group.active = False
80
+ task_group = add_commit_refresh(obj=task_group, db=db)
81
+
82
+ # Proceed with deactivation
83
+ logger.info(f"Now removing '{source_dir.as_posix()}'.")
84
+ shutil.rmtree(source_dir)
85
+ logger.info(f"All good, '{source_dir.as_posix()}' removed.")
86
+ activity.status = TaskGroupActivityStatusV2.OK
87
+ activity.log = get_current_log(log_file_path)
88
+ activity.timestamp_ended = get_timestamp()
89
+ activity = add_commit_refresh(obj=activity, db=db)
90
+
91
+ reset_logger_handlers(logger)
92
+
93
+ except Exception as e:
94
+ fail_and_cleanup(
95
+ task_group=task_group,
96
+ task_group_activity=activity,
97
+ logger_name=LOGGER_NAME,
98
+ log_file_path=log_file_path,
99
+ exception=e,
100
+ db=db,
101
+ )
102
+ return