fractal-server 2.14.15__py3-none-any.whl → 2.15.0a0__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 (43) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/v2/history.py +2 -0
  3. fractal_server/app/models/v2/task_group.py +17 -5
  4. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +2 -2
  5. fractal_server/app/routes/api/v2/__init__.py +6 -0
  6. fractal_server/app/routes/api/v2/history.py +2 -2
  7. fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
  8. fractal_server/app/routes/api/v2/task_collection.py +3 -3
  9. fractal_server/app/routes/api/v2/task_collection_custom.py +2 -2
  10. fractal_server/app/routes/api/v2/task_collection_pixi.py +236 -0
  11. fractal_server/app/routes/api/v2/task_group_lifecycle.py +8 -3
  12. fractal_server/app/runner/executors/slurm_ssh/runner.py +3 -1
  13. fractal_server/app/runner/v2/runner.py +2 -2
  14. fractal_server/app/schemas/v2/__init__.py +2 -1
  15. fractal_server/app/schemas/v2/dumps.py +1 -1
  16. fractal_server/app/schemas/v2/task_collection.py +1 -1
  17. fractal_server/app/schemas/v2/task_group.py +16 -5
  18. fractal_server/config.py +42 -0
  19. fractal_server/images/status_tools.py +80 -75
  20. fractal_server/migrations/versions/791ce783d3d8_add_indices.py +41 -0
  21. fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py +53 -0
  22. fractal_server/ssh/_fabric.py +3 -0
  23. fractal_server/tasks/v2/local/__init__.py +2 -0
  24. fractal_server/tasks/v2/local/_utils.py +7 -2
  25. fractal_server/tasks/v2/local/collect.py +14 -12
  26. fractal_server/tasks/v2/local/collect_pixi.py +222 -0
  27. fractal_server/tasks/v2/local/deactivate.py +29 -25
  28. fractal_server/tasks/v2/local/deactivate_pixi.py +110 -0
  29. fractal_server/tasks/v2/local/reactivate.py +1 -1
  30. fractal_server/tasks/v2/ssh/__init__.py +1 -0
  31. fractal_server/tasks/v2/ssh/_utils.py +5 -5
  32. fractal_server/tasks/v2/ssh/collect.py +16 -15
  33. fractal_server/tasks/v2/ssh/collect_pixi.py +296 -0
  34. fractal_server/tasks/v2/ssh/deactivate.py +32 -31
  35. fractal_server/tasks/v2/ssh/reactivate.py +1 -1
  36. fractal_server/tasks/v2/templates/pixi_1_collect.sh +70 -0
  37. fractal_server/tasks/v2/utils_background.py +37 -9
  38. fractal_server/tasks/v2/utils_pixi.py +36 -0
  39. {fractal_server-2.14.15.dist-info → fractal_server-2.15.0a0.dist-info}/METADATA +4 -4
  40. {fractal_server-2.14.15.dist-info → fractal_server-2.15.0a0.dist-info}/RECORD +43 -35
  41. {fractal_server-2.14.15.dist-info → fractal_server-2.15.0a0.dist-info}/LICENSE +0 -0
  42. {fractal_server-2.14.15.dist-info → fractal_server-2.15.0a0.dist-info}/WHEEL +0 -0
  43. {fractal_server-2.14.15.dist-info → fractal_server-2.15.0a0.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,4 @@
1
1
  import time
2
- from copy import deepcopy
3
2
  from typing import Any
4
3
 
5
4
  from sqlalchemy import Select
@@ -11,7 +10,6 @@ from fractal_server.app.models.v2 import HistoryImageCache
11
10
  from fractal_server.app.models.v2 import HistoryUnit
12
11
  from fractal_server.app.schemas.v2 import HistoryUnitStatusWithUnset
13
12
  from fractal_server.logger import set_logger
14
- from fractal_server.types import ImageAttributeValue
15
13
 
16
14
  logger = set_logger(__name__)
17
15
 
@@ -19,36 +17,84 @@ logger = set_logger(__name__)
19
17
  IMAGE_STATUS_KEY = "__wftask_dataset_image_status__"
20
18
 
21
19
 
22
- def _enriched_image(*, img: dict[str, Any], status: str) -> dict[str, Any]:
23
- img["attributes"][IMAGE_STATUS_KEY] = status
24
- return img
20
+ def _enriched_image(
21
+ *,
22
+ img: dict[str, Any],
23
+ status: str,
24
+ ) -> dict[str, Any]:
25
+ return img | {
26
+ "attributes": (img["attributes"] | {IMAGE_STATUS_KEY: status})
27
+ }
25
28
 
26
29
 
27
30
  def _prepare_query(
28
31
  *,
29
32
  dataset_id: int,
30
33
  workflowtask_id: int,
31
- zarr_urls: list[str],
32
34
  ) -> Select:
35
+ """
36
+ Note: the query does not include `.order_by`.
37
+ """
33
38
  stm = (
34
39
  select(HistoryImageCache.zarr_url, HistoryUnit.status)
35
40
  .join(HistoryUnit)
36
41
  .where(HistoryImageCache.dataset_id == dataset_id)
37
42
  .where(HistoryImageCache.workflowtask_id == workflowtask_id)
38
43
  .where(HistoryImageCache.latest_history_unit_id == HistoryUnit.id)
39
- .where(HistoryImageCache.zarr_url.in_(zarr_urls))
40
- .order_by(HistoryImageCache.zarr_url)
41
44
  )
42
45
  return stm
43
46
 
44
47
 
45
- async def enrich_images_async(
48
+ def _postprocess_image_lists(
49
+ target_images: list[dict[str, Any]],
50
+ list_query_url_status: list[tuple[str, str]],
51
+ ) -> list[dict[str, Any]]:
52
+ """ """
53
+ t_1 = time.perf_counter()
54
+
55
+ # Select only processed images that are part of the target image set
56
+ zarr_url_to_image = {img["zarr_url"]: img for img in target_images}
57
+ target_zarr_urls = zarr_url_to_image.keys()
58
+ list_processed_url_status = [
59
+ url_status
60
+ for url_status in list_query_url_status
61
+ if url_status[0] in target_zarr_urls
62
+ ]
63
+
64
+ set_processed_urls = set(
65
+ url_status[0] for url_status in list_processed_url_status
66
+ )
67
+ processed_images_with_status = [
68
+ _enriched_image(
69
+ img=zarr_url_to_image[item[0]],
70
+ status=item[1],
71
+ )
72
+ for item in list_processed_url_status
73
+ ]
74
+
75
+ non_processed_urls = target_zarr_urls - set_processed_urls
76
+ non_processed_images_with_status = [
77
+ _enriched_image(
78
+ img=zarr_url_to_image[zarr_url],
79
+ status=HistoryUnitStatusWithUnset.UNSET,
80
+ )
81
+ for zarr_url in non_processed_urls
82
+ ]
83
+ t_2 = time.perf_counter()
84
+ logger.debug(
85
+ f"[enrich_images_async] post-processing, elapsed={t_2 - t_1:.5f} s"
86
+ )
87
+
88
+ return processed_images_with_status + non_processed_images_with_status
89
+
90
+
91
+ async def enrich_images_unsorted_async(
46
92
  *,
47
93
  images: list[dict[str, Any]],
48
94
  dataset_id: int,
49
95
  workflowtask_id: int,
50
96
  db: AsyncSession,
51
- ) -> list[dict[str, ImageAttributeValue]]:
97
+ ) -> list[dict[str, Any]]:
52
98
  """
53
99
  Enrich images with a status-related attribute.
54
100
 
@@ -59,116 +105,75 @@ async def enrich_images_async(
59
105
  db: An async db session
60
106
 
61
107
  Returns:
62
- The list of enriched images
108
+ The list of enriched images, not necessarily in the same order as
109
+ the input.
63
110
  """
64
111
  t_0 = time.perf_counter()
65
112
  logger.info(
66
113
  f"[enrich_images_async] START, {dataset_id=}, {workflowtask_id=}"
67
114
  )
68
115
 
69
- zarr_url_to_image = {img["zarr_url"]: deepcopy(img) for img in images}
70
-
116
+ # Get `(zarr_url, status)` for _all_ processed images (including those that
117
+ # are not part of the target image set)
71
118
  res = await db.execute(
72
119
  _prepare_query(
73
120
  dataset_id=dataset_id,
74
121
  workflowtask_id=workflowtask_id,
75
- zarr_urls=zarr_url_to_image.keys(),
76
122
  )
77
123
  )
78
- list_processed_url_status = res.all()
124
+ list_query_url_status = res.all()
79
125
  t_1 = time.perf_counter()
80
- logger.debug(f"[enrich_images_async] db-query, elapsed={t_1 - t_0:.3f} s")
126
+ logger.debug(f"[enrich_images_async] query, elapsed={t_1 - t_0:.5f} s")
81
127
 
82
- set_processed_urls = set(item[0] for item in list_processed_url_status)
83
- processed_images_with_status = [
84
- _enriched_image(
85
- img=zarr_url_to_image[item[0]],
86
- status=item[1],
87
- )
88
- for item in list_processed_url_status
89
- ]
90
- t_2 = time.perf_counter()
91
- logger.debug(
92
- "[enrich_images_async] processed-images, " f"elapsed={t_2 - t_1:.3f} s"
93
- )
94
-
95
- non_processed_urls = zarr_url_to_image.keys() - set_processed_urls
96
- non_processed_images_with_status = [
97
- _enriched_image(
98
- img=zarr_url_to_image[zarr_url],
99
- status=HistoryUnitStatusWithUnset.UNSET,
100
- )
101
- for zarr_url in non_processed_urls
102
- ]
103
- t_3 = time.perf_counter()
104
- logger.debug(
105
- "[enrich_images_async] non-processed-images, "
106
- f"elapsed={t_3 - t_2:.3f} s"
128
+ output = _postprocess_image_lists(
129
+ target_images=images,
130
+ list_query_url_status=list_query_url_status,
107
131
  )
108
132
 
109
- return processed_images_with_status + non_processed_images_with_status
133
+ return output
110
134
 
111
135
 
112
- def enrich_images_sync(
136
+ def enrich_images_unsorted_sync(
113
137
  *,
114
138
  images: list[dict[str, Any]],
115
139
  dataset_id: int,
116
140
  workflowtask_id: int,
117
- ) -> list[dict[str, ImageAttributeValue]]:
141
+ ) -> list[dict[str, Any]]:
118
142
  """
119
143
  Enrich images with a status-related attribute.
120
144
 
145
+
121
146
  Args:
122
147
  images: The input image list
123
148
  dataset_id: The dataset ID
124
149
  workflowtask_id: The workflow-task ID
125
150
 
126
151
  Returns:
127
- The list of enriched images
152
+ The list of enriched images, not necessarily in the same order as
153
+ the input.
128
154
  """
155
+
129
156
  t_0 = time.perf_counter()
130
157
  logger.info(
131
158
  f"[enrich_images_async] START, {dataset_id=}, {workflowtask_id=}"
132
159
  )
133
160
 
134
- zarr_url_to_image = {img["zarr_url"]: deepcopy(img) for img in images}
161
+ # Get `(zarr_url, status)` for _all_ processed images (including those that
162
+ # are not part of the target image set)
135
163
  with next(get_sync_db()) as db:
136
164
  res = db.execute(
137
165
  _prepare_query(
138
166
  dataset_id=dataset_id,
139
167
  workflowtask_id=workflowtask_id,
140
- zarr_urls=zarr_url_to_image.keys(),
141
168
  )
142
169
  )
143
- list_processed_url_status = res.all()
170
+ list_query_url_status = res.all()
144
171
  t_1 = time.perf_counter()
145
- logger.debug(f"[enrich_images_async] db-query, elapsed={t_1 - t_0:.3f} s")
146
-
147
- set_processed_urls = set(item[0] for item in list_processed_url_status)
148
- processed_images_with_status = [
149
- _enriched_image(
150
- img=zarr_url_to_image[item[0]],
151
- status=item[1],
152
- )
153
- for item in list_processed_url_status
154
- ]
155
- t_2 = time.perf_counter()
156
- logger.debug(
157
- "[enrich_images_async] processed-images, " f"elapsed={t_2 - t_1:.3f} s"
158
- )
172
+ logger.debug(f"[enrich_images_async] query, elapsed={t_1 - t_0:.5f} s")
159
173
 
160
- non_processed_urls = zarr_url_to_image.keys() - set_processed_urls
161
- non_processed_images_with_status = [
162
- _enriched_image(
163
- img=zarr_url_to_image[zarr_url],
164
- status=HistoryUnitStatusWithUnset.UNSET,
165
- )
166
- for zarr_url in non_processed_urls
167
- ]
168
- t_3 = time.perf_counter()
169
- logger.debug(
170
- "[enrich_images_async] non-processed-images, "
171
- f"elapsed={t_3 - t_2:.3f} s"
174
+ output = _postprocess_image_lists(
175
+ target_images=images,
176
+ list_query_url_status=list_query_url_status,
172
177
  )
173
178
 
174
- return processed_images_with_status + non_processed_images_with_status
179
+ return output
@@ -0,0 +1,41 @@
1
+ """Add indices
2
+
3
+ Revision ID: 791ce783d3d8
4
+ Revises: 969d84257cac
5
+ Create Date: 2025-06-03 09:32:30.757651
6
+
7
+ """
8
+ from alembic import op
9
+
10
+
11
+ # revision identifiers, used by Alembic.
12
+ revision = "791ce783d3d8"
13
+ down_revision = "969d84257cac"
14
+ branch_labels = None
15
+ depends_on = None
16
+
17
+
18
+ def upgrade() -> None:
19
+ # ### commands auto generated by Alembic - please adjust! ###
20
+ with op.batch_alter_table("historyimagecache", schema=None) as batch_op:
21
+ batch_op.create_index(
22
+ batch_op.f("ix_historyimagecache_dataset_id"),
23
+ ["dataset_id"],
24
+ unique=False,
25
+ )
26
+ batch_op.create_index(
27
+ batch_op.f("ix_historyimagecache_workflowtask_id"),
28
+ ["workflowtask_id"],
29
+ unique=False,
30
+ )
31
+
32
+ # ### end Alembic commands ###
33
+
34
+
35
+ def downgrade() -> None:
36
+ # ### commands auto generated by Alembic - please adjust! ###
37
+ with op.batch_alter_table("historyimagecache", schema=None) as batch_op:
38
+ batch_op.drop_index(batch_op.f("ix_historyimagecache_workflowtask_id"))
39
+ batch_op.drop_index(batch_op.f("ix_historyimagecache_dataset_id"))
40
+
41
+ # ### end Alembic commands ###
@@ -0,0 +1,53 @@
1
+ """Task group for pixi
2
+
3
+ Revision ID: b1e7f7a1ff71
4
+ Revises: 791ce783d3d8
5
+ Create Date: 2025-05-29 16:31:17.565973
6
+
7
+ """
8
+ import sqlalchemy as sa
9
+ import sqlmodel
10
+ from alembic import op
11
+
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision = "b1e7f7a1ff71"
15
+ down_revision = "791ce783d3d8"
16
+ branch_labels = None
17
+ depends_on = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
22
+ batch_op.add_column(
23
+ sa.Column(
24
+ "pixi_version",
25
+ sqlmodel.sql.sqltypes.AutoString(),
26
+ nullable=True,
27
+ )
28
+ )
29
+ batch_op.alter_column(
30
+ "wheel_path",
31
+ nullable=True,
32
+ new_column_name="archive_path",
33
+ )
34
+ batch_op.alter_column(
35
+ "pip_freeze",
36
+ nullable=True,
37
+ new_column_name="env_info",
38
+ )
39
+
40
+
41
+ def downgrade() -> None:
42
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
43
+ batch_op.alter_column(
44
+ "archive_path",
45
+ nullable=True,
46
+ new_column_name="wheel_path",
47
+ )
48
+ batch_op.alter_column(
49
+ "env_info",
50
+ nullable=True,
51
+ new_column_name="pip_freeze",
52
+ )
53
+ batch_op.drop_column("pixi_version")
@@ -642,6 +642,9 @@ class FractalSSHList:
642
642
  connect_kwargs={
643
643
  "key_filename": key_path,
644
644
  "look_for_keys": False,
645
+ "banner_timeout": 30,
646
+ "auth_timeout": 30, # default value
647
+ "channel_timeout": 60 * 60, # default value
645
648
  },
646
649
  )
647
650
  with _acquire_lock_with_timeout(
@@ -1,3 +1,5 @@
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
@@ -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` "
@@ -10,18 +10,18 @@ from ._utils import _customize_and_run_template
10
10
  from fractal_server.app.db import get_sync_db
11
11
  from fractal_server.app.models.v2 import TaskGroupActivityV2
12
12
  from fractal_server.app.models.v2 import TaskGroupV2
13
+ from fractal_server.app.schemas.v2 import FractalUploadedFile
13
14
  from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
14
15
  from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
15
- from fractal_server.app.schemas.v2 import WheelFile
16
16
  from fractal_server.app.schemas.v2.manifest import ManifestV2
17
17
  from fractal_server.logger import reset_logger_handlers
18
18
  from fractal_server.logger import set_logger
19
19
  from fractal_server.tasks.utils import get_log_path
20
20
  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
21
  from fractal_server.tasks.v2.utils_background import add_commit_refresh
23
22
  from fractal_server.tasks.v2.utils_background import fail_and_cleanup
24
23
  from fractal_server.tasks.v2.utils_background import get_current_log
24
+ from fractal_server.tasks.v2.utils_background import prepare_tasks_metadata
25
25
  from fractal_server.tasks.v2.utils_package_names import compare_package_names
26
26
  from fractal_server.tasks.v2.utils_python_interpreter import (
27
27
  get_python_interpreter_v2,
@@ -38,7 +38,7 @@ def collect_local(
38
38
  *,
39
39
  task_group_activity_id: int,
40
40
  task_group_id: int,
41
- wheel_file: WheelFile | None = None,
41
+ wheel_file: FractalUploadedFile | None = None,
42
42
  ) -> None:
43
43
  """
44
44
  Collect a task package.
@@ -103,16 +103,18 @@ def collect_local(
103
103
  Path(task_group.path).mkdir(parents=True)
104
104
  logger.info(f"Created {task_group.path}")
105
105
 
106
- # Write wheel file and set task_group.wheel_path
106
+ # Write wheel file and set task_group.archive_path
107
107
  if wheel_file is not None:
108
108
 
109
- wheel_path = (
109
+ archive_path = (
110
110
  Path(task_group.path) / wheel_file.filename
111
111
  ).as_posix()
112
- logger.info(f"Write wheel-file contents into {wheel_path}")
113
- with open(wheel_path, "wb") as f:
112
+ logger.info(
113
+ f"Write wheel-file contents into {archive_path}"
114
+ )
115
+ with open(archive_path, "wb") as f:
114
116
  f.write(wheel_file.contents)
115
- task_group.wheel_path = wheel_path
117
+ task_group.archive_path = archive_path
116
118
  task_group = add_commit_refresh(obj=task_group, db=db)
117
119
 
118
120
  # Prepare replacements for templates
@@ -220,7 +222,7 @@ def collect_local(
220
222
  activity = add_commit_refresh(obj=activity, db=db)
221
223
 
222
224
  logger.info("_prepare_tasks_metadata - start")
223
- task_list = _prepare_tasks_metadata(
225
+ task_list = prepare_tasks_metadata(
224
226
  package_manifest=pkg_manifest,
225
227
  package_version=task_group.version,
226
228
  package_root=Path(package_root),
@@ -241,15 +243,15 @@ def collect_local(
241
243
 
242
244
  # Update task_group data
243
245
  logger.info(
244
- "Add pip_freeze, venv_size and venv_file_number "
246
+ "Add env_info, venv_size and venv_file_number "
245
247
  "to TaskGroupV2 - start"
246
248
  )
247
- task_group.pip_freeze = pip_freeze_stdout
249
+ task_group.env_info = pip_freeze_stdout
248
250
  task_group.venv_size_in_kB = int(venv_size)
249
251
  task_group.venv_file_number = int(venv_file_number)
250
252
  task_group = add_commit_refresh(obj=task_group, db=db)
251
253
  logger.info(
252
- "Add pip_freeze, venv_size and venv_file_number "
254
+ "Add env_info, venv_size and venv_file_number "
253
255
  "to TaskGroupV2 - end"
254
256
  )
255
257