fractal-server 2.13.1__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.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +3 -1
- fractal_server/app/models/linkusergroup.py +6 -2
- fractal_server/app/models/v2/__init__.py +7 -1
- fractal_server/app/models/v2/dataset.py +1 -11
- fractal_server/app/models/v2/history.py +78 -0
- fractal_server/app/models/v2/job.py +10 -3
- fractal_server/app/models/v2/task_group.py +2 -2
- fractal_server/app/models/v2/workflow.py +1 -1
- fractal_server/app/models/v2/workflowtask.py +1 -1
- fractal_server/app/routes/admin/v2/accounting.py +18 -28
- fractal_server/app/routes/admin/v2/task.py +1 -1
- fractal_server/app/routes/admin/v2/task_group.py +0 -17
- fractal_server/app/routes/api/__init__.py +1 -1
- fractal_server/app/routes/api/v2/__init__.py +8 -2
- fractal_server/app/routes/api/v2/_aux_functions.py +66 -0
- fractal_server/app/routes/api/v2/_aux_functions_history.py +166 -0
- fractal_server/app/routes/api/v2/dataset.py +0 -17
- fractal_server/app/routes/api/v2/history.py +544 -0
- fractal_server/app/routes/api/v2/images.py +31 -43
- fractal_server/app/routes/api/v2/job.py +30 -0
- fractal_server/app/routes/api/v2/project.py +1 -53
- fractal_server/app/routes/api/v2/{status.py → status_legacy.py} +6 -6
- fractal_server/app/routes/api/v2/submit.py +16 -14
- fractal_server/app/routes/api/v2/task.py +3 -10
- fractal_server/app/routes/api/v2/task_collection_custom.py +4 -9
- fractal_server/app/routes/api/v2/task_group.py +0 -17
- fractal_server/app/routes/api/v2/verify_image_types.py +61 -0
- fractal_server/app/routes/api/v2/workflow.py +28 -69
- fractal_server/app/routes/api/v2/workflowtask.py +53 -50
- fractal_server/app/routes/auth/group.py +0 -16
- fractal_server/app/routes/auth/oauth.py +5 -3
- fractal_server/app/routes/pagination.py +47 -0
- fractal_server/app/runner/components.py +0 -3
- fractal_server/app/runner/compress_folder.py +57 -29
- fractal_server/app/runner/exceptions.py +4 -0
- fractal_server/app/runner/executors/base_runner.py +157 -0
- fractal_server/app/runner/{v2/_local/_local_config.py → executors/local/get_local_config.py} +7 -9
- fractal_server/app/runner/executors/local/runner.py +248 -0
- fractal_server/app/runner/executors/{slurm → slurm_common}/_batching.py +1 -1
- fractal_server/app/runner/executors/{slurm → slurm_common}/_slurm_config.py +9 -7
- fractal_server/app/runner/executors/slurm_common/base_slurm_runner.py +868 -0
- fractal_server/app/runner/{v2/_slurm_common → executors/slurm_common}/get_slurm_config.py +48 -17
- fractal_server/app/runner/executors/{slurm → slurm_common}/remote.py +36 -47
- fractal_server/app/runner/executors/slurm_common/slurm_job_task_models.py +134 -0
- fractal_server/app/runner/executors/slurm_ssh/runner.py +268 -0
- fractal_server/app/runner/executors/slurm_sudo/__init__.py +0 -0
- fractal_server/app/runner/executors/{slurm/sudo → slurm_sudo}/_subprocess_run_as_user.py +2 -83
- fractal_server/app/runner/executors/slurm_sudo/runner.py +193 -0
- fractal_server/app/runner/extract_archive.py +1 -3
- fractal_server/app/runner/task_files.py +134 -87
- fractal_server/app/runner/v2/__init__.py +0 -399
- fractal_server/app/runner/v2/_local.py +88 -0
- fractal_server/app/runner/v2/{_slurm_ssh/__init__.py → _slurm_ssh.py} +20 -19
- fractal_server/app/runner/v2/{_slurm_sudo/__init__.py → _slurm_sudo.py} +17 -15
- fractal_server/app/runner/v2/db_tools.py +119 -0
- fractal_server/app/runner/v2/runner.py +206 -95
- fractal_server/app/runner/v2/runner_functions.py +488 -187
- fractal_server/app/runner/v2/runner_functions_low_level.py +40 -43
- fractal_server/app/runner/v2/submit_workflow.py +358 -0
- fractal_server/app/runner/v2/task_interface.py +31 -0
- fractal_server/app/schemas/_validators.py +13 -24
- fractal_server/app/schemas/user.py +10 -7
- fractal_server/app/schemas/user_settings.py +9 -21
- fractal_server/app/schemas/v2/__init__.py +9 -1
- fractal_server/app/schemas/v2/dataset.py +12 -94
- fractal_server/app/schemas/v2/dumps.py +26 -9
- fractal_server/app/schemas/v2/history.py +80 -0
- fractal_server/app/schemas/v2/job.py +15 -8
- fractal_server/app/schemas/v2/manifest.py +14 -7
- fractal_server/app/schemas/v2/project.py +9 -7
- fractal_server/app/schemas/v2/status_legacy.py +35 -0
- fractal_server/app/schemas/v2/task.py +72 -77
- fractal_server/app/schemas/v2/task_collection.py +14 -32
- fractal_server/app/schemas/v2/task_group.py +10 -9
- fractal_server/app/schemas/v2/workflow.py +10 -11
- fractal_server/app/schemas/v2/workflowtask.py +2 -21
- fractal_server/app/security/__init__.py +3 -3
- fractal_server/app/security/signup_email.py +2 -2
- fractal_server/config.py +41 -46
- fractal_server/images/tools.py +23 -0
- fractal_server/migrations/versions/47351f8c7ebc_drop_dataset_filters.py +50 -0
- fractal_server/migrations/versions/9db60297b8b2_set_ondelete.py +250 -0
- fractal_server/migrations/versions/c90a7c76e996_job_id_in_history_run.py +41 -0
- fractal_server/migrations/versions/e81103413827_add_job_type_filters.py +36 -0
- fractal_server/migrations/versions/f37aceb45062_make_historyunit_logfile_required.py +39 -0
- fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py +120 -0
- fractal_server/ssh/_fabric.py +28 -14
- fractal_server/tasks/v2/local/collect.py +2 -2
- fractal_server/tasks/v2/ssh/collect.py +2 -2
- fractal_server/tasks/v2/templates/2_pip_install.sh +1 -1
- fractal_server/tasks/v2/templates/4_pip_show.sh +1 -1
- fractal_server/tasks/v2/utils_background.py +0 -19
- fractal_server/tasks/v2/utils_database.py +30 -17
- fractal_server/tasks/v2/utils_templates.py +6 -0
- {fractal_server-2.13.1.dist-info → fractal_server-2.14.0.dist-info}/METADATA +4 -4
- {fractal_server-2.13.1.dist-info → fractal_server-2.14.0.dist-info}/RECORD +106 -96
- {fractal_server-2.13.1.dist-info → fractal_server-2.14.0.dist-info}/WHEEL +1 -1
- fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +0 -126
- fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +0 -116
- fractal_server/app/runner/executors/slurm/ssh/executor.py +0 -1386
- fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py +0 -71
- fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py +0 -130
- fractal_server/app/runner/executors/slurm/sudo/executor.py +0 -1281
- fractal_server/app/runner/v2/_local/__init__.py +0 -132
- fractal_server/app/runner/v2/_local/_submit_setup.py +0 -52
- fractal_server/app/runner/v2/_local/executor.py +0 -100
- fractal_server/app/runner/v2/_slurm_ssh/_submit_setup.py +0 -83
- fractal_server/app/runner/v2/_slurm_sudo/_submit_setup.py +0 -83
- fractal_server/app/runner/v2/handle_failed_job.py +0 -59
- fractal_server/app/schemas/v2/status.py +0 -16
- /fractal_server/app/{runner/executors/slurm → history}/__init__.py +0 -0
- /fractal_server/app/runner/executors/{slurm/ssh → local}/__init__.py +0 -0
- /fractal_server/app/runner/executors/{slurm/sudo → slurm_common}/__init__.py +0 -0
- /fractal_server/app/runner/executors/{_job_states.py → slurm_common/_job_states.py} +0 -0
- /fractal_server/app/runner/executors/{slurm → slurm_common}/utils_executors.py +0 -0
- /fractal_server/app/runner/{v2/_slurm_common → executors/slurm_ssh}/__init__.py +0 -0
- {fractal_server-2.13.1.dist-info → fractal_server-2.14.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.13.1.dist-info → fractal_server-2.14.0.dist-info}/entry_points.txt +0 -0
@@ -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
|
|
@@ -0,0 +1,544 @@
|
|
1
|
+
from copy import deepcopy
|
2
|
+
from typing import Any
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from fastapi import APIRouter
|
6
|
+
from fastapi import Depends
|
7
|
+
from fastapi import HTTPException
|
8
|
+
from fastapi import status
|
9
|
+
from fastapi.responses import JSONResponse
|
10
|
+
from sqlmodel import func
|
11
|
+
from sqlmodel import select
|
12
|
+
|
13
|
+
from ._aux_functions import _get_dataset_check_owner
|
14
|
+
from ._aux_functions import _get_workflow_check_owner
|
15
|
+
from ._aux_functions_history import _verify_workflow_and_dataset_access
|
16
|
+
from ._aux_functions_history import get_history_run_or_404
|
17
|
+
from ._aux_functions_history import get_history_unit_or_404
|
18
|
+
from ._aux_functions_history import get_wftask_check_owner
|
19
|
+
from ._aux_functions_history import read_log_file
|
20
|
+
from .images import ImageQuery
|
21
|
+
from fractal_server.app.db import AsyncSession
|
22
|
+
from fractal_server.app.db import get_async_db
|
23
|
+
from fractal_server.app.models import UserOAuth
|
24
|
+
from fractal_server.app.models.v2 import HistoryImageCache
|
25
|
+
from fractal_server.app.models.v2 import HistoryRun
|
26
|
+
from fractal_server.app.models.v2 import HistoryUnit
|
27
|
+
from fractal_server.app.routes.auth import current_active_user
|
28
|
+
from fractal_server.app.routes.pagination import get_pagination_params
|
29
|
+
from fractal_server.app.routes.pagination import PaginationRequest
|
30
|
+
from fractal_server.app.routes.pagination import PaginationResponse
|
31
|
+
from fractal_server.app.schemas.v2 import HistoryRunRead
|
32
|
+
from fractal_server.app.schemas.v2 import HistoryRunReadAggregated
|
33
|
+
from fractal_server.app.schemas.v2 import HistoryUnitRead
|
34
|
+
from fractal_server.app.schemas.v2 import HistoryUnitStatus
|
35
|
+
from fractal_server.app.schemas.v2 import HistoryUnitStatusQuery
|
36
|
+
from fractal_server.app.schemas.v2 import ImageLogsRequest
|
37
|
+
from fractal_server.app.schemas.v2 import SingleImageWithStatus
|
38
|
+
from fractal_server.images.tools import aggregate_attributes
|
39
|
+
from fractal_server.images.tools import aggregate_types
|
40
|
+
from fractal_server.images.tools import filter_image_list
|
41
|
+
from fractal_server.images.tools import merge_type_filters
|
42
|
+
from fractal_server.logger import set_logger
|
43
|
+
|
44
|
+
|
45
|
+
def check_historyrun_related_to_dataset_and_wftask(
|
46
|
+
history_run: HistoryRun,
|
47
|
+
dataset_id: int,
|
48
|
+
workflowtask_id: int,
|
49
|
+
):
|
50
|
+
if (
|
51
|
+
history_run.dataset_id != dataset_id
|
52
|
+
or history_run.workflowtask_id != workflowtask_id
|
53
|
+
):
|
54
|
+
raise HTTPException(
|
55
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
56
|
+
detail=(
|
57
|
+
f"Invalid query parameters: HistoryRun[{history_run.id}] is "
|
58
|
+
f"not related to {dataset_id=} and {workflowtask_id=}."
|
59
|
+
),
|
60
|
+
)
|
61
|
+
|
62
|
+
|
63
|
+
class ImageWithStatusPage(PaginationResponse[SingleImageWithStatus]):
|
64
|
+
|
65
|
+
attributes: dict[str, list[Any]]
|
66
|
+
types: list[str]
|
67
|
+
|
68
|
+
|
69
|
+
router = APIRouter()
|
70
|
+
logger = set_logger(__name__)
|
71
|
+
|
72
|
+
|
73
|
+
@router.get("/project/{project_id}/status/")
|
74
|
+
async def get_workflow_tasks_statuses(
|
75
|
+
project_id: int,
|
76
|
+
dataset_id: int,
|
77
|
+
workflow_id: int,
|
78
|
+
user: UserOAuth = Depends(current_active_user),
|
79
|
+
db: AsyncSession = Depends(get_async_db),
|
80
|
+
) -> JSONResponse:
|
81
|
+
|
82
|
+
# Access control
|
83
|
+
workflow = await _get_workflow_check_owner(
|
84
|
+
project_id=project_id,
|
85
|
+
workflow_id=workflow_id,
|
86
|
+
user_id=user.id,
|
87
|
+
db=db,
|
88
|
+
)
|
89
|
+
await _get_dataset_check_owner(
|
90
|
+
project_id=project_id,
|
91
|
+
dataset_id=dataset_id,
|
92
|
+
user_id=user.id,
|
93
|
+
db=db,
|
94
|
+
)
|
95
|
+
|
96
|
+
response: dict[int, dict[str, int | str] | None] = {}
|
97
|
+
for wftask in workflow.task_list:
|
98
|
+
res = await db.execute(
|
99
|
+
select(HistoryRun)
|
100
|
+
.where(HistoryRun.dataset_id == dataset_id)
|
101
|
+
.where(HistoryRun.workflowtask_id == wftask.id)
|
102
|
+
.order_by(HistoryRun.timestamp_started.desc())
|
103
|
+
.limit(1)
|
104
|
+
)
|
105
|
+
latest_history_run = res.scalar_one_or_none()
|
106
|
+
if latest_history_run is None:
|
107
|
+
logger.debug(
|
108
|
+
f"No HistoryRun found for {dataset_id=} and {wftask.id=}."
|
109
|
+
)
|
110
|
+
response[wftask.id] = None
|
111
|
+
continue
|
112
|
+
response[wftask.id] = dict(
|
113
|
+
status=latest_history_run.status,
|
114
|
+
num_available_images=latest_history_run.num_available_images,
|
115
|
+
)
|
116
|
+
|
117
|
+
for target_status in HistoryUnitStatus:
|
118
|
+
stm = (
|
119
|
+
select(func.count(HistoryImageCache.zarr_url))
|
120
|
+
.join(HistoryUnit)
|
121
|
+
.where(HistoryImageCache.dataset_id == dataset_id)
|
122
|
+
.where(HistoryImageCache.workflowtask_id == wftask.id)
|
123
|
+
.where(
|
124
|
+
HistoryImageCache.latest_history_unit_id == HistoryUnit.id
|
125
|
+
)
|
126
|
+
.where(HistoryUnit.status == target_status.value)
|
127
|
+
)
|
128
|
+
res = await db.execute(stm)
|
129
|
+
num_images = res.scalar()
|
130
|
+
response[wftask.id][
|
131
|
+
f"num_{target_status.value}_images"
|
132
|
+
] = num_images
|
133
|
+
|
134
|
+
new_response = deepcopy(response)
|
135
|
+
for key, value in response.items():
|
136
|
+
if value is not None:
|
137
|
+
num_total_images = sum(
|
138
|
+
value[f"num_{target_status.value}_images"]
|
139
|
+
for target_status in HistoryUnitStatus
|
140
|
+
)
|
141
|
+
if num_total_images > value["num_available_images"]:
|
142
|
+
value["num_available_images"] = None
|
143
|
+
new_response[key] = value
|
144
|
+
|
145
|
+
return JSONResponse(content=new_response, status_code=200)
|
146
|
+
|
147
|
+
|
148
|
+
@router.get("/project/{project_id}/status/run/")
|
149
|
+
async def get_history_run_list(
|
150
|
+
project_id: int,
|
151
|
+
dataset_id: int,
|
152
|
+
workflowtask_id: int,
|
153
|
+
user: UserOAuth = Depends(current_active_user),
|
154
|
+
db: AsyncSession = Depends(get_async_db),
|
155
|
+
) -> list[HistoryRunReadAggregated]:
|
156
|
+
|
157
|
+
# Access control
|
158
|
+
await get_wftask_check_owner(
|
159
|
+
project_id=project_id,
|
160
|
+
dataset_id=dataset_id,
|
161
|
+
workflowtask_id=workflowtask_id,
|
162
|
+
user_id=user.id,
|
163
|
+
db=db,
|
164
|
+
)
|
165
|
+
|
166
|
+
# Get all runs
|
167
|
+
stm = (
|
168
|
+
select(HistoryRun)
|
169
|
+
.where(HistoryRun.dataset_id == dataset_id)
|
170
|
+
.where(HistoryRun.workflowtask_id == workflowtask_id)
|
171
|
+
.order_by(HistoryRun.timestamp_started)
|
172
|
+
)
|
173
|
+
res = await db.execute(stm)
|
174
|
+
runs = res.scalars().all()
|
175
|
+
|
176
|
+
# Respond early if there are no runs
|
177
|
+
if not runs:
|
178
|
+
return []
|
179
|
+
|
180
|
+
# Add units count by status
|
181
|
+
run_ids = [run.id for run in runs]
|
182
|
+
stm = (
|
183
|
+
select(
|
184
|
+
HistoryUnit.history_run_id,
|
185
|
+
HistoryUnit.status,
|
186
|
+
func.count(HistoryUnit.id),
|
187
|
+
)
|
188
|
+
.where(HistoryUnit.history_run_id.in_(run_ids))
|
189
|
+
.group_by(HistoryUnit.history_run_id, HistoryUnit.status)
|
190
|
+
)
|
191
|
+
res = await db.execute(stm)
|
192
|
+
unit_counts = res.all()
|
193
|
+
|
194
|
+
count_map = {
|
195
|
+
run_id: {
|
196
|
+
"num_done_units": 0,
|
197
|
+
"num_submitted_units": 0,
|
198
|
+
"num_failed_units": 0,
|
199
|
+
}
|
200
|
+
for run_id in run_ids
|
201
|
+
}
|
202
|
+
for run_id, unit_status, count in unit_counts:
|
203
|
+
count_map[run_id][f"num_{unit_status}_units"] = count
|
204
|
+
|
205
|
+
runs = [dict(**run.model_dump(), **count_map[run.id]) for run in runs]
|
206
|
+
|
207
|
+
return runs
|
208
|
+
|
209
|
+
|
210
|
+
@router.get("/project/{project_id}/status/run/{history_run_id}/units/")
|
211
|
+
async def get_history_run_units(
|
212
|
+
project_id: int,
|
213
|
+
dataset_id: int,
|
214
|
+
workflowtask_id: int,
|
215
|
+
history_run_id: int,
|
216
|
+
unit_status: Optional[HistoryUnitStatus] = None,
|
217
|
+
user: UserOAuth = Depends(current_active_user),
|
218
|
+
db: AsyncSession = Depends(get_async_db),
|
219
|
+
pagination: PaginationRequest = Depends(get_pagination_params),
|
220
|
+
) -> PaginationResponse[HistoryUnitRead]:
|
221
|
+
|
222
|
+
# Access control
|
223
|
+
await get_wftask_check_owner(
|
224
|
+
project_id=project_id,
|
225
|
+
dataset_id=dataset_id,
|
226
|
+
workflowtask_id=workflowtask_id,
|
227
|
+
user_id=user.id,
|
228
|
+
db=db,
|
229
|
+
)
|
230
|
+
|
231
|
+
# Check that `HistoryRun` exists
|
232
|
+
history_run = await get_history_run_or_404(
|
233
|
+
history_run_id=history_run_id, db=db
|
234
|
+
)
|
235
|
+
check_historyrun_related_to_dataset_and_wftask(
|
236
|
+
history_run=history_run,
|
237
|
+
dataset_id=dataset_id,
|
238
|
+
workflowtask_id=workflowtask_id,
|
239
|
+
)
|
240
|
+
|
241
|
+
# Count `HistoryUnit`s
|
242
|
+
stmt = select(func.count(HistoryUnit.id)).where(
|
243
|
+
HistoryUnit.history_run_id == history_run_id
|
244
|
+
)
|
245
|
+
if unit_status:
|
246
|
+
stmt = stmt.where(HistoryUnit.status == unit_status)
|
247
|
+
res = await db.execute(stmt)
|
248
|
+
total_count = res.scalar()
|
249
|
+
page_size = pagination.page_size or total_count
|
250
|
+
|
251
|
+
# Query `HistoryUnit`s
|
252
|
+
stmt = (
|
253
|
+
select(HistoryUnit)
|
254
|
+
.where(HistoryUnit.history_run_id == history_run_id)
|
255
|
+
.order_by(HistoryUnit.id)
|
256
|
+
)
|
257
|
+
if unit_status:
|
258
|
+
stmt = stmt.where(HistoryUnit.status == unit_status)
|
259
|
+
stmt = stmt.offset((pagination.page - 1) * page_size).limit(page_size)
|
260
|
+
res = await db.execute(stmt)
|
261
|
+
units = res.scalars().all()
|
262
|
+
|
263
|
+
return dict(
|
264
|
+
current_page=pagination.page,
|
265
|
+
page_size=page_size,
|
266
|
+
total_count=total_count,
|
267
|
+
items=units,
|
268
|
+
)
|
269
|
+
|
270
|
+
|
271
|
+
@router.post("/project/{project_id}/status/images/")
|
272
|
+
async def get_history_images(
|
273
|
+
project_id: int,
|
274
|
+
dataset_id: int,
|
275
|
+
workflowtask_id: int,
|
276
|
+
request_body: ImageQuery,
|
277
|
+
unit_status: Optional[HistoryUnitStatusQuery] = None,
|
278
|
+
user: UserOAuth = Depends(current_active_user),
|
279
|
+
db: AsyncSession = Depends(get_async_db),
|
280
|
+
pagination: PaginationRequest = Depends(get_pagination_params),
|
281
|
+
) -> ImageWithStatusPage:
|
282
|
+
|
283
|
+
# Access control and object retrieval
|
284
|
+
wftask = await get_wftask_check_owner(
|
285
|
+
project_id=project_id,
|
286
|
+
dataset_id=dataset_id,
|
287
|
+
workflowtask_id=workflowtask_id,
|
288
|
+
user_id=user.id,
|
289
|
+
db=db,
|
290
|
+
)
|
291
|
+
res = await _verify_workflow_and_dataset_access(
|
292
|
+
project_id=project_id,
|
293
|
+
workflow_id=wftask.workflow_id,
|
294
|
+
dataset_id=dataset_id,
|
295
|
+
user_id=user.id,
|
296
|
+
db=db,
|
297
|
+
)
|
298
|
+
dataset = res["dataset"]
|
299
|
+
workflow = res["workflow"]
|
300
|
+
|
301
|
+
# Setup prefix for logging
|
302
|
+
prefix = f"[DS{dataset.id}-WFT{wftask.id}-images]"
|
303
|
+
|
304
|
+
# (1) Get the type-filtered list of dataset images
|
305
|
+
|
306
|
+
# (1A) Reconstruct dataset type filters by starting from {} and making
|
307
|
+
# incremental updates with `output_types` of all previous tasks
|
308
|
+
inferred_dataset_type_filters = {}
|
309
|
+
for current_wftask in workflow.task_list[0 : wftask.order]:
|
310
|
+
inferred_dataset_type_filters.update(current_wftask.task.output_types)
|
311
|
+
logger.debug(f"{prefix} {inferred_dataset_type_filters=}")
|
312
|
+
# (1B) Compute type filters for the current wftask
|
313
|
+
type_filters_patch = merge_type_filters(
|
314
|
+
task_input_types=wftask.task.input_types,
|
315
|
+
wftask_type_filters=wftask.type_filters,
|
316
|
+
)
|
317
|
+
logger.debug(f"{prefix} {type_filters_patch=}")
|
318
|
+
# (1C) Combine dataset type filters (lower priority) and current-wftask
|
319
|
+
# filters (higher priority)
|
320
|
+
actual_filters = inferred_dataset_type_filters
|
321
|
+
actual_filters.update(type_filters_patch)
|
322
|
+
logger.debug(f"{prefix} {actual_filters=}")
|
323
|
+
# (1D) Get all matching images from the dataset
|
324
|
+
pre_filtered_dataset_images = filter_image_list(
|
325
|
+
images=dataset.images,
|
326
|
+
type_filters=inferred_dataset_type_filters,
|
327
|
+
)
|
328
|
+
filtered_dataset_images = filter_image_list(
|
329
|
+
pre_filtered_dataset_images,
|
330
|
+
type_filters=request_body.type_filters,
|
331
|
+
attribute_filters=request_body.attribute_filters,
|
332
|
+
)
|
333
|
+
logger.debug(f"{prefix} {len(dataset.images)=}")
|
334
|
+
logger.debug(f"{prefix} {len(filtered_dataset_images)=}")
|
335
|
+
# (1E) Extract the list of URLs for filtered images
|
336
|
+
filtered_dataset_images_url = list(
|
337
|
+
img["zarr_url"] for img in filtered_dataset_images
|
338
|
+
)
|
339
|
+
|
340
|
+
# (2) Get `(zarr_url, status)` pairs for all images that have already
|
341
|
+
# been processed, and
|
342
|
+
# (3) When relevant, find images that have not been processed
|
343
|
+
base_stmt = (
|
344
|
+
select(HistoryImageCache.zarr_url, HistoryUnit.status)
|
345
|
+
.join(HistoryUnit)
|
346
|
+
.where(HistoryImageCache.dataset_id == dataset_id)
|
347
|
+
.where(HistoryImageCache.workflowtask_id == workflowtask_id)
|
348
|
+
.where(HistoryImageCache.latest_history_unit_id == HistoryUnit.id)
|
349
|
+
.where(HistoryImageCache.zarr_url.in_(filtered_dataset_images_url))
|
350
|
+
)
|
351
|
+
|
352
|
+
if unit_status in [HistoryUnitStatusQuery.UNSET, None]:
|
353
|
+
stmt = base_stmt.order_by(HistoryImageCache.zarr_url)
|
354
|
+
res = await db.execute(stmt)
|
355
|
+
list_processed_url_status = res.all()
|
356
|
+
list_processed_url = list(
|
357
|
+
item[0] for item in list_processed_url_status
|
358
|
+
)
|
359
|
+
list_non_processed_url_status = list(
|
360
|
+
(url, None)
|
361
|
+
for url in filtered_dataset_images_url
|
362
|
+
if url not in list_processed_url
|
363
|
+
)
|
364
|
+
if unit_status == HistoryUnitStatusQuery.UNSET:
|
365
|
+
list_processed_url_status = []
|
366
|
+
else:
|
367
|
+
stmt = base_stmt.where(HistoryUnit.status == unit_status).order_by(
|
368
|
+
HistoryImageCache.zarr_url
|
369
|
+
)
|
370
|
+
res = await db.execute(stmt)
|
371
|
+
list_processed_url_status = res.all()
|
372
|
+
list_non_processed_url_status = []
|
373
|
+
|
374
|
+
logger.debug(f"{prefix} {len(list_processed_url_status)=}")
|
375
|
+
logger.debug(f"{prefix} {len(list_non_processed_url_status)=}")
|
376
|
+
|
377
|
+
# (3) Combine outputs from 1 and 2
|
378
|
+
full_list_url_status = (
|
379
|
+
list_processed_url_status + list_non_processed_url_status
|
380
|
+
)
|
381
|
+
logger.debug(f"{prefix} {len(full_list_url_status)=}")
|
382
|
+
|
383
|
+
attributes = aggregate_attributes(pre_filtered_dataset_images)
|
384
|
+
types = aggregate_types(pre_filtered_dataset_images)
|
385
|
+
|
386
|
+
sorted_list_url_status = sorted(
|
387
|
+
full_list_url_status,
|
388
|
+
key=lambda url_status: url_status[0],
|
389
|
+
)
|
390
|
+
logger.debug(f"{prefix} {len(sorted_list_url_status)=}")
|
391
|
+
|
392
|
+
# Final list of objects
|
393
|
+
|
394
|
+
total_count = len(sorted_list_url_status)
|
395
|
+
page_size = pagination.page_size or total_count
|
396
|
+
|
397
|
+
paginated_list_url_status = sorted_list_url_status[
|
398
|
+
(pagination.page - 1) * page_size : pagination.page * page_size
|
399
|
+
]
|
400
|
+
|
401
|
+
# Aggregate information to create 'SingleImageWithStatus'
|
402
|
+
items = [
|
403
|
+
{
|
404
|
+
**filtered_dataset_images[
|
405
|
+
filtered_dataset_images_url.index(url_status[0])
|
406
|
+
],
|
407
|
+
"status": url_status[1],
|
408
|
+
}
|
409
|
+
for url_status in paginated_list_url_status
|
410
|
+
]
|
411
|
+
|
412
|
+
return dict(
|
413
|
+
current_page=pagination.page,
|
414
|
+
page_size=page_size,
|
415
|
+
total_count=total_count,
|
416
|
+
items=items,
|
417
|
+
attributes=attributes,
|
418
|
+
types=types,
|
419
|
+
)
|
420
|
+
|
421
|
+
|
422
|
+
@router.post("/project/{project_id}/status/image-log/")
|
423
|
+
async def get_image_log(
|
424
|
+
project_id: int,
|
425
|
+
request_data: ImageLogsRequest,
|
426
|
+
user: UserOAuth = Depends(current_active_user),
|
427
|
+
db: AsyncSession = Depends(get_async_db),
|
428
|
+
) -> JSONResponse:
|
429
|
+
# Access control
|
430
|
+
wftask = await get_wftask_check_owner(
|
431
|
+
project_id=project_id,
|
432
|
+
dataset_id=request_data.dataset_id,
|
433
|
+
workflowtask_id=request_data.workflowtask_id,
|
434
|
+
user_id=user.id,
|
435
|
+
db=db,
|
436
|
+
)
|
437
|
+
|
438
|
+
# Get HistoryImageCache
|
439
|
+
history_image_cache = await db.get(
|
440
|
+
HistoryImageCache,
|
441
|
+
(
|
442
|
+
request_data.zarr_url,
|
443
|
+
request_data.dataset_id,
|
444
|
+
request_data.workflowtask_id,
|
445
|
+
),
|
446
|
+
)
|
447
|
+
if history_image_cache is None:
|
448
|
+
raise HTTPException(
|
449
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
450
|
+
detail="HistoryImageCache not found",
|
451
|
+
)
|
452
|
+
# Get history unit
|
453
|
+
history_unit = await get_history_unit_or_404(
|
454
|
+
history_unit_id=history_image_cache.latest_history_unit_id,
|
455
|
+
db=db,
|
456
|
+
)
|
457
|
+
|
458
|
+
# Get log or placeholder text
|
459
|
+
log = read_log_file(
|
460
|
+
logfile=history_unit.logfile,
|
461
|
+
wftask=wftask,
|
462
|
+
dataset_id=request_data.dataset_id,
|
463
|
+
)
|
464
|
+
return JSONResponse(content=log)
|
465
|
+
|
466
|
+
|
467
|
+
@router.get("/project/{project_id}/status/unit-log/")
|
468
|
+
async def get_history_unit_log(
|
469
|
+
project_id: int,
|
470
|
+
history_run_id: int,
|
471
|
+
history_unit_id: int,
|
472
|
+
workflowtask_id: int,
|
473
|
+
dataset_id: int,
|
474
|
+
user: UserOAuth = Depends(current_active_user),
|
475
|
+
db: AsyncSession = Depends(get_async_db),
|
476
|
+
) -> JSONResponse:
|
477
|
+
# Access control
|
478
|
+
wftask = await get_wftask_check_owner(
|
479
|
+
project_id=project_id,
|
480
|
+
dataset_id=dataset_id,
|
481
|
+
workflowtask_id=workflowtask_id,
|
482
|
+
user_id=user.id,
|
483
|
+
db=db,
|
484
|
+
)
|
485
|
+
|
486
|
+
# Get history unit
|
487
|
+
history_unit = await get_history_unit_or_404(
|
488
|
+
history_unit_id=history_unit_id,
|
489
|
+
db=db,
|
490
|
+
)
|
491
|
+
|
492
|
+
if history_unit.history_run_id != history_run_id:
|
493
|
+
raise HTTPException(
|
494
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
495
|
+
detail=(
|
496
|
+
f"Invalid query parameters: HistoryUnit[{history_unit_id}] "
|
497
|
+
f"is not related to HistoryRun[{history_run_id}]"
|
498
|
+
),
|
499
|
+
)
|
500
|
+
history_run = await get_history_run_or_404(
|
501
|
+
history_run_id=history_run_id, db=db
|
502
|
+
)
|
503
|
+
check_historyrun_related_to_dataset_and_wftask(
|
504
|
+
history_run=history_run,
|
505
|
+
dataset_id=dataset_id,
|
506
|
+
workflowtask_id=workflowtask_id,
|
507
|
+
)
|
508
|
+
|
509
|
+
# Get log or placeholder text
|
510
|
+
log = read_log_file(
|
511
|
+
logfile=history_unit.logfile,
|
512
|
+
wftask=wftask,
|
513
|
+
dataset_id=dataset_id,
|
514
|
+
)
|
515
|
+
return JSONResponse(content=log)
|
516
|
+
|
517
|
+
|
518
|
+
@router.get("/project/{project_id}/dataset/{dataset_id}/history/")
|
519
|
+
async def get_dataset_history(
|
520
|
+
project_id: int,
|
521
|
+
dataset_id: int,
|
522
|
+
user: UserOAuth = Depends(current_active_user),
|
523
|
+
db: AsyncSession = Depends(get_async_db),
|
524
|
+
) -> list[HistoryRunRead]:
|
525
|
+
"""
|
526
|
+
Returns a list of all HistoryRuns associated to a given dataset, sorted by
|
527
|
+
timestamp.
|
528
|
+
"""
|
529
|
+
# Access control
|
530
|
+
await _get_dataset_check_owner(
|
531
|
+
project_id=project_id,
|
532
|
+
dataset_id=dataset_id,
|
533
|
+
user_id=user.id,
|
534
|
+
db=db,
|
535
|
+
)
|
536
|
+
|
537
|
+
res = await db.execute(
|
538
|
+
select(HistoryRun)
|
539
|
+
.where(HistoryRun.dataset_id == dataset_id)
|
540
|
+
.order_by(HistoryRun.timestamp_started)
|
541
|
+
)
|
542
|
+
history_run_list = res.scalars().all()
|
543
|
+
|
544
|
+
return history_run_list
|