fractal-server 2.13.0__py3-none-any.whl → 2.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +3 -1
  3. fractal_server/app/models/linkusergroup.py +6 -2
  4. fractal_server/app/models/v2/__init__.py +11 -1
  5. fractal_server/app/models/v2/accounting.py +35 -0
  6. fractal_server/app/models/v2/dataset.py +1 -11
  7. fractal_server/app/models/v2/history.py +78 -0
  8. fractal_server/app/models/v2/job.py +10 -3
  9. fractal_server/app/models/v2/task_group.py +2 -2
  10. fractal_server/app/models/v2/workflow.py +1 -1
  11. fractal_server/app/models/v2/workflowtask.py +1 -1
  12. fractal_server/app/routes/admin/v2/__init__.py +4 -0
  13. fractal_server/app/routes/admin/v2/accounting.py +98 -0
  14. fractal_server/app/routes/admin/v2/impersonate.py +35 -0
  15. fractal_server/app/routes/admin/v2/job.py +5 -13
  16. fractal_server/app/routes/admin/v2/task.py +1 -1
  17. fractal_server/app/routes/admin/v2/task_group.py +4 -29
  18. fractal_server/app/routes/api/__init__.py +1 -1
  19. fractal_server/app/routes/api/v2/__init__.py +8 -2
  20. fractal_server/app/routes/api/v2/_aux_functions.py +66 -0
  21. fractal_server/app/routes/api/v2/_aux_functions_history.py +166 -0
  22. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +3 -3
  23. fractal_server/app/routes/api/v2/dataset.py +0 -17
  24. fractal_server/app/routes/api/v2/history.py +544 -0
  25. fractal_server/app/routes/api/v2/images.py +31 -43
  26. fractal_server/app/routes/api/v2/job.py +30 -0
  27. fractal_server/app/routes/api/v2/project.py +1 -53
  28. fractal_server/app/routes/api/v2/{status.py → status_legacy.py} +6 -6
  29. fractal_server/app/routes/api/v2/submit.py +17 -14
  30. fractal_server/app/routes/api/v2/task.py +3 -10
  31. fractal_server/app/routes/api/v2/task_collection_custom.py +4 -9
  32. fractal_server/app/routes/api/v2/task_group.py +2 -22
  33. fractal_server/app/routes/api/v2/verify_image_types.py +61 -0
  34. fractal_server/app/routes/api/v2/workflow.py +28 -69
  35. fractal_server/app/routes/api/v2/workflowtask.py +53 -50
  36. fractal_server/app/routes/auth/group.py +0 -16
  37. fractal_server/app/routes/auth/oauth.py +5 -3
  38. fractal_server/app/routes/aux/__init__.py +0 -20
  39. fractal_server/app/routes/pagination.py +47 -0
  40. fractal_server/app/runner/components.py +0 -3
  41. fractal_server/app/runner/compress_folder.py +57 -29
  42. fractal_server/app/runner/exceptions.py +4 -0
  43. fractal_server/app/runner/executors/base_runner.py +157 -0
  44. fractal_server/app/runner/{v2/_local/_local_config.py → executors/local/get_local_config.py} +7 -9
  45. fractal_server/app/runner/executors/local/runner.py +248 -0
  46. fractal_server/app/runner/executors/{slurm → slurm_common}/_batching.py +1 -1
  47. fractal_server/app/runner/executors/{slurm → slurm_common}/_slurm_config.py +9 -7
  48. fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py +868 -0
  49. fractal_server/app/runner/{v2/_slurm_common → executors/slurm_common}/get_slurm_config.py +48 -17
  50. fractal_server/app/runner/executors/{slurm → slurm_common}/remote.py +36 -47
  51. fractal_server/app/runner/executors/slurm_common/slurm_job_task_models.py +134 -0
  52. fractal_server/app/runner/executors/slurm_ssh/runner.py +268 -0
  53. fractal_server/app/runner/executors/slurm_sudo/__init__.py +0 -0
  54. fractal_server/app/runner/executors/{slurm/sudo → slurm_sudo}/_subprocess_run_as_user.py +2 -83
  55. fractal_server/app/runner/executors/slurm_sudo/runner.py +193 -0
  56. fractal_server/app/runner/extract_archive.py +1 -3
  57. fractal_server/app/runner/task_files.py +134 -87
  58. fractal_server/app/runner/v2/__init__.py +0 -395
  59. fractal_server/app/runner/v2/_local.py +88 -0
  60. fractal_server/app/runner/v2/{_slurm_ssh/__init__.py → _slurm_ssh.py} +22 -19
  61. fractal_server/app/runner/v2/{_slurm_sudo/__init__.py → _slurm_sudo.py} +19 -15
  62. fractal_server/app/runner/v2/db_tools.py +119 -0
  63. fractal_server/app/runner/v2/runner.py +219 -98
  64. fractal_server/app/runner/v2/runner_functions.py +491 -189
  65. fractal_server/app/runner/v2/runner_functions_low_level.py +40 -43
  66. fractal_server/app/runner/v2/submit_workflow.py +358 -0
  67. fractal_server/app/runner/v2/task_interface.py +31 -0
  68. fractal_server/app/schemas/_validators.py +13 -24
  69. fractal_server/app/schemas/user.py +10 -7
  70. fractal_server/app/schemas/user_settings.py +9 -21
  71. fractal_server/app/schemas/v2/__init__.py +10 -1
  72. fractal_server/app/schemas/v2/accounting.py +18 -0
  73. fractal_server/app/schemas/v2/dataset.py +12 -94
  74. fractal_server/app/schemas/v2/dumps.py +26 -9
  75. fractal_server/app/schemas/v2/history.py +80 -0
  76. fractal_server/app/schemas/v2/job.py +15 -8
  77. fractal_server/app/schemas/v2/manifest.py +14 -7
  78. fractal_server/app/schemas/v2/project.py +9 -7
  79. fractal_server/app/schemas/v2/status_legacy.py +35 -0
  80. fractal_server/app/schemas/v2/task.py +72 -77
  81. fractal_server/app/schemas/v2/task_collection.py +14 -32
  82. fractal_server/app/schemas/v2/task_group.py +10 -9
  83. fractal_server/app/schemas/v2/workflow.py +10 -11
  84. fractal_server/app/schemas/v2/workflowtask.py +2 -21
  85. fractal_server/app/security/__init__.py +3 -3
  86. fractal_server/app/security/signup_email.py +2 -2
  87. fractal_server/config.py +91 -90
  88. fractal_server/images/tools.py +23 -0
  89. fractal_server/migrations/versions/47351f8c7ebc_drop_dataset_filters.py +50 -0
  90. fractal_server/migrations/versions/9db60297b8b2_set_ondelete.py +250 -0
  91. fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py +57 -0
  92. fractal_server/migrations/versions/c90a7c76e996_job_id_in_history_run.py +41 -0
  93. fractal_server/migrations/versions/e81103413827_add_job_type_filters.py +36 -0
  94. fractal_server/migrations/versions/f37aceb45062_make_historyunit_logfile_required.py +39 -0
  95. fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py +120 -0
  96. fractal_server/ssh/_fabric.py +28 -14
  97. fractal_server/tasks/v2/local/collect.py +2 -2
  98. fractal_server/tasks/v2/ssh/collect.py +2 -2
  99. fractal_server/tasks/v2/templates/2_pip_install.sh +1 -1
  100. fractal_server/tasks/v2/templates/4_pip_show.sh +1 -1
  101. fractal_server/tasks/v2/utils_background.py +1 -20
  102. fractal_server/tasks/v2/utils_database.py +30 -17
  103. fractal_server/tasks/v2/utils_templates.py +6 -0
  104. {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/METADATA +4 -4
  105. {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/RECORD +114 -99
  106. {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/WHEEL +1 -1
  107. fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +0 -126
  108. fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +0 -116
  109. fractal_server/app/runner/executors/slurm/ssh/executor.py +0 -1386
  110. fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py +0 -71
  111. fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py +0 -130
  112. fractal_server/app/runner/executors/slurm/sudo/executor.py +0 -1281
  113. fractal_server/app/runner/v2/_local/__init__.py +0 -129
  114. fractal_server/app/runner/v2/_local/_submit_setup.py +0 -52
  115. fractal_server/app/runner/v2/_local/executor.py +0 -100
  116. fractal_server/app/runner/v2/_slurm_ssh/_submit_setup.py +0 -83
  117. fractal_server/app/runner/v2/_slurm_sudo/_submit_setup.py +0 -83
  118. fractal_server/app/runner/v2/handle_failed_job.py +0 -59
  119. fractal_server/app/schemas/v2/status.py +0 -16
  120. /fractal_server/app/{runner/executors/slurm → history}/__init__.py +0 -0
  121. /fractal_server/app/runner/executors/{slurm/ssh → local}/__init__.py +0 -0
  122. /fractal_server/app/runner/executors/{slurm/sudo → slurm_common}/__init__.py +0 -0
  123. /fractal_server/app/runner/executors/{_job_states.py → slurm_common/_job_states.py} +0 -0
  124. /fractal_server/app/runner/executors/{slurm → slurm_common}/utils_executors.py +0 -0
  125. /fractal_server/app/runner/{v2/_slurm_common → executors/slurm_ssh}/__init__.py +0 -0
  126. {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/LICENSE +0 -0
  127. {fractal_server-2.13.0.dist-info → fractal_server-2.14.0.dist-info}/entry_points.txt +0 -0
@@ -417,3 +417,69 @@ async def clean_app_job_list_v2(
417
417
  if job.status == JobStatusTypeV2.SUBMITTED
418
418
  ]
419
419
  return submitted_job_ids
420
+
421
+
422
+ async def _get_dataset_or_404(
423
+ *,
424
+ dataset_id: int,
425
+ db: AsyncSession,
426
+ ) -> DatasetV2:
427
+ """
428
+ Get a dataset or raise 404.
429
+
430
+ Args:
431
+ dataset_id:
432
+ db:
433
+ """
434
+ ds = await db.get(DatasetV2, dataset_id)
435
+ if ds is None:
436
+ raise HTTPException(
437
+ status_code=status.HTTP_404_NOT_FOUND,
438
+ detail=f"Dataset {dataset_id} not found.",
439
+ )
440
+ else:
441
+ return ds
442
+
443
+
444
+ async def _get_workflow_or_404(
445
+ *,
446
+ workflow_id: int,
447
+ db: AsyncSession,
448
+ ) -> WorkflowV2:
449
+ """
450
+ Get a workflow or raise 404.
451
+
452
+ Args:
453
+ workflow_id:
454
+ db:
455
+ """
456
+ wf = await db.get(WorkflowV2, workflow_id)
457
+ if wf is None:
458
+ raise HTTPException(
459
+ status_code=status.HTTP_404_NOT_FOUND,
460
+ detail=f"Workflow {workflow_id} not found.",
461
+ )
462
+ else:
463
+ return wf
464
+
465
+
466
+ async def _get_workflowtask_or_404(
467
+ *,
468
+ workflowtask_id: int,
469
+ db: AsyncSession,
470
+ ) -> WorkflowTaskV2:
471
+ """
472
+ Get a workflow task or raise 404.
473
+
474
+ Args:
475
+ workflowtask_id:
476
+ db:
477
+ """
478
+ wftask = await db.get(WorkflowTaskV2, workflowtask_id)
479
+ if wftask is None:
480
+ raise HTTPException(
481
+ status_code=status.HTTP_404_NOT_FOUND,
482
+ detail=f"WorkflowTask {workflowtask_id} not found.",
483
+ )
484
+ else:
485
+ return wftask
@@ -0,0 +1,166 @@
1
+ from pathlib import Path
2
+ from typing import Literal
3
+
4
+ from fastapi import HTTPException
5
+ from fastapi import status
6
+
7
+ from fractal_server.app.db import AsyncSession
8
+ from fractal_server.app.models import WorkflowTaskV2
9
+ from fractal_server.app.models.v2 import DatasetV2
10
+ from fractal_server.app.models.v2 import HistoryRun
11
+ from fractal_server.app.models.v2 import HistoryUnit
12
+ from fractal_server.app.models.v2 import WorkflowV2
13
+ from fractal_server.app.routes.api.v2._aux_functions import _get_dataset_or_404
14
+ from fractal_server.app.routes.api.v2._aux_functions import (
15
+ _get_project_check_owner,
16
+ )
17
+ from fractal_server.app.routes.api.v2._aux_functions import (
18
+ _get_workflow_or_404,
19
+ )
20
+ from fractal_server.app.routes.api.v2._aux_functions import (
21
+ _get_workflowtask_or_404,
22
+ )
23
+ from fractal_server.logger import set_logger
24
+
25
+
26
+ logger = set_logger(__name__)
27
+
28
+
29
+ async def get_history_unit_or_404(
30
+ *, history_unit_id: int, db: AsyncSession
31
+ ) -> HistoryUnit:
32
+ """
33
+ Get an existing HistoryUnit or raise a 404.
34
+
35
+ Arguments:
36
+ history_unit_id: The `HistoryUnit` id
37
+ db: An asynchronous db session
38
+ """
39
+ history_unit = await db.get(HistoryUnit, history_unit_id)
40
+ if history_unit is None:
41
+ raise HTTPException(
42
+ status_code=status.HTTP_404_NOT_FOUND,
43
+ detail=f"HistoryUnit {history_unit_id} not found",
44
+ )
45
+ return history_unit
46
+
47
+
48
+ async def get_history_run_or_404(
49
+ *, history_run_id: int, db: AsyncSession
50
+ ) -> HistoryRun:
51
+ """
52
+ Get an existing HistoryRun or raise a 404.
53
+
54
+ Arguments:
55
+ history_run_id:
56
+ db:
57
+ """
58
+ history_run = await db.get(HistoryRun, history_run_id)
59
+ if history_run is None:
60
+ raise HTTPException(
61
+ status_code=status.HTTP_404_NOT_FOUND,
62
+ detail=f"HistoryRun {history_run_id} not found",
63
+ )
64
+ return history_run
65
+
66
+
67
+ def read_log_file(
68
+ *,
69
+ logfile: str | None,
70
+ wftask: WorkflowTaskV2,
71
+ dataset_id: int,
72
+ ):
73
+ if logfile is None or not Path(logfile).exists():
74
+ logger.debug(
75
+ f"Logs for task '{wftask.task.name}' in dataset "
76
+ f"{dataset_id} are not available ({logfile=})."
77
+ )
78
+ return (
79
+ f"Logs for task '{wftask.task.name}' in dataset "
80
+ f"{dataset_id} are not available."
81
+ )
82
+
83
+ try:
84
+ with open(logfile, "r") as f:
85
+ return f.read()
86
+ except Exception as e:
87
+ return (
88
+ f"Error while retrieving logs for task '{wftask.task.name}' "
89
+ f"in dataset {dataset_id}. Original error: {str(e)}."
90
+ )
91
+
92
+
93
+ async def _verify_workflow_and_dataset_access(
94
+ *,
95
+ project_id: int,
96
+ workflow_id: int,
97
+ dataset_id: int,
98
+ user_id: int,
99
+ db: AsyncSession,
100
+ ) -> dict[Literal["dataset", "workflow"], DatasetV2 | WorkflowV2]:
101
+ """
102
+ Verify user access to a dataset/workflow pair.
103
+
104
+ Args:
105
+ dataset_id:
106
+ workflow_task_id:
107
+ user_id:
108
+ db:
109
+ """
110
+ await _get_project_check_owner(
111
+ project_id=project_id,
112
+ user_id=user_id,
113
+ db=db,
114
+ )
115
+ workflow = await _get_workflow_or_404(
116
+ workflow_id=workflow_id,
117
+ db=db,
118
+ )
119
+ if workflow.project_id != project_id:
120
+ raise HTTPException(
121
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
122
+ detail="Workflow does not belong to expected project.",
123
+ )
124
+ dataset = await _get_dataset_or_404(
125
+ dataset_id=dataset_id,
126
+ db=db,
127
+ )
128
+ if dataset.project_id != project_id:
129
+ raise HTTPException(
130
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
131
+ detail="Dataset does not belong to expected project.",
132
+ )
133
+
134
+ return dict(dataset=dataset, workflow=workflow)
135
+
136
+
137
+ async def get_wftask_check_owner(
138
+ *,
139
+ project_id: int,
140
+ dataset_id: int,
141
+ workflowtask_id: int,
142
+ user_id: int,
143
+ db: AsyncSession,
144
+ ) -> WorkflowTaskV2:
145
+ """
146
+ Verify user access for the history of this dataset and workflowtask.
147
+
148
+ Args:
149
+ project_id:
150
+ dataset_id:
151
+ workflow_task_id:
152
+ user_id:
153
+ db:
154
+ """
155
+ wftask = await _get_workflowtask_or_404(
156
+ workflowtask_id=workflowtask_id,
157
+ db=db,
158
+ )
159
+ await _verify_workflow_and_dataset_access(
160
+ project_id=project_id,
161
+ dataset_id=dataset_id,
162
+ workflow_id=wftask.workflow_id,
163
+ user_id=user_id,
164
+ db=db,
165
+ )
166
+ return wftask
@@ -55,7 +55,7 @@ async def get_package_version_from_pypi(
55
55
  f"A TimeoutException occurred while getting {url}.\n"
56
56
  f"Original error: {str(e)}."
57
57
  )
58
- logger.error(error_msg)
58
+ logger.warning(error_msg)
59
59
  raise HTTPException(
60
60
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
61
61
  detail=error_msg,
@@ -65,7 +65,7 @@ async def get_package_version_from_pypi(
65
65
  f"An unknown error occurred while getting {url}. "
66
66
  f"Original error: {str(e)}."
67
67
  )
68
- logger.error(error_msg)
68
+ logger.warning(error_msg)
69
69
  raise HTTPException(
70
70
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
71
71
  detail=error_msg,
@@ -85,7 +85,7 @@ async def get_package_version_from_pypi(
85
85
  latest_version = response_data["info"]["version"]
86
86
  available_releases = response_data["releases"].keys()
87
87
  except KeyError as e:
88
- logger.error(
88
+ logger.warning(
89
89
  f"A KeyError occurred while getting {url}. "
90
90
  f"Original error: {str(e)}."
91
91
  )
@@ -47,7 +47,6 @@ async def create_dataset(
47
47
  )
48
48
 
49
49
  if dataset.zarr_dir is None:
50
-
51
50
  if user.settings.project_dir is None:
52
51
  raise HTTPException(
53
52
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -91,7 +90,6 @@ async def create_dataset(
91
90
  )
92
91
  async def read_dataset_list(
93
92
  project_id: int,
94
- history: bool = True,
95
93
  user: UserOAuth = Depends(current_active_user),
96
94
  db: AsyncSession = Depends(get_async_db),
97
95
  ) -> Optional[list[DatasetReadV2]]:
@@ -110,9 +108,6 @@ async def read_dataset_list(
110
108
  res = await db.execute(stm)
111
109
  dataset_list = res.scalars().all()
112
110
  await db.close()
113
- if not history:
114
- for ds in dataset_list:
115
- setattr(ds, "history", [])
116
111
  return dataset_list
117
112
 
118
113
 
@@ -217,14 +212,6 @@ async def delete_dataset(
217
212
  ),
218
213
  )
219
214
 
220
- # Cascade operations: set foreign-keys to null for jobs which are in
221
- # relationship with the current dataset
222
- stm = select(JobV2).where(JobV2.dataset_id == dataset_id)
223
- res = await db.execute(stm)
224
- jobs = res.scalars().all()
225
- for job in jobs:
226
- job.dataset_id = None
227
-
228
215
  # Delete dataset
229
216
  await db.delete(dataset)
230
217
  await db.commit()
@@ -234,7 +221,6 @@ async def delete_dataset(
234
221
 
235
222
  @router.get("/dataset/", response_model=list[DatasetReadV2])
236
223
  async def get_user_datasets(
237
- history: bool = True,
238
224
  user: UserOAuth = Depends(current_active_user),
239
225
  db: AsyncSession = Depends(get_async_db),
240
226
  ) -> list[DatasetReadV2]:
@@ -249,9 +235,6 @@ async def get_user_datasets(
249
235
  res = await db.execute(stm)
250
236
  dataset_list = res.scalars().all()
251
237
  await db.close()
252
- if not history:
253
- for ds in dataset_list:
254
- setattr(ds, "history", [])
255
238
  return dataset_list
256
239
 
257
240