fractal-server 2.15.0a5__py3-none-any.whl → 2.15.2__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 (32) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/db/__init__.py +2 -6
  3. fractal_server/app/models/security.py +2 -2
  4. fractal_server/app/models/user_settings.py +2 -2
  5. fractal_server/app/models/v2/dataset.py +3 -3
  6. fractal_server/app/models/v2/job.py +6 -6
  7. fractal_server/app/models/v2/task.py +5 -4
  8. fractal_server/app/models/v2/task_group.py +2 -2
  9. fractal_server/app/models/v2/workflowtask.py +5 -4
  10. fractal_server/app/routes/admin/v2/job.py +8 -1
  11. fractal_server/app/routes/api/v2/_aux_functions.py +18 -0
  12. fractal_server/app/routes/api/v2/workflow.py +13 -0
  13. fractal_server/app/routes/api/v2/workflowtask.py +10 -0
  14. fractal_server/app/runner/executors/slurm_common/_slurm_config.py +10 -0
  15. fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py +85 -18
  16. fractal_server/app/runner/executors/slurm_common/get_slurm_config.py +8 -1
  17. fractal_server/app/runner/executors/slurm_common/remote.py +9 -9
  18. fractal_server/app/runner/v2/_slurm_ssh.py +0 -13
  19. fractal_server/migrations/versions/b3ffb095f973_json_to_jsonb.py +264 -0
  20. fractal_server/ssh/_fabric.py +6 -1
  21. fractal_server/tasks/v2/local/collect_pixi.py +1 -1
  22. fractal_server/tasks/v2/local/reactivate_pixi.py +1 -1
  23. fractal_server/tasks/v2/ssh/collect_pixi.py +6 -3
  24. fractal_server/tasks/v2/ssh/reactivate_pixi.py +7 -4
  25. fractal_server/tasks/v2/templates/pixi_1_extract.sh +1 -1
  26. fractal_server/tasks/v2/templates/pixi_2_install.sh +2 -2
  27. fractal_server/tasks/v2/templates/pixi_3_post_install.sh +1 -1
  28. {fractal_server-2.15.0a5.dist-info → fractal_server-2.15.2.dist-info}/METADATA +1 -1
  29. {fractal_server-2.15.0a5.dist-info → fractal_server-2.15.2.dist-info}/RECORD +32 -31
  30. {fractal_server-2.15.0a5.dist-info → fractal_server-2.15.2.dist-info}/LICENSE +0 -0
  31. {fractal_server-2.15.0a5.dist-info → fractal_server-2.15.2.dist-info}/WHEEL +0 -0
  32. {fractal_server-2.15.0a5.dist-info → fractal_server-2.15.2.dist-info}/entry_points.txt +0 -0
@@ -1 +1 @@
1
- __VERSION__ = "2.15.0a5"
1
+ __VERSION__ = "2.15.2"
@@ -45,13 +45,11 @@ class DB:
45
45
  settings = Inject(get_settings)
46
46
  settings.check_db()
47
47
 
48
- engine_kwargs_async = {"pool_pre_ping": True}
49
-
50
48
  cls._engine_async = create_async_engine(
51
49
  settings.DATABASE_ASYNC_URL,
52
50
  echo=settings.DB_ECHO,
53
51
  future=True,
54
- **engine_kwargs_async,
52
+ pool_pre_ping=True,
55
53
  )
56
54
  cls._async_session_maker = sessionmaker(
57
55
  cls._engine_async,
@@ -65,13 +63,11 @@ class DB:
65
63
  settings = Inject(get_settings)
66
64
  settings.check_db()
67
65
 
68
- engine_kwargs_sync = {}
69
-
70
66
  cls._engine_sync = create_engine(
71
67
  settings.DATABASE_SYNC_URL,
72
68
  echo=settings.DB_ECHO,
73
69
  future=True,
74
- **engine_kwargs_sync,
70
+ pool_pre_ping=True,
75
71
  )
76
72
 
77
73
  cls._sync_session_maker = sessionmaker(
@@ -15,8 +15,8 @@ from typing import Optional
15
15
  from pydantic import ConfigDict
16
16
  from pydantic import EmailStr
17
17
  from sqlalchemy import Column
18
+ from sqlalchemy.dialects.postgresql import JSONB
18
19
  from sqlalchemy.types import DateTime
19
- from sqlalchemy.types import JSON
20
20
  from sqlmodel import Field
21
21
  from sqlmodel import Relationship
22
22
  from sqlmodel import SQLModel
@@ -124,5 +124,5 @@ class UserGroup(SQLModel, table=True):
124
124
  sa_column=Column(DateTime(timezone=True), nullable=False),
125
125
  )
126
126
  viewer_paths: list[str] = Field(
127
- sa_column=Column(JSON, server_default="[]", nullable=False)
127
+ sa_column=Column(JSONB, server_default="[]", nullable=False)
128
128
  )
@@ -1,5 +1,5 @@
1
1
  from sqlalchemy import Column
2
- from sqlalchemy.types import JSON
2
+ from sqlalchemy.dialects.postgresql import JSONB
3
3
  from sqlmodel import Field
4
4
  from sqlmodel import SQLModel
5
5
 
@@ -25,7 +25,7 @@ class UserSettings(SQLModel, table=True):
25
25
 
26
26
  id: int | None = Field(default=None, primary_key=True)
27
27
  slurm_accounts: list[str] = Field(
28
- sa_column=Column(JSON, server_default="[]", nullable=False)
28
+ sa_column=Column(JSONB, server_default="[]", nullable=False)
29
29
  )
30
30
  ssh_host: str | None = None
31
31
  ssh_username: str | None = None
@@ -3,8 +3,8 @@ from typing import Any
3
3
 
4
4
  from pydantic import ConfigDict
5
5
  from sqlalchemy import Column
6
+ from sqlalchemy.dialects.postgresql import JSONB
6
7
  from sqlalchemy.types import DateTime
7
- from sqlalchemy.types import JSON
8
8
  from sqlmodel import Field
9
9
  from sqlmodel import Relationship
10
10
  from sqlmodel import SQLModel
@@ -24,7 +24,7 @@ class DatasetV2(SQLModel, table=True):
24
24
  )
25
25
 
26
26
  history: list[dict[str, Any]] = Field(
27
- sa_column=Column(JSON, server_default="[]", nullable=False)
27
+ sa_column=Column(JSONB, server_default="[]", nullable=False)
28
28
  )
29
29
 
30
30
  timestamp_created: datetime = Field(
@@ -34,7 +34,7 @@ class DatasetV2(SQLModel, table=True):
34
34
 
35
35
  zarr_dir: str
36
36
  images: list[dict[str, Any]] = Field(
37
- sa_column=Column(JSON, server_default="[]", nullable=False)
37
+ sa_column=Column(JSONB, server_default="[]", nullable=False)
38
38
  )
39
39
 
40
40
  @property
@@ -3,8 +3,8 @@ from typing import Any
3
3
 
4
4
  from pydantic import ConfigDict
5
5
  from sqlalchemy import Column
6
+ from sqlalchemy.dialects.postgresql import JSONB
6
7
  from sqlalchemy.types import DateTime
7
- from sqlalchemy.types import JSON
8
8
  from sqlmodel import Field
9
9
  from sqlmodel import SQLModel
10
10
 
@@ -31,13 +31,13 @@ class JobV2(SQLModel, table=True):
31
31
  slurm_account: str | None = None
32
32
 
33
33
  dataset_dump: dict[str, Any] = Field(
34
- sa_column=Column(JSON, nullable=False)
34
+ sa_column=Column(JSONB, nullable=False)
35
35
  )
36
36
  workflow_dump: dict[str, Any] = Field(
37
- sa_column=Column(JSON, nullable=False)
37
+ sa_column=Column(JSONB, nullable=False)
38
38
  )
39
39
  project_dump: dict[str, Any] = Field(
40
- sa_column=Column(JSON, nullable=False)
40
+ sa_column=Column(JSONB, nullable=False)
41
41
  )
42
42
 
43
43
  worker_init: str | None = None
@@ -57,8 +57,8 @@ class JobV2(SQLModel, table=True):
57
57
  log: str | None = None
58
58
 
59
59
  attribute_filters: AttributeFilters = Field(
60
- sa_column=Column(JSON, nullable=False, server_default="{}")
60
+ sa_column=Column(JSONB, nullable=False, server_default="{}")
61
61
  )
62
62
  type_filters: dict[str, bool] = Field(
63
- sa_column=Column(JSON, nullable=False, server_default="{}")
63
+ sa_column=Column(JSONB, nullable=False, server_default="{}")
64
64
  )
@@ -1,7 +1,8 @@
1
1
  from typing import Any
2
2
 
3
3
  from sqlalchemy import Column
4
- from sqlalchemy.types import JSON
4
+ from sqlalchemy.dialects.postgresql import JSON
5
+ from sqlalchemy.dialects.postgresql import JSONB
5
6
  from sqlmodel import Field
6
7
  from sqlmodel import SQLModel
7
8
 
@@ -33,8 +34,8 @@ class TaskV2(SQLModel, table=True):
33
34
  docs_info: str | None = None
34
35
  docs_link: str | None = None
35
36
 
36
- input_types: dict[str, bool] = Field(sa_column=Column(JSON), default={})
37
- output_types: dict[str, bool] = Field(sa_column=Column(JSON), default={})
37
+ input_types: dict[str, bool] = Field(sa_column=Column(JSONB), default={})
38
+ output_types: dict[str, bool] = Field(sa_column=Column(JSONB), default={})
38
39
 
39
40
  taskgroupv2_id: int = Field(foreign_key="taskgroupv2.id")
40
41
 
@@ -42,5 +43,5 @@ class TaskV2(SQLModel, table=True):
42
43
  modality: str | None = None
43
44
  authors: str | None = None
44
45
  tags: list[str] = Field(
45
- sa_column=Column(JSON, server_default="[]", nullable=False)
46
+ sa_column=Column(JSONB, server_default="[]", nullable=False)
46
47
  )
@@ -2,8 +2,8 @@ from datetime import datetime
2
2
  from datetime import timezone
3
3
 
4
4
  from sqlalchemy import Column
5
+ from sqlalchemy.dialects.postgresql import JSONB
5
6
  from sqlalchemy.types import DateTime
6
- from sqlalchemy.types import JSON
7
7
  from sqlmodel import Field
8
8
  from sqlmodel import Relationship
9
9
  from sqlmodel import SQLModel
@@ -35,7 +35,7 @@ class TaskGroupV2(SQLModel, table=True):
35
35
  pip_extras: str | None = None
36
36
  pinned_package_versions: dict[str, str] = Field(
37
37
  sa_column=Column(
38
- JSON,
38
+ JSONB,
39
39
  server_default="{}",
40
40
  default={},
41
41
  nullable=True,
@@ -2,7 +2,8 @@ from typing import Any
2
2
 
3
3
  from pydantic import ConfigDict
4
4
  from sqlalchemy import Column
5
- from sqlalchemy.types import JSON
5
+ from sqlalchemy.dialects.postgresql import JSON
6
+ from sqlalchemy.dialects.postgresql import JSONB
6
7
  from sqlmodel import Field
7
8
  from sqlmodel import Relationship
8
9
  from sqlmodel import SQLModel
@@ -24,14 +25,14 @@ class WorkflowTaskV2(SQLModel, table=True):
24
25
  sa_column=Column(JSON), default=None
25
26
  )
26
27
  args_parallel: dict[str, Any] | None = Field(
27
- sa_column=Column(JSON), default=None
28
+ sa_column=Column(JSONB), default=None
28
29
  )
29
30
  args_non_parallel: dict[str, Any] | None = Field(
30
- sa_column=Column(JSON), default=None
31
+ sa_column=Column(JSONB), default=None
31
32
  )
32
33
 
33
34
  type_filters: dict[str, bool] = Field(
34
- sa_column=Column(JSON, nullable=False, server_default="{}")
35
+ sa_column=Column(JSONB, nullable=False, server_default="{}")
35
36
  )
36
37
 
37
38
  # Task
@@ -156,8 +156,15 @@ async def update_job(
156
156
  detail=f"Cannot set job status to {job_update.status}",
157
157
  )
158
158
 
159
+ timestamp = get_timestamp()
159
160
  setattr(job, "status", job_update.status)
160
- setattr(job, "end_timestamp", get_timestamp())
161
+ setattr(job, "end_timestamp", timestamp)
162
+ setattr(
163
+ job,
164
+ "log",
165
+ f"{job.log or ''}\nThis job was manually marked as "
166
+ f"'{JobStatusTypeV2.FAILED}' by an admin ({timestamp.isoformat()}).",
167
+ )
161
168
  await db.commit()
162
169
  await db.refresh(job)
163
170
  await db.close()
@@ -325,6 +325,24 @@ def _get_submitted_jobs_statement() -> SelectOfScalar:
325
325
  return stm
326
326
 
327
327
 
328
+ async def _workflow_has_submitted_job(
329
+ workflow_id: int,
330
+ db: AsyncSession,
331
+ ) -> bool:
332
+
333
+ res = await db.execute(
334
+ select(JobV2.id)
335
+ .where(JobV2.status == JobStatusTypeV2.SUBMITTED)
336
+ .where(JobV2.workflow_id == workflow_id)
337
+ .limit(1)
338
+ )
339
+ submitted_jobs = res.scalar_one_or_none()
340
+ if submitted_jobs is not None:
341
+ return True
342
+
343
+ return False
344
+
345
+
328
346
  async def _workflow_insert_task(
329
347
  *,
330
348
  workflow_id: int,
@@ -22,6 +22,7 @@ from ._aux_functions import _check_workflow_exists
22
22
  from ._aux_functions import _get_project_check_owner
23
23
  from ._aux_functions import _get_submitted_jobs_statement
24
24
  from ._aux_functions import _get_workflow_check_owner
25
+ from ._aux_functions import _workflow_has_submitted_job
25
26
  from ._aux_functions_tasks import _add_warnings_to_workflow_tasks
26
27
  from fractal_server.app.models import UserOAuth
27
28
  from fractal_server.app.models.v2 import TaskGroupV2
@@ -146,6 +147,18 @@ async def update_workflow(
146
147
 
147
148
  for key, value in patch.model_dump(exclude_unset=True).items():
148
149
  if key == "reordered_workflowtask_ids":
150
+
151
+ if await _workflow_has_submitted_job(
152
+ workflow_id=workflow_id, db=db
153
+ ):
154
+ raise HTTPException(
155
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
156
+ detail=(
157
+ "Cannot re-order WorkflowTasks while a Job is running "
158
+ "for this Workflow."
159
+ ),
160
+ )
161
+
149
162
  current_workflowtask_ids = [
150
163
  wftask.id for wftask in workflow.task_list
151
164
  ]
@@ -10,6 +10,7 @@ from ....db import AsyncSession
10
10
  from ....db import get_async_db
11
11
  from ._aux_functions import _get_workflow_check_owner
12
12
  from ._aux_functions import _get_workflow_task_check_owner
13
+ from ._aux_functions import _workflow_has_submitted_job
13
14
  from ._aux_functions import _workflow_insert_task
14
15
  from ._aux_functions_tasks import _check_type_filters_compatibility
15
16
  from ._aux_functions_tasks import _get_task_read_access
@@ -224,6 +225,15 @@ async def delete_workflowtask(
224
225
  db=db,
225
226
  )
226
227
 
228
+ if await _workflow_has_submitted_job(workflow_id=workflow_id, db=db):
229
+ raise HTTPException(
230
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
231
+ detail=(
232
+ "Cannot delete a WorkflowTask while a Job is running for this "
233
+ "Workflow."
234
+ ),
235
+ )
236
+
227
237
  # Delete WorkflowTask
228
238
  await db.delete(db_workflow_task)
229
239
  await db.commit()
@@ -48,6 +48,8 @@ class _SlurmConfigSet(BaseModel):
48
48
  constraint:
49
49
  gres:
50
50
  time:
51
+ exclude:
52
+ nodelist:
51
53
  account:
52
54
  extra_lines:
53
55
  """
@@ -59,6 +61,8 @@ class _SlurmConfigSet(BaseModel):
59
61
  mem: int | str | None = None
60
62
  constraint: str | None = None
61
63
  gres: str | None = None
64
+ exclude: str | None = None
65
+ nodelist: str | None = None
62
66
  time: str | None = None
63
67
  account: str | None = None
64
68
  extra_lines: list[str] | None = None
@@ -227,6 +231,8 @@ class SlurmConfig(BaseModel):
227
231
  account: Corresponds to SLURM option.
228
232
  gpus: Corresponds to SLURM option.
229
233
  time: Corresponds to SLURM option (WARNING: not fully supported).
234
+ nodelist: Corresponds to SLURM option.
235
+ exclude: Corresponds to SLURM option.
230
236
  prefix: Prefix of configuration lines in SLURM submission scripts.
231
237
  shebang_line: Shebang line for SLURM submission scripts.
232
238
  extra_lines: Additional lines to include in SLURM submission scripts.
@@ -268,6 +274,8 @@ class SlurmConfig(BaseModel):
268
274
  gpus: str | None = None
269
275
  time: str | None = None
270
276
  account: str | None = None
277
+ nodelist: str | None = None
278
+ exclude: str | None = None
271
279
 
272
280
  # Free-field attribute for extra lines to be added to the SLURM job
273
281
  # preamble
@@ -361,6 +369,8 @@ class SlurmConfig(BaseModel):
361
369
  "gpus",
362
370
  "time",
363
371
  "account",
372
+ "exclude",
373
+ "nodelist",
364
374
  ]:
365
375
  value = getattr(self, key)
366
376
  if value is not None:
@@ -137,6 +137,34 @@ class BaseSlurmRunner(BaseRunner):
137
137
  def run_squeue(self, *, job_ids: list[str], **kwargs) -> str:
138
138
  raise NotImplementedError("Implement in child class.")
139
139
 
140
+ def _is_squeue_error_recoverable(self, exception: BaseException) -> True:
141
+ """
142
+ Determine whether a `squeue` error is considered recoverable.
143
+
144
+ A _recoverable_ error is one which will disappear after some time,
145
+ without any specific action from the `fractal-server` side.
146
+
147
+ Note: if this function returns `True` for an error that does not
148
+ actually recover, this leads to an infinite loop where
149
+ `fractal-server` keeps polling `squeue` information forever.
150
+
151
+ More info at
152
+ https://github.com/fractal-analytics-platform/fractal-server/issues/2682
153
+
154
+ Args:
155
+ exception: The exception raised by `self.run_squeue`.
156
+ Returns:
157
+ Whether the error is considered recoverable.
158
+ """
159
+ str_exception = str(exception)
160
+ if (
161
+ "slurm_load_jobs" in str_exception
162
+ and "Socket timed out on send/recv operation" in str_exception
163
+ ):
164
+ return True
165
+ else:
166
+ return False
167
+
140
168
  def _get_finished_jobs(self, job_ids: list[str]) -> set[str]:
141
169
  # If there is no Slurm job to check, return right away
142
170
  if not job_ids:
@@ -161,12 +189,26 @@ class BaseSlurmRunner(BaseRunner):
161
189
  {stdout.split()[0]: stdout.split()[1]}
162
190
  )
163
191
  except Exception as e:
164
- logger.warning(
165
- "[_get_finished_jobs] `squeue` failed for "
166
- f"{job_id=}, mark job as completed. "
192
+ msg = (
193
+ f"[_get_finished_jobs] `squeue` failed for {job_id=}. "
167
194
  f"Original error: {str(e)}."
168
195
  )
169
- slurm_statuses.update({str(job_id): "COMPLETED"})
196
+ logger.warning(msg)
197
+ if self._is_squeue_error_recoverable(e):
198
+ logger.warning(
199
+ "[_get_finished_jobs] Recoverable `squeue` "
200
+ f"error - mark {job_id=} as FRACTAL_UNDEFINED and"
201
+ " retry later."
202
+ )
203
+ slurm_statuses.update(
204
+ {str(job_id): "FRACTAL_UNDEFINED"}
205
+ )
206
+ else:
207
+ logger.warning(
208
+ "[_get_finished_jobs] Non-recoverable `squeue`"
209
+ f"error - mark {job_id=} as completed."
210
+ )
211
+ slurm_statuses.update({str(job_id): "COMPLETED"})
170
212
 
171
213
  # If a job is not in `squeue` output, mark it as completed.
172
214
  finished_jobs = {
@@ -182,33 +224,53 @@ class BaseSlurmRunner(BaseRunner):
182
224
  def _mkdir_remote_folder(self, folder: str) -> None:
183
225
  raise NotImplementedError("Implement in child class.")
184
226
 
185
- def _submit_single_sbatch(
227
+ def _enrich_slurm_config(
186
228
  self,
187
- *,
188
- base_command: str,
189
- slurm_job: SlurmJob,
190
229
  slurm_config: SlurmConfig,
191
- ) -> str:
192
- logger.debug("[_submit_single_sbatch] START")
230
+ ) -> SlurmConfig:
231
+ """
232
+ Return an enriched `SlurmConfig` object
193
233
 
194
- # Include SLURM account in `slurm_config`. Note: we make this change
195
- # here, rather than exposing a new argument of `get_slurm_config`,
196
- # because it's a backend-specific argument while `get_slurm_config` has
197
- # a generic interface.
234
+ Include `self.account` and `self.common_script_lines` into a
235
+ `SlurmConfig` object. Extracting this logic into an independent
236
+ class method is useful to fix issue #2659 (which was due to
237
+ performing this same operation multiple times rather than once).
238
+
239
+ Args:
240
+ slurm_config: The original `SlurmConfig` object.
241
+
242
+ Returns:
243
+ A new, up-to-date, `SlurmConfig` object.
244
+ """
245
+
246
+ new_slurm_config = slurm_config.model_copy()
247
+
248
+ # Include SLURM account in `slurm_config`.
198
249
  if self.slurm_account is not None:
199
- slurm_config.account = self.slurm_account
250
+ new_slurm_config.account = self.slurm_account
200
251
 
201
252
  # Include common_script_lines in extra_lines
202
253
  if len(self.common_script_lines) > 0:
203
254
  logger.debug(
204
255
  f"Add {self.common_script_lines} to "
205
- f"{slurm_config.extra_lines=}."
256
+ f"{new_slurm_config.extra_lines=}."
206
257
  )
207
- current_extra_lines = slurm_config.extra_lines or []
208
- slurm_config.extra_lines = (
258
+ current_extra_lines = new_slurm_config.extra_lines or []
259
+ new_slurm_config.extra_lines = (
209
260
  current_extra_lines + self.common_script_lines
210
261
  )
211
262
 
263
+ return new_slurm_config
264
+
265
+ def _submit_single_sbatch(
266
+ self,
267
+ *,
268
+ base_command: str,
269
+ slurm_job: SlurmJob,
270
+ slurm_config: SlurmConfig,
271
+ ) -> str:
272
+ logger.debug("[_submit_single_sbatch] START")
273
+
212
274
  for task in slurm_job.tasks:
213
275
  # Write input file
214
276
  if self.slurm_runner_type == "ssh":
@@ -508,6 +570,9 @@ class BaseSlurmRunner(BaseRunner):
508
570
  user_id: int,
509
571
  ) -> tuple[Any, Exception]:
510
572
  logger.debug("[submit] START")
573
+
574
+ config = self._enrich_slurm_config(config)
575
+
511
576
  try:
512
577
  workdir_local = task_files.wftask_subfolder_local
513
578
  workdir_remote = task_files.wftask_subfolder_remote
@@ -649,6 +714,8 @@ class BaseSlurmRunner(BaseRunner):
649
714
  input images, while for compound tasks these can differ.
650
715
  """
651
716
 
717
+ config = self._enrich_slurm_config(config)
718
+
652
719
  logger.debug(f"[multisubmit] START, {len(list_parameters)=}")
653
720
  try:
654
721
  if self.is_shutdown():
@@ -125,7 +125,14 @@ def get_slurm_config_internal(
125
125
  )
126
126
  logger.error(error_msg)
127
127
  raise SlurmConfigError(error_msg)
128
- for key in ["time", "gres", "gpus", "constraint"]:
128
+ for key in [
129
+ "time",
130
+ "gres",
131
+ "gpus",
132
+ "constraint",
133
+ "nodelist",
134
+ "exclude",
135
+ ]:
129
136
  value = wftask_meta.get(key, None)
130
137
  if value is not None:
131
138
  slurm_dict[key] = value
@@ -1,6 +1,5 @@
1
1
  import argparse
2
2
  import json
3
- import logging
4
3
  import os
5
4
  import sys
6
5
 
@@ -32,7 +31,6 @@ def worker(
32
31
  # Create output folder, if missing
33
32
  out_dir = os.path.dirname(out_fname)
34
33
  if not os.path.exists(out_dir):
35
- logging.debug(f"_slurm.remote.worker: create {out_dir=}")
36
34
  os.mkdir(out_dir)
37
35
 
38
36
  # Execute the job and capture exceptions
@@ -40,10 +38,8 @@ def worker(
40
38
  with open(in_fname) as f:
41
39
  input_data = json.load(f)
42
40
 
43
- server_python_version = input_data["python_version"]
44
- server_fractal_server_version = input_data["fractal_server_version"]
45
-
46
41
  # Fractal-server version must be identical
42
+ server_fractal_server_version = input_data["fractal_server_version"]
47
43
  worker_fractal_server_version = __VERSION__
48
44
  if worker_fractal_server_version != server_fractal_server_version:
49
45
  raise FractalVersionMismatch(
@@ -51,11 +47,16 @@ def worker(
51
47
  f"{worker_fractal_server_version=}"
52
48
  )
53
49
 
54
- # Python version mismatch only raises a warning
55
- worker_python_version = tuple(sys.version_info[:3])
50
+ # Get `worker_python_version` as a `list` since this is the type of
51
+ # `server_python_version` after a JSON dump/load round trip.
52
+ worker_python_version = list(sys.version_info[:3])
53
+
54
+ # Print a warning for Python version mismatch
55
+ server_python_version = input_data["python_version"]
56
56
  if worker_python_version != server_python_version:
57
57
  if worker_python_version[:2] != server_python_version[:2]:
58
- logging.warning(
58
+ print(
59
+ "WARNING: "
59
60
  f"{server_python_version=} but {worker_python_version=}."
60
61
  )
61
62
 
@@ -116,7 +117,6 @@ if __name__ == "__main__":
116
117
  required=True,
117
118
  )
118
119
  parsed_args = parser.parse_args()
119
- logging.debug(f"{parsed_args=}")
120
120
 
121
121
  kwargs = dict(
122
122
  in_fname=parsed_args.input_file,
@@ -20,7 +20,6 @@ from pathlib import Path
20
20
  from ....ssh._fabric import FractalSSH
21
21
  from ...models.v2 import DatasetV2
22
22
  from ...models.v2 import WorkflowV2
23
- from ..exceptions import JobExecutionError
24
23
  from ..executors.slurm_common.get_slurm_config import get_slurm_config
25
24
  from ..executors.slurm_ssh.runner import SlurmSSHRunner
26
25
  from ..set_start_and_last_task_index import set_start_and_last_task_index
@@ -64,18 +63,6 @@ def process_workflow(
64
63
  if isinstance(worker_init, str):
65
64
  worker_init = worker_init.split("\n")
66
65
 
67
- # Create main remote folder
68
- try:
69
- fractal_ssh.mkdir(folder=str(workflow_dir_remote))
70
- logger.info(f"Created {str(workflow_dir_remote)} via SSH.")
71
- except Exception as e:
72
- error_msg = (
73
- f"Could not create {str(workflow_dir_remote)} via SSH.\n"
74
- f"Original error: {str(e)}."
75
- )
76
- logger.error(error_msg)
77
- raise JobExecutionError(info=error_msg)
78
-
79
66
  with SlurmSSHRunner(
80
67
  fractal_ssh=fractal_ssh,
81
68
  root_dir_local=workflow_dir_local,
@@ -0,0 +1,264 @@
1
+ """JSON to JSONB
2
+
3
+ Revision ID: b3ffb095f973
4
+ Revises: b1e7f7a1ff71
5
+ Create Date: 2025-06-19 10:12:06.699107
6
+
7
+ """
8
+ import sqlalchemy as sa
9
+ from alembic import op
10
+ from sqlalchemy.dialects import postgresql
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = "b3ffb095f973"
14
+ down_revision = "b1e7f7a1ff71"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ # ### commands auto generated by Alembic - please adjust! ###
21
+ with op.batch_alter_table("datasetv2", schema=None) as batch_op:
22
+ batch_op.alter_column(
23
+ "history",
24
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
25
+ type_=postgresql.JSONB(astext_type=sa.Text()),
26
+ existing_nullable=False,
27
+ existing_server_default=sa.text("'[]'::json"),
28
+ )
29
+ batch_op.alter_column(
30
+ "images",
31
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
32
+ type_=postgresql.JSONB(astext_type=sa.Text()),
33
+ existing_nullable=False,
34
+ existing_server_default=sa.text("'[]'::json"),
35
+ )
36
+
37
+ with op.batch_alter_table("jobv2", schema=None) as batch_op:
38
+ batch_op.alter_column(
39
+ "dataset_dump",
40
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
41
+ type_=postgresql.JSONB(astext_type=sa.Text()),
42
+ existing_nullable=False,
43
+ )
44
+ batch_op.alter_column(
45
+ "workflow_dump",
46
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
47
+ type_=postgresql.JSONB(astext_type=sa.Text()),
48
+ existing_nullable=False,
49
+ )
50
+ batch_op.alter_column(
51
+ "project_dump",
52
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
53
+ type_=postgresql.JSONB(astext_type=sa.Text()),
54
+ existing_nullable=False,
55
+ )
56
+ batch_op.alter_column(
57
+ "attribute_filters",
58
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
59
+ type_=postgresql.JSONB(astext_type=sa.Text()),
60
+ existing_nullable=False,
61
+ existing_server_default=sa.text("'{}'::json"),
62
+ )
63
+ batch_op.alter_column(
64
+ "type_filters",
65
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
66
+ type_=postgresql.JSONB(astext_type=sa.Text()),
67
+ existing_nullable=False,
68
+ existing_server_default=sa.text("'{}'::json"),
69
+ )
70
+
71
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
72
+ batch_op.alter_column(
73
+ "pinned_package_versions",
74
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
75
+ type_=postgresql.JSONB(astext_type=sa.Text()),
76
+ existing_nullable=True,
77
+ existing_server_default=sa.text("'{}'::json"),
78
+ )
79
+
80
+ with op.batch_alter_table("taskv2", schema=None) as batch_op:
81
+ batch_op.alter_column(
82
+ "input_types",
83
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
84
+ type_=postgresql.JSONB(astext_type=sa.Text()),
85
+ existing_nullable=True,
86
+ )
87
+ batch_op.alter_column(
88
+ "output_types",
89
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
90
+ type_=postgresql.JSONB(astext_type=sa.Text()),
91
+ existing_nullable=True,
92
+ )
93
+ batch_op.alter_column(
94
+ "tags",
95
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
96
+ type_=postgresql.JSONB(astext_type=sa.Text()),
97
+ existing_nullable=False,
98
+ existing_server_default=sa.text("'[]'::json"),
99
+ )
100
+
101
+ with op.batch_alter_table("user_settings", schema=None) as batch_op:
102
+ batch_op.alter_column(
103
+ "slurm_accounts",
104
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
105
+ type_=postgresql.JSONB(astext_type=sa.Text()),
106
+ existing_nullable=False,
107
+ existing_server_default=sa.text("'[]'::json"),
108
+ )
109
+
110
+ with op.batch_alter_table("usergroup", schema=None) as batch_op:
111
+ batch_op.alter_column(
112
+ "viewer_paths",
113
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
114
+ type_=postgresql.JSONB(astext_type=sa.Text()),
115
+ existing_nullable=False,
116
+ existing_server_default=sa.text("'[]'::json"),
117
+ )
118
+
119
+ with op.batch_alter_table("workflowtaskv2", schema=None) as batch_op:
120
+ batch_op.alter_column(
121
+ "args_parallel",
122
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
123
+ type_=postgresql.JSONB(astext_type=sa.Text()),
124
+ existing_nullable=True,
125
+ )
126
+ batch_op.alter_column(
127
+ "args_non_parallel",
128
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
129
+ type_=postgresql.JSONB(astext_type=sa.Text()),
130
+ existing_nullable=True,
131
+ )
132
+ batch_op.alter_column(
133
+ "type_filters",
134
+ existing_type=postgresql.JSON(astext_type=sa.Text()),
135
+ type_=postgresql.JSONB(astext_type=sa.Text()),
136
+ existing_nullable=False,
137
+ existing_server_default=sa.text("'{}'::json"),
138
+ )
139
+
140
+ # ### end Alembic commands ###
141
+
142
+
143
+ def downgrade() -> None:
144
+ # ### commands auto generated by Alembic - please adjust! ###
145
+ with op.batch_alter_table("workflowtaskv2", schema=None) as batch_op:
146
+ batch_op.alter_column(
147
+ "type_filters",
148
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
149
+ type_=postgresql.JSON(astext_type=sa.Text()),
150
+ existing_nullable=False,
151
+ existing_server_default=sa.text("'{}'::json"),
152
+ )
153
+ batch_op.alter_column(
154
+ "args_non_parallel",
155
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
156
+ type_=postgresql.JSON(astext_type=sa.Text()),
157
+ existing_nullable=True,
158
+ )
159
+ batch_op.alter_column(
160
+ "args_parallel",
161
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
162
+ type_=postgresql.JSON(astext_type=sa.Text()),
163
+ existing_nullable=True,
164
+ )
165
+
166
+ with op.batch_alter_table("usergroup", schema=None) as batch_op:
167
+ batch_op.alter_column(
168
+ "viewer_paths",
169
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
170
+ type_=postgresql.JSON(astext_type=sa.Text()),
171
+ existing_nullable=False,
172
+ existing_server_default=sa.text("'[]'::json"),
173
+ )
174
+
175
+ with op.batch_alter_table("user_settings", schema=None) as batch_op:
176
+ batch_op.alter_column(
177
+ "slurm_accounts",
178
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
179
+ type_=postgresql.JSON(astext_type=sa.Text()),
180
+ existing_nullable=False,
181
+ existing_server_default=sa.text("'[]'::json"),
182
+ )
183
+
184
+ with op.batch_alter_table("taskv2", schema=None) as batch_op:
185
+ batch_op.alter_column(
186
+ "tags",
187
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
188
+ type_=postgresql.JSON(astext_type=sa.Text()),
189
+ existing_nullable=False,
190
+ existing_server_default=sa.text("'[]'::json"),
191
+ )
192
+ batch_op.alter_column(
193
+ "output_types",
194
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
195
+ type_=postgresql.JSON(astext_type=sa.Text()),
196
+ existing_nullable=True,
197
+ )
198
+ batch_op.alter_column(
199
+ "input_types",
200
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
201
+ type_=postgresql.JSON(astext_type=sa.Text()),
202
+ existing_nullable=True,
203
+ )
204
+
205
+ with op.batch_alter_table("taskgroupv2", schema=None) as batch_op:
206
+ batch_op.alter_column(
207
+ "pinned_package_versions",
208
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
209
+ type_=postgresql.JSON(astext_type=sa.Text()),
210
+ existing_nullable=True,
211
+ existing_server_default=sa.text("'{}'::json"),
212
+ )
213
+
214
+ with op.batch_alter_table("jobv2", schema=None) as batch_op:
215
+ batch_op.alter_column(
216
+ "type_filters",
217
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
218
+ type_=postgresql.JSON(astext_type=sa.Text()),
219
+ existing_nullable=False,
220
+ existing_server_default=sa.text("'{}'::json"),
221
+ )
222
+ batch_op.alter_column(
223
+ "attribute_filters",
224
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
225
+ type_=postgresql.JSON(astext_type=sa.Text()),
226
+ existing_nullable=False,
227
+ existing_server_default=sa.text("'{}'::json"),
228
+ )
229
+ batch_op.alter_column(
230
+ "project_dump",
231
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
232
+ type_=postgresql.JSON(astext_type=sa.Text()),
233
+ existing_nullable=False,
234
+ )
235
+ batch_op.alter_column(
236
+ "workflow_dump",
237
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
238
+ type_=postgresql.JSON(astext_type=sa.Text()),
239
+ existing_nullable=False,
240
+ )
241
+ batch_op.alter_column(
242
+ "dataset_dump",
243
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
244
+ type_=postgresql.JSON(astext_type=sa.Text()),
245
+ existing_nullable=False,
246
+ )
247
+
248
+ with op.batch_alter_table("datasetv2", schema=None) as batch_op:
249
+ batch_op.alter_column(
250
+ "images",
251
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
252
+ type_=postgresql.JSON(astext_type=sa.Text()),
253
+ existing_nullable=False,
254
+ existing_server_default=sa.text("'[]'::json"),
255
+ )
256
+ batch_op.alter_column(
257
+ "history",
258
+ existing_type=postgresql.JSONB(astext_type=sa.Text()),
259
+ type_=postgresql.JSON(astext_type=sa.Text()),
260
+ existing_nullable=False,
261
+ existing_server_default=sa.text("'[]'::json"),
262
+ )
263
+
264
+ # ### end Alembic commands ###
@@ -165,7 +165,11 @@ class FractalSSH:
165
165
  raise e
166
166
 
167
167
  def _run(
168
- self, *args, label: str, lock_timeout: float | None = None, **kwargs
168
+ self,
169
+ *args,
170
+ label: str,
171
+ lock_timeout: float | None = None,
172
+ **kwargs,
169
173
  ) -> Any:
170
174
  actual_lock_timeout = self.default_lock_timeout
171
175
  if lock_timeout is not None:
@@ -353,6 +357,7 @@ class FractalSSH:
353
357
  label=f"run {cmd}",
354
358
  lock_timeout=actual_lock_timeout,
355
359
  hide=True,
360
+ in_stream=False,
356
361
  )
357
362
  t_1 = time.perf_counter()
358
363
  self.logger.info(
@@ -161,7 +161,7 @@ def collect_local_pixi(
161
161
 
162
162
  # Make task folder 755
163
163
  source_dir = Path(task_group.path, SOURCE_DIR_NAME).as_posix()
164
- command = f"chmod 755 {source_dir} -R"
164
+ command = f"chmod -R 755 {source_dir}"
165
165
  execute_command_sync(
166
166
  command=command,
167
167
  logger_name=LOGGER_NAME,
@@ -145,7 +145,7 @@ def reactivate_local_pixi(
145
145
 
146
146
  # Make task folder 755
147
147
  source_dir = Path(task_group.path, SOURCE_DIR_NAME).as_posix()
148
- command = f"chmod 755 {source_dir} -R"
148
+ command = f"chmod -R 755 {source_dir}"
149
149
  execute_command_sync(
150
150
  command=command,
151
151
  logger_name=LOGGER_NAME,
@@ -182,19 +182,21 @@ def collect_ssh_pixi(
182
182
  )
183
183
 
184
184
  # Run the three pixi-related scripts
185
- _customize_and_run_template(
185
+ stdout = _customize_and_run_template(
186
186
  template_filename="pixi_1_extract.sh",
187
187
  replacements=replacements,
188
188
  **common_args,
189
189
  )
190
+ logger.debug(f"STDOUT: {stdout}")
190
191
  activity.log = get_current_log(log_file_path)
191
192
  activity = add_commit_refresh(obj=activity, db=db)
192
193
 
193
- _customize_and_run_template(
194
+ stdout = _customize_and_run_template(
194
195
  template_filename="pixi_2_install.sh",
195
196
  replacements=replacements,
196
197
  **common_args,
197
198
  )
199
+ logger.debug(f"STDOUT: {stdout}")
198
200
  activity.log = get_current_log(log_file_path)
199
201
  activity = add_commit_refresh(obj=activity, db=db)
200
202
 
@@ -203,6 +205,7 @@ def collect_ssh_pixi(
203
205
  replacements=replacements,
204
206
  **common_args,
205
207
  )
208
+ logger.debug(f"STDOUT: {stdout}")
206
209
  activity.log = get_current_log(log_file_path)
207
210
  activity = add_commit_refresh(obj=activity, db=db)
208
211
 
@@ -218,7 +221,7 @@ def collect_ssh_pixi(
218
221
  source_dir = Path(
219
222
  task_group.path, SOURCE_DIR_NAME
220
223
  ).as_posix()
221
- fractal_ssh.run_command(cmd=f"chmod 755 {source_dir} -R")
224
+ fractal_ssh.run_command(cmd=f"chmod -R 755 {source_dir}")
222
225
 
223
226
  # Read and validate remote manifest file
224
227
  manifest_path_remote = (
@@ -152,11 +152,12 @@ def reactivate_ssh_pixi(
152
152
  )
153
153
 
154
154
  # Run script 1 - extract tar.gz into `source_dir`
155
- _customize_and_run_template(
155
+ stdout = _customize_and_run_template(
156
156
  template_filename="pixi_1_extract.sh",
157
157
  replacements=replacements,
158
158
  **common_args,
159
159
  )
160
+ logger.debug(f"STDOUT: {stdout}")
160
161
  activity.log = get_current_log(log_file_path)
161
162
  activity = add_commit_refresh(obj=activity, db=db)
162
163
 
@@ -176,24 +177,26 @@ def reactivate_ssh_pixi(
176
177
  )
177
178
 
178
179
  # Run script 2 - run pixi-install command
179
- _customize_and_run_template(
180
+ stdout = _customize_and_run_template(
180
181
  template_filename="pixi_2_install.sh",
181
182
  replacements=replacements,
182
183
  **common_args,
183
184
  )
185
+ logger.debug(f"STDOUT: {stdout}")
184
186
  activity.log = get_current_log(log_file_path)
185
187
  activity = add_commit_refresh(obj=activity, db=db)
186
188
 
187
189
  # Run script 3 - post-install
188
- _customize_and_run_template(
190
+ stdout = _customize_and_run_template(
189
191
  template_filename="pixi_3_post_install.sh",
190
192
  replacements=replacements,
191
193
  **common_args,
192
194
  )
195
+ logger.debug(f"STDOUT: {stdout}")
193
196
  activity.log = get_current_log(log_file_path)
194
197
  activity = add_commit_refresh(obj=activity, db=db)
195
198
 
196
- fractal_ssh.run_command(cmd=f"chmod 755 {source_dir} -R")
199
+ fractal_ssh.run_command(cmd=f"chmod -R 755 {source_dir}")
197
200
 
198
201
  # Finalize (write metadata to DB)
199
202
  activity.status = TaskGroupActivityStatusV2.OK
@@ -2,7 +2,7 @@ set -e
2
2
 
3
3
  write_log(){
4
4
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
5
- echo "[collect-task-pixi, ${TIMESTAMP}] ${1}"
5
+ echo "[extract-tar-gz-pixi, ${TIMESTAMP}] ${1}"
6
6
  }
7
7
 
8
8
  # Replacements
@@ -2,7 +2,7 @@ set -e
2
2
 
3
3
  write_log(){
4
4
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
5
- echo "[collect-task-pixi, ${TIMESTAMP}] ${1}"
5
+ echo "[install-tasks-pixi, ${TIMESTAMP}] ${1}"
6
6
  }
7
7
 
8
8
  # Replacements
@@ -31,7 +31,7 @@ export TOKIO_WORKER_THREADS="${TOKIO_WORKER_THREADS}"
31
31
 
32
32
  TIME_START=$(date +%s)
33
33
 
34
- echo "Hostname: $(hostname)"
34
+ write_log "Hostname: $(hostname)"
35
35
 
36
36
  cd "${PACKAGE_DIR}"
37
37
  write_log "Changed working directory to ${PACKAGE_DIR}"
@@ -2,7 +2,7 @@ set -e
2
2
 
3
3
  write_log(){
4
4
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
5
- echo "[collect-task-pixi, ${TIMESTAMP}] ${1}"
5
+ echo "[after-install-pixi, ${TIMESTAMP}] ${1}"
6
6
  }
7
7
 
8
8
  # Replacements
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fractal-server
3
- Version: 2.15.0a5
3
+ Version: 2.15.2
4
4
  Summary: Backend component of the Fractal analytics platform
5
5
  License: BSD-3-Clause
6
6
  Author: Tommaso Comparin
@@ -1,36 +1,36 @@
1
- fractal_server/__init__.py,sha256=EiSBiIGr3J4HykWpwvOxbtsBmNSbQnrB9Kqpelw5XWA,25
1
+ fractal_server/__init__.py,sha256=3aUOEwrozdEb6uIH3H2d1pXz5bTLTWOObIlVGVMgxVc,23
2
2
  fractal_server/__main__.py,sha256=rkM8xjY1KeS3l63irB8yCrlVobR-73uDapC4wvrIlxI,6957
3
3
  fractal_server/alembic.ini,sha256=MWwi7GzjzawI9cCAK1LW7NxIBQDUqD12-ptJoq5JpP0,3153
4
4
  fractal_server/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- fractal_server/app/db/__init__.py,sha256=EFzcf6iKemWlOSRj4vtDT63hAE9HBYWh4abYOdDwzMo,2907
5
+ fractal_server/app/db/__init__.py,sha256=U2gwpNyy79iMsK1lg43LRl9z-MW8wiOaICJ7GGdA4yo,2814
6
6
  fractal_server/app/models/__init__.py,sha256=xJWiGAwpXmCpnFMC4c_HTqoUCzMOXrakoGLUH_uMvdA,415
7
7
  fractal_server/app/models/linkusergroup.py,sha256=3KkkE4QIUAlTrBAZs_tVy0pGvAxUAq6yOEjflct_z2M,678
8
8
  fractal_server/app/models/linkuserproject.py,sha256=hvaxh3Lkiy2uUCwB8gvn8RorCpvxSSdzWdCS_U1GL7g,315
9
- fractal_server/app/models/security.py,sha256=PVZ3nTZO3TYpOTLiMARNy2mHAET49i6nE7bKxn1H-vQ,3836
10
- fractal_server/app/models/user_settings.py,sha256=RxzRBGLHF_wc5csrTeHGUSV77Md_X0Lf-SnYVOsEWHc,1263
9
+ fractal_server/app/models/security.py,sha256=NfR0I4dRbOEmCWOKeEHyFO-uqhSJ11dS0B6yWtZRqs4,3852
10
+ fractal_server/app/models/user_settings.py,sha256=WdnrLOP2w8Nqh_3K-4-b-8a7XEC9ILrE6SfbYoTk-7Y,1279
11
11
  fractal_server/app/models/v2/__init__.py,sha256=vjHwek7-IXmaZZL9VF0nD30YL9ca4wNc8P4RXJK_kDc,832
12
12
  fractal_server/app/models/v2/accounting.py,sha256=i-2TsjqyuclxFQ21C-TeDoss7ZBTRuXdzIJfVr2UxwE,1081
13
- fractal_server/app/models/v2/dataset.py,sha256=B_bPnYCSLRFN-vBIOc5nJ31JTruQPxLda9mqpPIJmGk,1209
13
+ fractal_server/app/models/v2/dataset.py,sha256=P_zy4dPQAqrCALQ6737VkAFk1SvcgYjnslGUZhPI8sc,1226
14
14
  fractal_server/app/models/v2/history.py,sha256=CBN2WVg9vW5pHU1RP8TkB_nnJrwnuifCcxgnd53UtEE,2163
15
- fractal_server/app/models/v2/job.py,sha256=LfpwAedMVcA_6Ne0Rr4g3tt0asAQkWz3LSPm7IwZhYc,1978
15
+ fractal_server/app/models/v2/job.py,sha256=e3Un_rUgWC-KazGLDQqy17NQK_2ZsL3EmEmDAky_bN0,1998
16
16
  fractal_server/app/models/v2/project.py,sha256=RmU5BQR4HD6xifRndUhvPBy30wntml-giBRoEysdWXw,755
17
- fractal_server/app/models/v2/task.py,sha256=P7nsS5mCmVyzr4WtcjoiedesqkWvkHA2cQPsMbQt-7o,1427
18
- fractal_server/app/models/v2/task_group.py,sha256=Q78ommMWEG2Sqvg2Y8ICgYA_aGH-N7LdLbnmnDl1l1M,3841
17
+ fractal_server/app/models/v2/task.py,sha256=iBIQB8POQE5MyKvLZhw7jZWlBhbrThzCDzRTcgiAczQ,1493
18
+ fractal_server/app/models/v2/task_group.py,sha256=1cn14RKKOOCCjh42iaT-HyuRrRpCPcYhWRrlMK-Enwc,3857
19
19
  fractal_server/app/models/v2/workflow.py,sha256=wuK9SV1TXrlYcieYLYj5fGvV3K3bW7g9jCM1uv9HHjA,1058
20
- fractal_server/app/models/v2/workflowtask.py,sha256=tph237DXitOnzSv88rk9qgN2VmlI1smWS5fNYHR8jMo,1200
20
+ fractal_server/app/models/v2/workflowtask.py,sha256=qkTc-hcFLpJUVsEUbnDq2BJL0qg9jagy2doZeusF1ek,1266
21
21
  fractal_server/app/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  fractal_server/app/routes/admin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  fractal_server/app/routes/admin/v2/__init__.py,sha256=_5lqb6-M8-fZqE1HRMep6pAFYRUKMxrvbZOKs-RXWkw,933
24
24
  fractal_server/app/routes/admin/v2/accounting.py,sha256=YPWwCWylXrJpV4bq_dJ3t6Kn5uuveTrFx-5w1wzfETU,3594
25
25
  fractal_server/app/routes/admin/v2/impersonate.py,sha256=gc4lshfEPFR6W2asH7aKu6hqE6chzusdhAUVV9p51eU,1131
26
- fractal_server/app/routes/admin/v2/job.py,sha256=EOnW645RaacyNof55O_NV_4ONyb7ihM9ORTPb0v68xY,7373
26
+ fractal_server/app/routes/admin/v2/job.py,sha256=VcyXHYjieOKnTAi1NsiO_bK3A6UufUwX2lmWCwa4sa0,7585
27
27
  fractal_server/app/routes/admin/v2/project.py,sha256=MA_LdoEuSuisSGRO43TapMuJ080y5iaUGSAUgKuuKOg,1188
28
28
  fractal_server/app/routes/admin/v2/task.py,sha256=93QIbWZNnqaBhG9R9-RStDX2mpqRNN3G7BIb0KM-jeE,4312
29
29
  fractal_server/app/routes/admin/v2/task_group.py,sha256=biibAvMPD2w-267eyTm3wH2s3mITjiS5gYzwCCwmLbI,7099
30
30
  fractal_server/app/routes/admin/v2/task_group_lifecycle.py,sha256=2J3M9VXWD_0j9jRTZ5APuUXl9E-aVv0qF8K02vvcO3s,9150
31
31
  fractal_server/app/routes/api/__init__.py,sha256=B8l6PSAhR10iZqHEiyTat-_0tkeKdrCigIE6DJGP5b8,638
32
32
  fractal_server/app/routes/api/v2/__init__.py,sha256=D3sRRsqkmZO6kBxUjg40q0aRDsnuXI4sOOfn0xF9JsM,2820
33
- fractal_server/app/routes/api/v2/_aux_functions.py,sha256=P5exwdiNm0ZxtoGw4wxvm_-u8e83gXz8iYEVFuUq_cU,12792
33
+ fractal_server/app/routes/api/v2/_aux_functions.py,sha256=oavMb8HM4lWKMW7_Iyx8Sc9AHKpbmFyyqcPqQLstP_I,13192
34
34
  fractal_server/app/routes/api/v2/_aux_functions_history.py,sha256=Z23xwvBaVEEQ5B-JsWZJpjj4_QqoXqHYONztnbAH6gw,4425
35
35
  fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py,sha256=GpKfw9yj01LmOAuNMTOreU1PFkCKpjK5oCt7_wp35-A,6741
36
36
  fractal_server/app/routes/api/v2/_aux_functions_task_version_update.py,sha256=WLDOYCnb6fnS5avKflyx6yN24Vo1n5kJk5ZyiKbzb8Y,1175
@@ -51,9 +51,9 @@ fractal_server/app/routes/api/v2/task_collection_pixi.py,sha256=LS5xOYRRvI25TyvP
51
51
  fractal_server/app/routes/api/v2/task_group.py,sha256=Wmp5Rt6NQm8_EbdJyi3XOkTXxJTTd4MNIy0ja6K-ifA,9205
52
52
  fractal_server/app/routes/api/v2/task_group_lifecycle.py,sha256=-uS_z8E3__t_twEqhZOzcEcAxZsgnpg-c7Ya9RF3_bs,9998
53
53
  fractal_server/app/routes/api/v2/task_version_update.py,sha256=o8W_C0I84X0u8gAMnCvi8ChiVAKrb5WzUBuJLSuujCA,8235
54
- fractal_server/app/routes/api/v2/workflow.py,sha256=gwMtpfUY_JiTv5_R_q1I9WNkp6nTqEVtYx8jWNJRxcU,10227
54
+ fractal_server/app/routes/api/v2/workflow.py,sha256=SfjegoVO4DaGmDD7OPhWNLkcvZhJKwNX4DTQAcVKk9Q,10699
55
55
  fractal_server/app/routes/api/v2/workflow_import.py,sha256=kOGDaCj0jCGK1WSYGbnUjtUg2U1YxUY9UMH-2ilqJg4,9027
56
- fractal_server/app/routes/api/v2/workflowtask.py,sha256=KQU9rSQNhc6TRFdUYM09zty8Bu150sKvcLGz_tX4Fgo,7548
56
+ fractal_server/app/routes/api/v2/workflowtask.py,sha256=5_SQAG8ztDnaaRXwKalcO69HVpSl-QbrhiI7fCP3YRI,7924
57
57
  fractal_server/app/routes/auth/__init__.py,sha256=fao6CS0WiAjHDTvBzgBVV_bSXFpEAeDBF6Z6q7rRkPc,1658
58
58
  fractal_server/app/routes/auth/_aux_auth.py,sha256=UZgauY0V6mSqjte_sYI1cBl2h8bcbLaeWzgpl1jdJlk,4883
59
59
  fractal_server/app/routes/auth/current_user.py,sha256=EjkwMxUA0l6FLbDJdertHGnuOoSS-HEysmm6l5FkAlY,5903
@@ -80,10 +80,10 @@ fractal_server/app/runner/executors/local/runner.py,sha256=DZK_oVxjIewyo7tjB7HvT
80
80
  fractal_server/app/runner/executors/slurm_common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
81
  fractal_server/app/runner/executors/slurm_common/_batching.py,sha256=gbHZIxt90GjUwhB9_UInwVqpX-KdxRQMDeXzUagdL3U,8816
82
82
  fractal_server/app/runner/executors/slurm_common/_job_states.py,sha256=nuV-Zba38kDrRESOVB3gaGbrSPZc4q7YGichQaeqTW0,238
83
- fractal_server/app/runner/executors/slurm_common/_slurm_config.py,sha256=Zv2l_6X1EfSHGRqcBMj2dbai_kP8hfuMfh-WoIUj0tY,15646
84
- fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py,sha256=2F2zgg3DJKAJ5LecFAzMSGLFmsMiM4lMk4Kh9It35F4,35626
85
- fractal_server/app/runner/executors/slurm_common/get_slurm_config.py,sha256=VJNryceLzF5_fx9_lS1nGq85EW8rOQ0KrgtMATcfdQc,7271
86
- fractal_server/app/runner/executors/slurm_common/remote.py,sha256=xWnI6WktHR_7cxUme72ztIeBb4osnbZNu6J2azWn9K8,3765
83
+ fractal_server/app/runner/executors/slurm_common/_slurm_config.py,sha256=U9BONnnwn8eDqDevwUtFSBcvIsxvNgDHirhcQGJ9t9E,15947
84
+ fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py,sha256=1Sh56lb7NERVtsBMvVs4K7nVHhMy_KDbwquPl1ub8vE,37937
85
+ fractal_server/app/runner/executors/slurm_common/get_slurm_config.py,sha256=jhoFHauWJm55bIC_v7pFylbK8WgcRJemGu2OjUiRbpQ,7377
86
+ fractal_server/app/runner/executors/slurm_common/remote.py,sha256=LHK2Ram8X8q6jNSCxnnwKUwmSJMsyQyRem_VjH53qdw,3811
87
87
  fractal_server/app/runner/executors/slurm_common/slurm_job_task_models.py,sha256=K4SdJOKsUWzDlnkb8Ug_UmTx6nBMsTqn9_oKqwE4XDI,3520
88
88
  fractal_server/app/runner/executors/slurm_ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
89
  fractal_server/app/runner/executors/slurm_ssh/run_subprocess.py,sha256=SyW6t4egvbiARph2YkFjc88Hj94fCamZVi50L7ph8VM,996
@@ -98,7 +98,7 @@ fractal_server/app/runner/shutdown.py,sha256=ViSNJyXWU_iWPSDOOMGNh_iQdUFrdPh_jvf
98
98
  fractal_server/app/runner/task_files.py,sha256=V_7aZhu6-c6Y-0XUe-5cZVDrdnXEJhp8pQoUMtx6ko0,4041
99
99
  fractal_server/app/runner/v2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
100
  fractal_server/app/runner/v2/_local.py,sha256=tTJgABK-zAZmmRzoie_MPNTXJx_zBAXiZiiWl1CC2qo,3035
101
- fractal_server/app/runner/v2/_slurm_ssh.py,sha256=VN89sFwqi139m9wpO1LmExIYIVhmYbEoMgtX7kLAMhE,3302
101
+ fractal_server/app/runner/v2/_slurm_ssh.py,sha256=JlDngsVSOUNqEubDl-5jkIxQQmV4mhTqbMbJVY_rL6M,2840
102
102
  fractal_server/app/runner/v2/_slurm_sudo.py,sha256=Gvsh4tUlc1_3KdF3B7zEqs-YIntC_joLtTGSNFbKKSs,2939
103
103
  fractal_server/app/runner/v2/db_tools.py,sha256=du5dKhMMFMErQXbGIgu9JvO_vtMensodyPsyDeqz1yQ,3324
104
104
  fractal_server/app/runner/v2/deduplicate_list.py,sha256=IVTE4abBU1bUprFTkxrTfYKnvkNTanWQ-KWh_etiT08,645
@@ -171,6 +171,7 @@ fractal_server/migrations/versions/a7f4d6137b53_add_workflow_dump_to_applyworkfl
171
171
  fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py,sha256=BftudWuSGvKGBzIL5AMb3yWkgTAuaKPBGsYcOzp_gLQ,1899
172
172
  fractal_server/migrations/versions/af8673379a5c_drop_old_filter_columns.py,sha256=9sLd0F7nO5chHHm7RZ4wBA-9bvWomS-av_odKwODADM,1551
173
173
  fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py,sha256=loDrqBB-9U3vqLKePEeJy4gK4EuPs_1F345mdrnoCt0,1293
174
+ fractal_server/migrations/versions/b3ffb095f973_json_to_jsonb.py,sha256=Q01lPlBNQgi3hpoUquWj2QUEF7cTsyQ7uikUhWunzWY,10035
174
175
  fractal_server/migrations/versions/c90a7c76e996_job_id_in_history_run.py,sha256=Y1cPwmFOZ4mx3v2XZM6adgu8u0L0VD_R4ADURyMb2ro,1102
175
176
  fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py,sha256=HN3_Pk8G81SzdYjg4K1RZAyjKSlsZGvcYE2nWOUbwxQ,3861
176
177
  fractal_server/migrations/versions/d4fe3708d309_make_applyworkflow_workflow_dump_non_.py,sha256=6cHEZFuTXiQg9yu32Y3RH1XAl71av141WQ6UMbiITIg,949
@@ -184,7 +185,7 @@ fractal_server/migrations/versions/f384e1c0cf5d_drop_task_default_args_columns.p
184
185
  fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py,sha256=TDWCaIoM0Q4SpRWmR9zr_rdp3lJXhCfBPTMhtrP5xYE,3950
185
186
  fractal_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
186
187
  fractal_server/ssh/__init__.py,sha256=sVUmzxf7_DuXG1xoLQ1_00fo5NPhi2LJipSmU5EAkPs,124
187
- fractal_server/ssh/_fabric.py,sha256=3Q0nlXctEXtRspQvg00wzCOyYK-0fAJlCowQuXEMk9I,25118
188
+ fractal_server/ssh/_fabric.py,sha256=7fCxTYqkAOaTTm67trfYdYQenOsI4EfrRQoG6x3M5kk,25188
188
189
  fractal_server/string_tools.py,sha256=qLB5u6-4QxXPiZrUeWn_cEo47axj4OXFzDd47kNTIWw,1847
189
190
  fractal_server/syringe.py,sha256=3YJeIALH-wibuJ9R5VMNYUWh7x1-MkWT0SqGcWG5MY8,2795
190
191
  fractal_server/tasks/__init__.py,sha256=kadmVUoIghl8s190_Tt-8f-WBqMi8u8oU4Pvw39NHE8,23
@@ -193,28 +194,28 @@ fractal_server/tasks/v2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
193
194
  fractal_server/tasks/v2/local/__init__.py,sha256=S842wRersYKBKjc7xbmj0ov8b5i1YuCHa2f_yYuxcaI,312
194
195
  fractal_server/tasks/v2/local/_utils.py,sha256=p2KJ4BvEwJxahICpzbvzrc5-ciLCFnLXWPCwdNGi-3Q,2495
195
196
  fractal_server/tasks/v2/local/collect.py,sha256=MQncScKbWv3g9lrjF8WOhzuEoTEOOgS02RqOJno5csI,11897
196
- fractal_server/tasks/v2/local/collect_pixi.py,sha256=i24MS7yxV0_sHkZJ8rd148n8TGqCPo6Zob5LPks3odk,10753
197
+ fractal_server/tasks/v2/local/collect_pixi.py,sha256=coV9SqOf5rz2dgUFG7uVisPFS0xvXEubFwU7rb3QHe8,10753
197
198
  fractal_server/tasks/v2/local/deactivate.py,sha256=LoEs2TUoHQOq3JfxufW6zroXD-Xx_b-hLtdigEBi1JU,9732
198
199
  fractal_server/tasks/v2/local/deactivate_pixi.py,sha256=_ycvnLIZ8zUFB3fZbCzDlNudh-SSetl4UkyFrClCcUU,3494
199
200
  fractal_server/tasks/v2/local/reactivate.py,sha256=Q43DOadNeFyyfgNP67lUqaXmZsS6onv67XwxH_-5ANA,5756
200
- fractal_server/tasks/v2/local/reactivate_pixi.py,sha256=wF_3gcMWO_8ArJFo4iYh-51LDZDF_1OuYYHrY9eUSL8,7320
201
+ fractal_server/tasks/v2/local/reactivate_pixi.py,sha256=X1gdeuFGPtohjWEZ7OX2v8m6aI7Z93M-y64FqtYjApg,7320
201
202
  fractal_server/tasks/v2/ssh/__init__.py,sha256=vX5aIM9Hbn2T_cIP_LrZ5ekRqJzYm_GSfp-4Iv7kqeI,300
202
203
  fractal_server/tasks/v2/ssh/_utils.py,sha256=ktVH7psQSAhh353fVUe-BwiBZHzTdgXnR-Xv_vfuX0Y,3857
203
204
  fractal_server/tasks/v2/ssh/collect.py,sha256=M9gFD1h9Q1Z-BFQq65dI0vFs6HPCkKQzOkxaLddmChY,14334
204
- fractal_server/tasks/v2/ssh/collect_pixi.py,sha256=g-dwkVbzV_4dMXAU8Ej_HmgivHmyq7p9sSOfDXNJhR4,13621
205
+ fractal_server/tasks/v2/ssh/collect_pixi.py,sha256=MYxHY5P69P7DdM4uC8FAsAoQBuqr8UJdDti0CPHAn_U,13801
205
206
  fractal_server/tasks/v2/ssh/deactivate.py,sha256=XAIy84cLT9MSTMiN67U-wfOjxMm5s7lmrGwhW0qp7BU,12439
206
207
  fractal_server/tasks/v2/ssh/deactivate_pixi.py,sha256=K0yK_NPUqhFMj6cp6G_0Kfn0Yo7oQux4kT5dFPulnos,4748
207
208
  fractal_server/tasks/v2/ssh/reactivate.py,sha256=NJIgMNFKaXMhbvK0iZOsMwMtsms6Boj9f8N4L01X9Bo,8271
208
- fractal_server/tasks/v2/ssh/reactivate_pixi.py,sha256=QNlY0cqZvQblsl0eAbKBPBs_QAkN0G233Hy58PpWHxs,9595
209
+ fractal_server/tasks/v2/ssh/reactivate_pixi.py,sha256=Vay6kfsrc5XKx2WJVTu_pLhgpuHZDdnrEB6Er8XchYo,9784
209
210
  fractal_server/tasks/v2/templates/1_create_venv.sh,sha256=PK0jdHKtQpda1zULebBaVPORt4t6V17wa4N1ohcj5ac,548
210
211
  fractal_server/tasks/v2/templates/2_pip_install.sh,sha256=jMJPQJXHKznO6fxOOXtFXKPdCmTf1VLLWj_JL_ZdKxo,1644
211
212
  fractal_server/tasks/v2/templates/3_pip_freeze.sh,sha256=JldREScEBI4cD_qjfX4UK7V4aI-FnX9ZvVNxgpSOBFc,168
212
213
  fractal_server/tasks/v2/templates/4_pip_show.sh,sha256=qm1vPy6AkKhWDjCJGXS8LqCLYO3KsAyRK325ZsFcF6U,1747
213
214
  fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh,sha256=q-6ZUvA6w6FDVEoSd9O63LaJ9tKZc7qAFH72SGPrd_k,284
214
215
  fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh,sha256=A2y8RngEjAcRhG-_owA6P7tAdrS_AszFuGXnaeMV8u0,1122
215
- fractal_server/tasks/v2/templates/pixi_1_extract.sh,sha256=1Z6sd_fTzqQkOfbFswaPZBNLUyv-OrS4euGlcoi8ces,1097
216
- fractal_server/tasks/v2/templates/pixi_2_install.sh,sha256=BkINfTU34vZ_zCyg_CIDEWvlAME3p6kF1qmxs4UAkPw,1595
217
- fractal_server/tasks/v2/templates/pixi_3_post_install.sh,sha256=uDCdjXpBMsQcexZ4pvZn3ctJenM4QMsazsWMf5aw7eA,2500
216
+ fractal_server/tasks/v2/templates/pixi_1_extract.sh,sha256=Jdy5OyKo2jxe_qIDB9Zi4a0FL0cMBysxvBPHlUrARQM,1099
217
+ fractal_server/tasks/v2/templates/pixi_2_install.sh,sha256=h6-M101Q1AdAfZNZyPfSUc8AlZ-uS84Hae4vJdDSglY,1601
218
+ fractal_server/tasks/v2/templates/pixi_3_post_install.sh,sha256=99J8KXkNeQk9utuEtUxfAZS6VCThC32X7I7HAp2gdTU,2501
218
219
  fractal_server/tasks/v2/utils_background.py,sha256=_4wGETgZ3JdnJXLYKSI0Lns8LwokJL-NEzUOK5SxCJU,4811
219
220
  fractal_server/tasks/v2/utils_database.py,sha256=yi7793Uue32O59OBVUgomO42oUrVKdSKXoShBUNDdK0,1807
220
221
  fractal_server/tasks/v2/utils_package_names.py,sha256=RDg__xrvQs4ieeVzmVdMcEh95vGQYrv9Hfal-5EDBM8,2393
@@ -229,8 +230,8 @@ fractal_server/types/validators/_workflow_task_arguments_validators.py,sha256=HL
229
230
  fractal_server/urls.py,sha256=QjIKAC1a46bCdiPMu3AlpgFbcv6a4l3ABcd5xz190Og,471
230
231
  fractal_server/utils.py,sha256=Vn35lApt1T1J8nc09sAVqd10Cy0sa3dLipcljI-hkuk,2185
231
232
  fractal_server/zip_tools.py,sha256=tqz_8f-vQ9OBRW-4OQfO6xxY-YInHTyHmZxU7U4PqZo,4885
232
- fractal_server-2.15.0a5.dist-info/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
233
- fractal_server-2.15.0a5.dist-info/METADATA,sha256=ItBnov8ODJ4zuzUPJQXjBLs6H5SviOLZ5K6nw1Lfjmw,4245
234
- fractal_server-2.15.0a5.dist-info/WHEEL,sha256=7dDg4QLnNKTvwIDR9Ac8jJaAmBC_owJrckbC0jjThyA,88
235
- fractal_server-2.15.0a5.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
236
- fractal_server-2.15.0a5.dist-info/RECORD,,
233
+ fractal_server-2.15.2.dist-info/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
234
+ fractal_server-2.15.2.dist-info/METADATA,sha256=ey4BDGRNNq1SLK-pmXThhvEbXwbgyB2jWyTOI3ADuC4,4243
235
+ fractal_server-2.15.2.dist-info/WHEEL,sha256=7dDg4QLnNKTvwIDR9Ac8jJaAmBC_owJrckbC0jjThyA,88
236
+ fractal_server-2.15.2.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
237
+ fractal_server-2.15.2.dist-info/RECORD,,