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 +1 @@
1
- __VERSION__ = "2.14.15"
1
+ __VERSION__ = "2.15.0a0"
@@ -65,11 +65,13 @@ class HistoryImageCache(SQLModel, table=True):
65
65
  primary_key=True,
66
66
  foreign_key="datasetv2.id",
67
67
  ondelete="CASCADE",
68
+ index=True,
68
69
  )
69
70
  workflowtask_id: int = Field(
70
71
  primary_key=True,
71
72
  foreign_key="workflowtaskv2.id",
72
73
  ondelete="CASCADE",
74
+ index=True,
73
75
  )
74
76
 
75
77
  latest_history_unit_id: int = Field(
@@ -29,8 +29,9 @@ class TaskGroupV2(SQLModel, table=True):
29
29
  pkg_name: str
30
30
  version: str | None = None
31
31
  python_version: str | None = None
32
+ pixi_version: str | None = None
32
33
  path: str | None = None
33
- wheel_path: str | None = None
34
+ archive_path: str | None = None
34
35
  pip_extras: str | None = None
35
36
  pinned_package_versions: dict[str, str] = Field(
36
37
  sa_column=Column(
@@ -40,7 +41,7 @@ class TaskGroupV2(SQLModel, table=True):
40
41
  nullable=True,
41
42
  ),
42
43
  )
43
- pip_freeze: str | None = None
44
+ env_info: str | None = None
44
45
  venv_path: str | None = None
45
46
  venv_size_in_kB: int | None = None
46
47
  venv_file_number: int | None = None
@@ -66,15 +67,20 @@ class TaskGroupV2(SQLModel, table=True):
66
67
  """
67
68
  Prepare string to be used in `python -m pip install`.
68
69
  """
70
+ if self.origin == "pixi":
71
+ raise ValueError(
72
+ f"Cannot call 'pip_install_string' if {self.origin=}."
73
+ )
74
+
69
75
  extras = f"[{self.pip_extras}]" if self.pip_extras is not None else ""
70
76
 
71
- if self.wheel_path is not None:
72
- return f"{self.wheel_path}{extras}"
77
+ if self.archive_path is not None:
78
+ return f"{self.archive_path}{extras}"
73
79
  else:
74
80
  if self.version is None:
75
81
  raise ValueError(
76
82
  "Cannot run `pip_install_string` with "
77
- f"{self.pkg_name=}, {self.wheel_path=}, {self.version=}."
83
+ f"{self.pkg_name=}, {self.archive_path=}, {self.version=}."
78
84
  )
79
85
  return f"{self.pkg_name}{extras}=={self.version}"
80
86
 
@@ -83,6 +89,12 @@ class TaskGroupV2(SQLModel, table=True):
83
89
  """
84
90
  Prepare string to be used in `python -m pip install`.
85
91
  """
92
+ if self.origin == "pixi":
93
+ raise ValueError(
94
+ "Cannot call 'pinned_package_versions_string' if "
95
+ f"{self.origin=}."
96
+ )
97
+
86
98
  if self.pinned_package_versions is None:
87
99
  return ""
88
100
  output = " ".join(
@@ -207,12 +207,12 @@ async def reactivate_task_group(
207
207
  response.status_code = status.HTTP_202_ACCEPTED
208
208
  return task_group_activity
209
209
 
210
- if task_group.pip_freeze is None:
210
+ if task_group.env_info is None:
211
211
  raise HTTPException(
212
212
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
213
213
  detail=(
214
214
  "Cannot reactivate a task group with "
215
- f"{task_group.pip_freeze=}."
215
+ f"{task_group.env_info=}."
216
216
  ),
217
217
  )
218
218
 
@@ -14,6 +14,7 @@ from .submit import router as submit_job_router_v2
14
14
  from .task import router as task_router_v2
15
15
  from .task_collection import router as task_collection_router_v2
16
16
  from .task_collection_custom import router as task_collection_router_v2_custom
17
+ from .task_collection_pixi import router as task_collection_pixi_router_v2
17
18
  from .task_group import router as task_group_router_v2
18
19
  from .task_group_lifecycle import router as task_group_lifecycle_router_v2
19
20
  from .task_version_update import router as task_version_update_router_v2
@@ -49,6 +50,11 @@ router_api_v2.include_router(
49
50
  prefix="/task",
50
51
  tags=["V2 Task Lifecycle"],
51
52
  )
53
+ router_api_v2.include_router(
54
+ task_collection_pixi_router_v2,
55
+ prefix="/task",
56
+ tags=["V2 Task Lifecycle"],
57
+ )
52
58
  router_api_v2.include_router(
53
59
  task_group_lifecycle_router_v2,
54
60
  prefix="/task-group",
@@ -34,7 +34,7 @@ from fractal_server.app.schemas.v2 import HistoryUnitRead
34
34
  from fractal_server.app.schemas.v2 import HistoryUnitStatus
35
35
  from fractal_server.app.schemas.v2 import HistoryUnitStatusWithUnset
36
36
  from fractal_server.app.schemas.v2 import ImageLogsRequest
37
- from fractal_server.images.status_tools import enrich_images_async
37
+ from fractal_server.images.status_tools import enrich_images_unsorted_async
38
38
  from fractal_server.images.status_tools import IMAGE_STATUS_KEY
39
39
  from fractal_server.images.tools import aggregate_attributes
40
40
  from fractal_server.images.tools import aggregate_types
@@ -334,7 +334,7 @@ async def get_history_images(
334
334
  types = aggregate_types(type_filtered_images)
335
335
 
336
336
  # (3) Enrich images with status attribute
337
- type_filtered_images_with_status = await enrich_images_async(
337
+ type_filtered_images_with_status = await enrich_images_unsorted_async(
338
338
  dataset_id=dataset_id,
339
339
  workflowtask_id=workflowtask_id,
340
340
  images=type_filtered_images,
@@ -14,7 +14,7 @@ from fractal_server.app.models import UserOAuth
14
14
  from fractal_server.app.routes.auth import current_active_user
15
15
  from fractal_server.app.schemas.v2 import HistoryUnitStatus
16
16
  from fractal_server.app.schemas.v2 import TaskType
17
- from fractal_server.images.status_tools import enrich_images_async
17
+ from fractal_server.images.status_tools import enrich_images_unsorted_async
18
18
  from fractal_server.images.status_tools import IMAGE_STATUS_KEY
19
19
  from fractal_server.images.tools import aggregate_types
20
20
  from fractal_server.images.tools import filter_image_list
@@ -46,7 +46,7 @@ async def verify_unique_types(
46
46
  filtered_images = dataset.images
47
47
  else:
48
48
  if IMAGE_STATUS_KEY in query.attribute_filters.keys():
49
- images = await enrich_images_async(
49
+ images = await enrich_images_unsorted_async(
50
50
  dataset_id=dataset_id,
51
51
  workflowtask_id=workflowtask_id,
52
52
  images=dataset.images,
@@ -134,7 +134,7 @@ async def check_non_processed_images(
134
134
  attribute_filters=filters.attribute_filters,
135
135
  )
136
136
 
137
- filtered_images_with_status = await enrich_images_async(
137
+ filtered_images_with_status = await enrich_images_unsorted_async(
138
138
  dataset_id=dataset_id,
139
139
  workflowtask_id=previous_wft.id,
140
140
  images=filtered_images,
@@ -23,11 +23,11 @@ from .....syringe import Inject
23
23
  from ....db import AsyncSession
24
24
  from ....db import get_async_db
25
25
  from ....models.v2 import TaskGroupV2
26
+ from ....schemas.v2 import FractalUploadedFile
26
27
  from ....schemas.v2 import TaskCollectPipV2
27
28
  from ....schemas.v2 import TaskGroupActivityStatusV2
28
29
  from ....schemas.v2 import TaskGroupActivityV2Read
29
30
  from ....schemas.v2 import TaskGroupCreateV2Strict
30
- from ....schemas.v2 import WheelFile
31
31
  from ...aux.validate_user_settings import validate_user_settings
32
32
  from ._aux_functions_task_lifecycle import get_package_version_from_pypi
33
33
  from ._aux_functions_tasks import _get_valid_user_group_id
@@ -208,13 +208,13 @@ async def collect_tasks_pip(
208
208
  # Initialize wheel_file_content as None
209
209
  wheel_file = None
210
210
 
211
- # Set pkg_name, version, origin and wheel_path
211
+ # Set pkg_name, version, origin and archive_path
212
212
  if request_data.origin == TaskGroupV2OriginEnum.WHEELFILE:
213
213
  try:
214
214
  wheel_filename = request_data.file.filename
215
215
  wheel_info = _parse_wheel_filename(wheel_filename)
216
216
  wheel_file_content = await request_data.file.read()
217
- wheel_file = WheelFile(
217
+ wheel_file = FractalUploadedFile(
218
218
  filename=wheel_filename,
219
219
  contents=wheel_file_content,
220
220
  )
@@ -26,7 +26,7 @@ from fractal_server.logger import set_logger
26
26
  from fractal_server.string_tools import validate_cmd
27
27
  from fractal_server.syringe import Inject
28
28
  from fractal_server.tasks.v2.utils_background import (
29
- _prepare_tasks_metadata,
29
+ prepare_tasks_metadata,
30
30
  )
31
31
  from fractal_server.tasks.v2.utils_database import (
32
32
  create_db_tasks_and_update_task_group_async,
@@ -138,7 +138,7 @@ async def collect_task_custom(
138
138
  else:
139
139
  package_root = Path(task_collect.package_root)
140
140
 
141
- task_list: list[TaskCreateV2] = _prepare_tasks_metadata(
141
+ task_list: list[TaskCreateV2] = prepare_tasks_metadata(
142
142
  package_manifest=task_collect.manifest,
143
143
  python_bin=Path(task_collect.python_interpreter),
144
144
  package_root=package_root,
@@ -0,0 +1,236 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from fastapi import APIRouter
5
+ from fastapi import BackgroundTasks
6
+ from fastapi import Depends
7
+ from fastapi import Form
8
+ from fastapi import HTTPException
9
+ from fastapi import Request
10
+ from fastapi import Response
11
+ from fastapi import status
12
+ from fastapi import UploadFile
13
+ from pydantic import ValidationError
14
+ from sqlmodel import select
15
+
16
+ from fractal_server.app.db import AsyncSession
17
+ from fractal_server.app.db import get_async_db
18
+ from fractal_server.app.models import UserOAuth
19
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
20
+ from fractal_server.app.models.v2 import TaskGroupV2
21
+ from fractal_server.app.routes.api.v2._aux_functions_tasks import (
22
+ _get_valid_user_group_id,
23
+ )
24
+ from fractal_server.app.routes.api.v2._aux_functions_tasks import (
25
+ _verify_non_duplication_group_constraint,
26
+ )
27
+ from fractal_server.app.routes.api.v2._aux_functions_tasks import (
28
+ _verify_non_duplication_user_constraint,
29
+ )
30
+ from fractal_server.app.routes.auth import current_active_verified_user
31
+ from fractal_server.app.routes.aux.validate_user_settings import (
32
+ validate_user_settings,
33
+ )
34
+ from fractal_server.app.schemas.v2 import FractalUploadedFile
35
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
36
+ from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
37
+ from fractal_server.app.schemas.v2 import TaskGroupActivityV2Read
38
+ from fractal_server.app.schemas.v2 import TaskGroupCreateV2StrictPixi
39
+ from fractal_server.app.schemas.v2.task_group import TaskGroupV2OriginEnum
40
+ from fractal_server.config import get_settings
41
+ from fractal_server.logger import set_logger
42
+ from fractal_server.ssh._fabric import SSHConfig
43
+ from fractal_server.syringe import Inject
44
+ from fractal_server.tasks.v2.local import collect_local_pixi
45
+ from fractal_server.tasks.v2.ssh import collect_ssh_pixi
46
+ from fractal_server.tasks.v2.utils_package_names import normalize_package_name
47
+ from fractal_server.types import NonEmptyStr
48
+
49
+
50
+ router = APIRouter()
51
+
52
+ logger = set_logger(__name__)
53
+
54
+
55
+ def validate_pkgname_and_version(filename: str) -> tuple[str, str]:
56
+ if not filename.endswith(".tar.gz"):
57
+ raise HTTPException(
58
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
59
+ detail=f"{filename=} does not end with '.tar.gz'.",
60
+ )
61
+ filename_splitted = filename.split("-")
62
+ if len(filename_splitted) != 2:
63
+ raise HTTPException(
64
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
65
+ detail=(
66
+ f"Invalid filename: '{filename}' must contain a single `-` "
67
+ "character, separating the package name from the version "
68
+ "(expected format: 'pkg_name-version')."
69
+ ),
70
+ )
71
+
72
+ pkg_name = filename_splitted[0]
73
+ version = filename.removeprefix(f"{pkg_name}-").removesuffix(".tar.gz")
74
+
75
+ return normalize_package_name(pkg_name), version
76
+
77
+
78
+ @router.post(
79
+ "/collect/pixi/",
80
+ status_code=202,
81
+ response_model=TaskGroupActivityV2Read,
82
+ )
83
+ async def collect_task_pixi(
84
+ request: Request,
85
+ response: Response,
86
+ background_tasks: BackgroundTasks,
87
+ file: UploadFile,
88
+ pixi_version: NonEmptyStr | None = Form(None),
89
+ private: bool = False,
90
+ user_group_id: int | None = None,
91
+ user: UserOAuth = Depends(current_active_verified_user),
92
+ db: AsyncSession = Depends(get_async_db),
93
+ ) -> TaskGroupActivityV2Read:
94
+
95
+ settings = Inject(get_settings)
96
+ # Check if Pixi is available
97
+ if settings.pixi is None:
98
+ raise HTTPException(
99
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
100
+ detail="Pixi task collection is not available.",
101
+ )
102
+ # Check if provided Pixi version is available. Use default if not provided
103
+ if pixi_version is None:
104
+ pixi_version = settings.pixi.default_version
105
+ else:
106
+ if pixi_version not in settings.pixi.versions:
107
+ raise HTTPException(
108
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
109
+ detail=(
110
+ f"Pixi version {pixi_version} is not available. Available"
111
+ f"versions: {list(settings.pixi.versions.keys())}"
112
+ ),
113
+ )
114
+
115
+ pkg_name, version = validate_pkgname_and_version(file.filename)
116
+ tar_gz_content = await file.read()
117
+ tar_gz_file = FractalUploadedFile(
118
+ filename=file.filename,
119
+ contents=tar_gz_content,
120
+ )
121
+
122
+ user_group_id = await _get_valid_user_group_id(
123
+ user_group_id=user_group_id,
124
+ private=private,
125
+ user_id=user.id,
126
+ db=db,
127
+ )
128
+
129
+ user_settings = await validate_user_settings(
130
+ user=user, backend=settings.FRACTAL_RUNNER_BACKEND, db=db
131
+ )
132
+
133
+ if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
134
+ base_tasks_path = user_settings.ssh_tasks_dir
135
+ else:
136
+ base_tasks_path = settings.FRACTAL_TASKS_DIR.as_posix()
137
+ task_group_path = (
138
+ Path(base_tasks_path) / str(user.id) / pkg_name / version
139
+ ).as_posix()
140
+
141
+ task_group_attrs = dict(
142
+ user_id=user.id,
143
+ user_group_id=user_group_id,
144
+ origin=TaskGroupV2OriginEnum.PIXI,
145
+ pixi_version=pixi_version,
146
+ pkg_name=pkg_name,
147
+ version=version,
148
+ path=task_group_path,
149
+ )
150
+ try:
151
+ TaskGroupCreateV2StrictPixi(**task_group_attrs)
152
+ except ValidationError as e:
153
+ raise HTTPException(
154
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
155
+ detail=f"Invalid task-group object. Original error: {e}",
156
+ )
157
+
158
+ await _verify_non_duplication_user_constraint(
159
+ user_id=user.id,
160
+ pkg_name=task_group_attrs["pkg_name"],
161
+ version=task_group_attrs["version"],
162
+ db=db,
163
+ )
164
+ await _verify_non_duplication_group_constraint(
165
+ user_group_id=task_group_attrs["user_group_id"],
166
+ pkg_name=task_group_attrs["pkg_name"],
167
+ version=task_group_attrs["version"],
168
+ db=db,
169
+ )
170
+
171
+ # FIXME: to be removed with issue #2634
172
+ stm = select(TaskGroupV2).where(TaskGroupV2.path == task_group_path)
173
+ res = await db.execute(stm)
174
+ for conflicting_task_group in res.scalars().all():
175
+ raise HTTPException(
176
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
177
+ detail=(
178
+ f"Another task-group already has path={task_group_path}.\n"
179
+ f"{conflicting_task_group=}"
180
+ ),
181
+ )
182
+
183
+ if settings.FRACTAL_RUNNER_BACKEND != "slurm_ssh":
184
+ if Path(task_group_path).exists():
185
+ raise HTTPException(
186
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
187
+ detail=f"{task_group_path} already exists.",
188
+ )
189
+
190
+ task_group = TaskGroupV2(**task_group_attrs)
191
+ db.add(task_group)
192
+ await db.commit()
193
+ await db.refresh(task_group)
194
+ db.expunge(task_group)
195
+
196
+ task_group_activity = TaskGroupActivityV2(
197
+ user_id=task_group.user_id,
198
+ taskgroupv2_id=task_group.id,
199
+ status=TaskGroupActivityStatusV2.PENDING,
200
+ action=TaskGroupActivityActionV2.COLLECT,
201
+ pkg_name=task_group.pkg_name,
202
+ version=task_group.version,
203
+ )
204
+ db.add(task_group_activity)
205
+ await db.commit()
206
+ await db.refresh(task_group_activity)
207
+
208
+ if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
209
+ ssh_config = SSHConfig(
210
+ user=user_settings.ssh_username,
211
+ host=user_settings.ssh_host,
212
+ key_path=user_settings.ssh_private_key_path,
213
+ )
214
+
215
+ background_tasks.add_task(
216
+ collect_ssh_pixi,
217
+ task_group_id=task_group.id,
218
+ task_group_activity_id=task_group_activity.id,
219
+ ssh_config=ssh_config,
220
+ tasks_base_dir=user_settings.ssh_tasks_dir,
221
+ tar_gz_file=tar_gz_file,
222
+ )
223
+ else:
224
+ background_tasks.add_task(
225
+ collect_local_pixi,
226
+ task_group_id=task_group.id,
227
+ task_group_activity_id=task_group_activity.id,
228
+ tar_gz_file=tar_gz_file,
229
+ )
230
+ logger.info(
231
+ "Task-collection endpoint: start background collection "
232
+ "and return task_group_activity. "
233
+ f"Current pid is {os.getpid()}. "
234
+ )
235
+ response.status_code = status.HTTP_202_ACCEPTED
236
+ return task_group_activity
@@ -25,6 +25,7 @@ from fractal_server.logger import set_logger
25
25
  from fractal_server.ssh._fabric import SSHConfig
26
26
  from fractal_server.syringe import Inject
27
27
  from fractal_server.tasks.v2.local import deactivate_local
28
+ from fractal_server.tasks.v2.local import deactivate_local_pixi
28
29
  from fractal_server.tasks.v2.local import reactivate_local
29
30
  from fractal_server.tasks.v2.ssh import deactivate_ssh
30
31
  from fractal_server.tasks.v2.ssh import reactivate_ssh
@@ -135,8 +136,12 @@ async def deactivate_task_group(
135
136
  )
136
137
 
137
138
  else:
139
+ if task_group.origin == TaskGroupV2OriginEnum.PIXI:
140
+ deactivate_function = deactivate_local_pixi
141
+ else:
142
+ deactivate_function = deactivate_local
138
143
  background_tasks.add_task(
139
- deactivate_local,
144
+ deactivate_function,
140
145
  task_group_id=task_group.id,
141
146
  task_group_activity_id=task_group_activity.id,
142
147
  )
@@ -210,12 +215,12 @@ async def reactivate_task_group(
210
215
  response.status_code = status.HTTP_202_ACCEPTED
211
216
  return task_group_activity
212
217
 
213
- if task_group.pip_freeze is None:
218
+ if task_group.env_info is None:
214
219
  raise HTTPException(
215
220
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
216
221
  detail=(
217
222
  "Cannot reactivate a task group with "
218
- f"{task_group.pip_freeze=}."
223
+ f"{task_group.env_info=}."
219
224
  ),
220
225
  )
221
226
 
@@ -40,8 +40,10 @@ class SlurmSSHRunner(BaseSlurmRunner):
40
40
  self.fractal_ssh = fractal_ssh
41
41
  logger.warning(self.fractal_ssh)
42
42
 
43
- settings = Inject(get_settings)
43
+ # Check SSH connection and try to recover from a closed-socket error
44
+ self.fractal_ssh.check_connection()
44
45
 
46
+ settings = Inject(get_settings)
45
47
  super().__init__(
46
48
  slurm_runner_type="ssh",
47
49
  root_dir_local=root_dir_local,
@@ -32,7 +32,7 @@ from fractal_server.app.schemas.v2 import TaskDumpV2
32
32
  from fractal_server.app.schemas.v2 import TaskGroupDumpV2
33
33
  from fractal_server.app.schemas.v2 import TaskType
34
34
  from fractal_server.images import SingleImage
35
- from fractal_server.images.status_tools import enrich_images_sync
35
+ from fractal_server.images.status_tools import enrich_images_unsorted_sync
36
36
  from fractal_server.images.status_tools import IMAGE_STATUS_KEY
37
37
  from fractal_server.images.tools import filter_image_list
38
38
  from fractal_server.images.tools import find_image_by_zarr_url
@@ -147,7 +147,7 @@ def execute_tasks_v2(
147
147
 
148
148
  if ind_wftask == 0 and ENRICH_IMAGES_WITH_STATUS:
149
149
  # FIXME: Could this be done on `type_filtered_images`?
150
- tmp_images = enrich_images_sync(
150
+ tmp_images = enrich_images_unsorted_sync(
151
151
  images=tmp_images,
152
152
  dataset_id=dataset.id,
153
153
  workflowtask_id=wftask.id,
@@ -33,14 +33,15 @@ from .task import TaskImportV2Legacy # noqa F401
33
33
  from .task import TaskReadV2 # noqa F401
34
34
  from .task import TaskType # noqa F401
35
35
  from .task import TaskUpdateV2 # noqa F401
36
+ from .task_collection import FractalUploadedFile # noqa F401
36
37
  from .task_collection import TaskCollectCustomV2 # noqa F401
37
38
  from .task_collection import TaskCollectPipV2 # noqa F401
38
- from .task_collection import WheelFile # noqa F401
39
39
  from .task_group import TaskGroupActivityActionV2 # noqa F401
40
40
  from .task_group import TaskGroupActivityStatusV2 # noqa F401
41
41
  from .task_group import TaskGroupActivityV2Read # noqa F401
42
42
  from .task_group import TaskGroupCreateV2 # noqa F401
43
43
  from .task_group import TaskGroupCreateV2Strict # noqa F401
44
+ from .task_group import TaskGroupCreateV2StrictPixi # noqa F401
44
45
  from .task_group import TaskGroupReadV2 # noqa F401
45
46
  from .task_group import TaskGroupUpdateV2 # noqa F401
46
47
  from .task_group import TaskGroupV2OriginEnum # noqa F401
@@ -86,4 +86,4 @@ class TaskGroupDumpV2(BaseModel):
86
86
 
87
87
  path: str | None = None
88
88
  venv_path: str | None = None
89
- wheel_path: str | None = None
89
+ archive_path: str | None = None
@@ -12,7 +12,7 @@ from fractal_server.types import DictStrStr
12
12
  from fractal_server.types import NonEmptyStr
13
13
 
14
14
 
15
- class WheelFile(BaseModel):
15
+ class FractalUploadedFile(BaseModel):
16
16
  """
17
17
  Model for data sent from the endpoint to the background task.
18
18
  """
@@ -16,6 +16,7 @@ from fractal_server.types import NonEmptyStr
16
16
  class TaskGroupV2OriginEnum(StrEnum):
17
17
  PYPI = "pypi"
18
18
  WHEELFILE = "wheel-file"
19
+ PIXI = "pixi"
19
20
  OTHER = "other"
20
21
 
21
22
 
@@ -41,11 +42,12 @@ class TaskGroupCreateV2(BaseModel):
41
42
  pkg_name: str
42
43
  version: str | None = None
43
44
  python_version: NonEmptyStr = None
45
+ pixi_version: NonEmptyStr = None
44
46
  path: AbsolutePathStr = None
45
47
  venv_path: AbsolutePathStr = None
46
- wheel_path: AbsolutePathStr = None
48
+ archive_path: AbsolutePathStr = None
47
49
  pip_extras: NonEmptyStr = None
48
- pip_freeze: str | None = None
50
+ env_info: str | None = None
49
51
  pinned_package_versions: DictStrStr = Field(default_factory=dict)
50
52
 
51
53
 
@@ -55,11 +57,20 @@ class TaskGroupCreateV2Strict(TaskGroupCreateV2):
55
57
  """
56
58
 
57
59
  path: AbsolutePathStr
58
- venv_path: AbsolutePathStr
59
60
  version: NonEmptyStr
61
+ venv_path: AbsolutePathStr
60
62
  python_version: NonEmptyStr
61
63
 
62
64
 
65
+ class TaskGroupCreateV2StrictPixi(TaskGroupCreateV2):
66
+ """
67
+ A strict version of TaskGroupCreateV2, to be used for pixi task collection.
68
+ """
69
+
70
+ path: AbsolutePathStr
71
+ pixi_version: NonEmptyStr
72
+
73
+
63
74
  class TaskGroupReadV2(BaseModel):
64
75
  id: int
65
76
  task_list: list[TaskReadV2]
@@ -71,10 +82,10 @@ class TaskGroupReadV2(BaseModel):
71
82
  pkg_name: str
72
83
  version: str | None = None
73
84
  python_version: str | None = None
85
+ pixi_version: str | None = None
74
86
  path: str | None = None
75
87
  venv_path: str | None = None
76
- wheel_path: str | None = None
77
- pip_freeze: str | None = None
88
+ archive_path: str | None = None
78
89
  pip_extras: str | None = None
79
90
  pinned_package_versions: dict[str, str] = Field(default_factory=dict)
80
91
 
fractal_server/config.py CHANGED
@@ -11,6 +11,7 @@
11
11
  # <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
12
12
  # Institute for Biomedical Research and Pelkmans Lab from the University of
13
13
  # Zurich.
14
+ import json
14
15
  import logging
15
16
  import shutil
16
17
  import sys
@@ -34,6 +35,7 @@ from sqlalchemy.engine import URL
34
35
 
35
36
  import fractal_server
36
37
  from fractal_server.types import AbsolutePathStr
38
+ from fractal_server.types import DictStrStr
37
39
 
38
40
 
39
41
  class MailSettings(BaseModel):
@@ -62,6 +64,35 @@ class MailSettings(BaseModel):
62
64
  use_login: bool
63
65
 
64
66
 
67
+ class PixiSettings(BaseModel):
68
+ default_version: str
69
+ versions: DictStrStr
70
+
71
+ @model_validator(mode="after")
72
+ def check_pixi_settings(self):
73
+
74
+ if self.default_version not in self.versions:
75
+ raise ValueError(
76
+ f"Default version '{self.default_version}' not in "
77
+ f"available version {list(self.versions.keys())}."
78
+ )
79
+
80
+ pixi_base_dir = Path(self.versions[self.default_version]).parent
81
+
82
+ for key, value in self.versions.items():
83
+
84
+ pixi_path = Path(value)
85
+
86
+ if pixi_path.parent != pixi_base_dir:
87
+ raise ValueError(
88
+ f"{pixi_path=} is not located within the {pixi_base_dir=}."
89
+ )
90
+ if pixi_path.name != key:
91
+ raise ValueError(f"{pixi_path.name=} is not equal to {key=}")
92
+
93
+ return self
94
+
95
+
65
96
  class FractalConfigurationError(RuntimeError):
66
97
  pass
67
98
 
@@ -513,6 +544,17 @@ class Settings(BaseSettings):
513
544
  FRACTAL_VIEWER_AUTHORIZATION_SCHEME is set to "users-folders".
514
545
  """
515
546
 
547
+ FRACTAL_PIXI_CONFIG_FILE: Path | None = None
548
+
549
+ pixi: PixiSettings | None = None
550
+
551
+ @model_validator(mode="after")
552
+ def populate_pixi_settings(self):
553
+ if self.FRACTAL_PIXI_CONFIG_FILE is not None:
554
+ with self.FRACTAL_PIXI_CONFIG_FILE.open("r") as f:
555
+ self.pixi = PixiSettings(**json.load(f))
556
+ return self
557
+
516
558
  ###########################################################################
517
559
  # SMTP SERVICE
518
560
  ###########################################################################