fractal-server 2.6.3__py3-none-any.whl → 2.7.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 +1 -1
- fractal_server/app/models/linkusergroup.py +11 -0
- fractal_server/app/models/v2/__init__.py +2 -0
- fractal_server/app/models/v2/collection_state.py +1 -0
- fractal_server/app/models/v2/task.py +67 -2
- fractal_server/app/routes/admin/v2/__init__.py +16 -0
- fractal_server/app/routes/admin/{v2.py → v2/job.py} +20 -191
- fractal_server/app/routes/admin/v2/project.py +43 -0
- fractal_server/app/routes/admin/v2/task.py +133 -0
- fractal_server/app/routes/admin/v2/task_group.py +162 -0
- fractal_server/app/routes/api/v1/task_collection.py +4 -4
- fractal_server/app/routes/api/v2/__init__.py +8 -0
- fractal_server/app/routes/api/v2/_aux_functions.py +1 -68
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +343 -0
- fractal_server/app/routes/api/v2/submit.py +16 -35
- fractal_server/app/routes/api/v2/task.py +85 -110
- fractal_server/app/routes/api/v2/task_collection.py +184 -196
- fractal_server/app/routes/api/v2/task_collection_custom.py +70 -64
- fractal_server/app/routes/api/v2/task_group.py +173 -0
- fractal_server/app/routes/api/v2/workflow.py +39 -102
- fractal_server/app/routes/api/v2/workflow_import.py +360 -0
- fractal_server/app/routes/api/v2/workflowtask.py +4 -8
- fractal_server/app/routes/auth/_aux_auth.py +86 -40
- fractal_server/app/routes/auth/current_user.py +5 -5
- fractal_server/app/routes/auth/group.py +73 -23
- fractal_server/app/routes/auth/router.py +0 -2
- fractal_server/app/routes/auth/users.py +8 -7
- fractal_server/app/runner/executors/slurm/ssh/executor.py +82 -63
- fractal_server/app/runner/v2/__init__.py +13 -7
- fractal_server/app/runner/v2/task_interface.py +4 -9
- fractal_server/app/schemas/user.py +1 -2
- fractal_server/app/schemas/v2/__init__.py +7 -0
- fractal_server/app/schemas/v2/dataset.py +2 -7
- fractal_server/app/schemas/v2/dumps.py +1 -2
- fractal_server/app/schemas/v2/job.py +1 -1
- fractal_server/app/schemas/v2/manifest.py +25 -1
- fractal_server/app/schemas/v2/project.py +1 -1
- fractal_server/app/schemas/v2/task.py +95 -36
- fractal_server/app/schemas/v2/task_collection.py +8 -6
- fractal_server/app/schemas/v2/task_group.py +85 -0
- fractal_server/app/schemas/v2/workflow.py +7 -2
- fractal_server/app/schemas/v2/workflowtask.py +9 -6
- fractal_server/app/security/__init__.py +8 -1
- fractal_server/config.py +8 -28
- fractal_server/data_migrations/2_7_0.py +323 -0
- fractal_server/images/models.py +2 -4
- fractal_server/main.py +1 -1
- fractal_server/migrations/env.py +4 -1
- fractal_server/migrations/versions/034a469ec2eb_task_groups.py +184 -0
- fractal_server/ssh/_fabric.py +186 -73
- fractal_server/string_tools.py +6 -2
- fractal_server/tasks/utils.py +19 -5
- fractal_server/tasks/v1/_TaskCollectPip.py +1 -1
- fractal_server/tasks/v1/background_operations.py +5 -5
- fractal_server/tasks/v1/get_collection_data.py +2 -2
- fractal_server/tasks/v2/_venv_pip.py +67 -70
- fractal_server/tasks/v2/background_operations.py +180 -69
- fractal_server/tasks/v2/background_operations_ssh.py +57 -70
- fractal_server/tasks/v2/database_operations.py +44 -0
- fractal_server/tasks/v2/endpoint_operations.py +104 -116
- fractal_server/tasks/v2/templates/_1_create_venv.sh +9 -5
- fractal_server/tasks/v2/templates/{_2_upgrade_pip.sh → _2_preliminary_pip_operations.sh} +1 -0
- fractal_server/tasks/v2/utils.py +5 -0
- fractal_server/utils.py +3 -2
- {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/METADATA +3 -7
- {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/RECORD +70 -61
- fractal_server/app/routes/auth/group_names.py +0 -34
- fractal_server/tasks/v2/_TaskCollectPip.py +0 -132
- {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.6.3.dist-info → fractal_server-2.7.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,5 @@
|
|
1
|
-
import json
|
2
1
|
from pathlib import Path
|
3
|
-
from
|
4
|
-
from tempfile import TemporaryDirectory
|
2
|
+
from typing import Optional
|
5
3
|
|
6
4
|
from fastapi import APIRouter
|
7
5
|
from fastapi import BackgroundTasks
|
@@ -10,7 +8,7 @@ from fastapi import HTTPException
|
|
10
8
|
from fastapi import Request
|
11
9
|
from fastapi import Response
|
12
10
|
from fastapi import status
|
13
|
-
from pydantic
|
11
|
+
from pydantic import ValidationError
|
14
12
|
from sqlmodel import select
|
15
13
|
|
16
14
|
from .....config import get_settings
|
@@ -20,26 +18,28 @@ from .....syringe import Inject
|
|
20
18
|
from ....db import AsyncSession
|
21
19
|
from ....db import get_async_db
|
22
20
|
from ....models.v2 import CollectionStateV2
|
23
|
-
from ....models.v2 import
|
21
|
+
from ....models.v2 import TaskGroupV2
|
24
22
|
from ....schemas.v2 import CollectionStateReadV2
|
25
23
|
from ....schemas.v2 import CollectionStatusV2
|
26
24
|
from ....schemas.v2 import TaskCollectPipV2
|
27
|
-
from ....schemas.v2 import
|
25
|
+
from ....schemas.v2 import TaskGroupCreateV2
|
28
26
|
from ...aux.validate_user_settings import validate_user_settings
|
27
|
+
from ._aux_functions_tasks import _get_valid_user_group_id
|
28
|
+
from ._aux_functions_tasks import _verify_non_duplication_group_constraint
|
29
|
+
from ._aux_functions_tasks import _verify_non_duplication_user_constraint
|
29
30
|
from fractal_server.app.models import UserOAuth
|
30
31
|
from fractal_server.app.routes.auth import current_active_user
|
31
32
|
from fractal_server.app.routes.auth import current_active_verified_user
|
32
|
-
from fractal_server.
|
33
|
-
from fractal_server.tasks.utils import
|
34
|
-
from fractal_server.tasks.utils import
|
35
|
-
from fractal_server.tasks.utils import get_collection_path
|
36
|
-
from fractal_server.tasks.v2._TaskCollectPip import _TaskCollectPip
|
33
|
+
from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
|
34
|
+
from fractal_server.tasks.utils import _normalize_package_name
|
35
|
+
from fractal_server.tasks.utils import get_collection_log_v2
|
37
36
|
from fractal_server.tasks.v2.background_operations import (
|
38
37
|
background_collect_pip,
|
39
38
|
)
|
40
|
-
from fractal_server.tasks.v2.endpoint_operations import
|
41
|
-
|
42
|
-
|
39
|
+
from fractal_server.tasks.v2.endpoint_operations import (
|
40
|
+
get_package_version_from_pypi,
|
41
|
+
)
|
42
|
+
from fractal_server.tasks.v2.utils import _parse_wheel_filename
|
43
43
|
from fractal_server.tasks.v2.utils import get_python_interpreter_v2
|
44
44
|
|
45
45
|
router = APIRouter()
|
@@ -50,25 +50,14 @@ logger = set_logger(__name__)
|
|
50
50
|
@router.post(
|
51
51
|
"/collect/pip/",
|
52
52
|
response_model=CollectionStateReadV2,
|
53
|
-
responses={
|
54
|
-
201: dict(
|
55
|
-
description=(
|
56
|
-
"Task collection successfully started in the background"
|
57
|
-
)
|
58
|
-
),
|
59
|
-
200: dict(
|
60
|
-
description=(
|
61
|
-
"Package already collected. Returning info on already "
|
62
|
-
"available tasks"
|
63
|
-
)
|
64
|
-
),
|
65
|
-
},
|
66
53
|
)
|
67
54
|
async def collect_tasks_pip(
|
68
55
|
task_collect: TaskCollectPipV2,
|
69
56
|
background_tasks: BackgroundTasks,
|
70
57
|
response: Response,
|
71
58
|
request: Request,
|
59
|
+
private: bool = False,
|
60
|
+
user_group_id: Optional[int] = None,
|
72
61
|
user: UserOAuth = Depends(current_active_verified_user),
|
73
62
|
db: AsyncSession = Depends(get_async_db),
|
74
63
|
) -> CollectionStateReadV2:
|
@@ -82,209 +71,212 @@ async def collect_tasks_pip(
|
|
82
71
|
# Get settings
|
83
72
|
settings = Inject(get_settings)
|
84
73
|
|
74
|
+
# Initialize task-group attributes
|
75
|
+
task_group_attrs = dict(user_id=user.id)
|
76
|
+
|
85
77
|
# Set/check python version
|
86
78
|
if task_collect.python_version is None:
|
87
|
-
|
88
|
-
|
89
|
-
|
79
|
+
task_group_attrs[
|
80
|
+
"python_version"
|
81
|
+
] = settings.FRACTAL_TASKS_PYTHON_DEFAULT_VERSION
|
82
|
+
else:
|
83
|
+
task_group_attrs["python_version"] = task_collect.python_version
|
90
84
|
try:
|
91
|
-
get_python_interpreter_v2(
|
85
|
+
get_python_interpreter_v2(
|
86
|
+
python_version=task_group_attrs["python_version"]
|
87
|
+
)
|
92
88
|
except ValueError:
|
93
89
|
raise HTTPException(
|
94
90
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
95
91
|
detail=(
|
96
|
-
f"Python version {
|
92
|
+
f"Python version {task_group_attrs['python_version']} is "
|
97
93
|
"not available for Fractal task collection."
|
98
94
|
),
|
99
95
|
)
|
100
96
|
|
101
|
-
#
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
97
|
+
# Set pip_extras
|
98
|
+
if task_collect.package_extras is not None:
|
99
|
+
task_group_attrs["pip_extras"] = task_collect.package_extras
|
100
|
+
|
101
|
+
# Set pinned_package_versions
|
102
|
+
if task_collect.pinned_package_versions is not None:
|
103
|
+
task_group_attrs[
|
104
|
+
"pinned_package_versions"
|
105
|
+
] = task_collect.pinned_package_versions
|
106
|
+
|
107
|
+
# Set pkg_name, version, origin and wheel_path
|
108
|
+
if task_collect.package.endswith(".whl"):
|
109
|
+
try:
|
110
|
+
task_group_attrs["wheel_path"] = task_collect.package
|
111
|
+
wheel_filename = Path(task_group_attrs["wheel_path"]).name
|
112
|
+
wheel_info = _parse_wheel_filename(wheel_filename)
|
113
|
+
except ValueError as e:
|
114
|
+
raise HTTPException(
|
115
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
116
|
+
detail=(
|
117
|
+
f"Invalid wheel-file name {wheel_filename}. "
|
118
|
+
f"Original error: {str(e)}",
|
119
|
+
),
|
120
|
+
)
|
121
|
+
task_group_attrs["pkg_name"] = _normalize_package_name(
|
122
|
+
wheel_info["distribution"]
|
108
123
|
)
|
124
|
+
task_group_attrs["version"] = wheel_info["version"]
|
125
|
+
task_group_attrs["origin"] = TaskGroupV2OriginEnum.WHEELFILE
|
126
|
+
else:
|
127
|
+
pkg_name = task_collect.package
|
128
|
+
task_group_attrs["pkg_name"] = _normalize_package_name(pkg_name)
|
129
|
+
task_group_attrs["origin"] = TaskGroupV2OriginEnum.PYPI
|
130
|
+
latest_version = await get_package_version_from_pypi(
|
131
|
+
task_collect.package,
|
132
|
+
task_collect.package_version,
|
133
|
+
)
|
134
|
+
task_group_attrs["version"] = latest_version
|
135
|
+
|
136
|
+
# Validate query parameters related to user-group ownership
|
137
|
+
user_group_id = await _get_valid_user_group_id(
|
138
|
+
user_group_id=user_group_id,
|
139
|
+
private=private,
|
140
|
+
user_id=user.id,
|
141
|
+
db=db,
|
142
|
+
)
|
143
|
+
|
144
|
+
# Set user_group_id
|
145
|
+
task_group_attrs["user_group_id"] = user_group_id
|
109
146
|
|
110
147
|
# Validate user settings (backend-specific)
|
111
148
|
user_settings = await validate_user_settings(
|
112
149
|
user=user, backend=settings.FRACTAL_RUNNER_BACKEND, db=db
|
113
150
|
)
|
114
151
|
|
115
|
-
#
|
116
|
-
|
152
|
+
# Set path and venv_path
|
117
153
|
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
118
|
-
|
119
|
-
|
120
|
-
|
154
|
+
base_tasks_path = user_settings.ssh_tasks_dir
|
155
|
+
else:
|
156
|
+
base_tasks_path = settings.FRACTAL_TASKS_DIR.as_posix()
|
157
|
+
task_group_path = (
|
158
|
+
Path(base_tasks_path)
|
159
|
+
/ str(user.id)
|
160
|
+
/ task_group_attrs["pkg_name"]
|
161
|
+
/ task_group_attrs["version"]
|
162
|
+
).as_posix()
|
163
|
+
task_group_attrs["path"] = task_group_path
|
164
|
+
task_group_attrs["venv_path"] = Path(task_group_path, "venv").as_posix()
|
165
|
+
|
166
|
+
# Validate TaskGroupV2 attributes
|
167
|
+
try:
|
168
|
+
TaskGroupCreateV2(**task_group_attrs)
|
169
|
+
except ValidationError as e:
|
170
|
+
raise HTTPException(
|
171
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
172
|
+
detail=f"Invalid task-group object. Original error: {e}",
|
121
173
|
)
|
122
174
|
|
123
|
-
|
124
|
-
state = CollectionStateV2(
|
125
|
-
data=dict(
|
126
|
-
status=CollectionStatusV2.PENDING, package=task_collect.package
|
127
|
-
)
|
128
|
-
)
|
129
|
-
db.add(state)
|
130
|
-
await db.commit()
|
175
|
+
# Database checks
|
131
176
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
177
|
+
# Verify non-duplication constraints
|
178
|
+
await _verify_non_duplication_user_constraint(
|
179
|
+
user_id=user.id,
|
180
|
+
pkg_name=task_group_attrs["pkg_name"],
|
181
|
+
version=task_group_attrs["version"],
|
182
|
+
db=db,
|
183
|
+
)
|
184
|
+
await _verify_non_duplication_group_constraint(
|
185
|
+
user_group_id=task_group_attrs["user_group_id"],
|
186
|
+
pkg_name=task_group_attrs["pkg_name"],
|
187
|
+
version=task_group_attrs["version"],
|
188
|
+
db=db,
|
189
|
+
)
|
140
190
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
191
|
+
# Verify that task-group path is unique
|
192
|
+
stm = select(TaskGroupV2).where(TaskGroupV2.path == task_group_path)
|
193
|
+
res = await db.execute(stm)
|
194
|
+
for conflicting_task_group in res.scalars().all():
|
195
|
+
raise HTTPException(
|
196
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
197
|
+
detail=(
|
198
|
+
f"Another task-group already has path={task_group_path}.\n"
|
199
|
+
f"{conflicting_task_group=}"
|
200
|
+
),
|
147
201
|
)
|
148
202
|
|
149
|
-
|
150
|
-
return state
|
151
|
-
|
152
|
-
# Actual non-SSH endpoint
|
203
|
+
# On-disk checks
|
153
204
|
|
154
|
-
|
205
|
+
if settings.FRACTAL_RUNNER_BACKEND != "slurm_ssh":
|
155
206
|
|
156
|
-
|
157
|
-
|
158
|
-
# Copy or download the package wheel file to tmpdir
|
159
|
-
if task_pkg.is_local_package:
|
160
|
-
shell_copy(task_pkg.package_path.as_posix(), tmpdir)
|
161
|
-
wheel_path = Path(tmpdir) / task_pkg.package_path.name
|
162
|
-
else:
|
163
|
-
logger.info(f"Now download {task_pkg}")
|
164
|
-
wheel_path = await download_package(
|
165
|
-
task_pkg=task_pkg, dest=tmpdir
|
166
|
-
)
|
167
|
-
# Read package info from wheel file, and override the ones coming
|
168
|
-
# from the request body. Note that `package_name` was already set
|
169
|
-
# (and normalized) as part of `_TaskCollectPip` initialization.
|
170
|
-
pkg_info = inspect_package(wheel_path)
|
171
|
-
task_pkg.package_version = pkg_info["pkg_version"]
|
172
|
-
task_pkg.package_manifest = pkg_info["pkg_manifest"]
|
173
|
-
except Exception as e:
|
207
|
+
# Verify that folder does not exist (for local collection)
|
208
|
+
if Path(task_group_path).exists():
|
174
209
|
raise HTTPException(
|
175
210
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
176
|
-
detail=f"
|
211
|
+
detail=f"{task_group_path} already exists.",
|
177
212
|
)
|
178
213
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
try:
|
184
|
-
package_path = get_absolute_venv_path(venv_path)
|
185
|
-
collection_path = get_collection_path(package_path)
|
186
|
-
with collection_path.open("r") as f:
|
187
|
-
task_collect_data = json.load(f)
|
188
|
-
|
189
|
-
err_msg = (
|
190
|
-
"Cannot collect package, possible reason: an old version of "
|
191
|
-
"the same package has already been collected.\n"
|
192
|
-
f"{str(collection_path)} has invalid content: "
|
193
|
-
)
|
194
|
-
if not isinstance(task_collect_data, dict):
|
195
|
-
raise HTTPException(
|
196
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
197
|
-
detail=f"{err_msg} it's not a Python dictionary.",
|
198
|
-
)
|
199
|
-
if "task_list" not in task_collect_data.keys():
|
200
|
-
raise HTTPException(
|
201
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
202
|
-
detail=f"{err_msg} it has no key 'task_list'.",
|
203
|
-
)
|
204
|
-
if not isinstance(task_collect_data["task_list"], list):
|
214
|
+
# Verify that wheel file exists
|
215
|
+
wheel_path = task_group_attrs.get("wheel_path", None)
|
216
|
+
if wheel_path is not None:
|
217
|
+
if not Path(wheel_path).exists():
|
205
218
|
raise HTTPException(
|
206
219
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
207
|
-
detail=f"
|
220
|
+
detail=f"No such file: {wheel_path}.",
|
208
221
|
)
|
209
222
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
or db_task.source != task.source
|
217
|
-
or db_task.name != task.name
|
218
|
-
):
|
219
|
-
await db.close()
|
220
|
-
raise HTTPException(
|
221
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
222
|
-
detail=(
|
223
|
-
"Cannot collect package. Folder already exists, "
|
224
|
-
f"but task {task.id} does not exists or it does "
|
225
|
-
f"not have the expected source ({task.source}) or "
|
226
|
-
f"name ({task.name})."
|
227
|
-
),
|
228
|
-
)
|
229
|
-
except FileNotFoundError as e:
|
230
|
-
await db.close()
|
231
|
-
raise HTTPException(
|
232
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
233
|
-
detail=(
|
234
|
-
"Cannot collect package. Possible reason: another "
|
235
|
-
"collection of the same package is in progress. "
|
236
|
-
f"Original FileNotFoundError: {e}"
|
237
|
-
),
|
238
|
-
)
|
239
|
-
except ValidationError as e:
|
240
|
-
await db.close()
|
241
|
-
raise HTTPException(
|
242
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
243
|
-
detail=(
|
244
|
-
"Cannot collect package. Possible reason: an old version "
|
245
|
-
"of the same package has already been collected. "
|
246
|
-
f"Original ValidationError: {e}"
|
247
|
-
),
|
248
|
-
)
|
249
|
-
task_collect_data["info"] = "Already installed"
|
250
|
-
state = CollectionStateV2(data=task_collect_data)
|
251
|
-
response.status_code == status.HTTP_200_OK
|
252
|
-
await db.close()
|
253
|
-
return state
|
254
|
-
settings = Inject(get_settings)
|
255
|
-
|
256
|
-
# Check that tasks are not already in the DB
|
257
|
-
for new_task in task_pkg.package_manifest.task_list:
|
258
|
-
new_task_name_slug = slugify_task_name_for_source(new_task.name)
|
259
|
-
new_task_source = f"{task_pkg.package_source}:{new_task_name_slug}"
|
260
|
-
stm = select(TaskV2).where(TaskV2.source == new_task_source)
|
261
|
-
res = await db.execute(stm)
|
262
|
-
if res.scalars().all():
|
263
|
-
raise HTTPException(
|
264
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
265
|
-
detail=(
|
266
|
-
"Cannot collect package. Task with source "
|
267
|
-
f'"{new_task_source}" already exists in the database.'
|
268
|
-
),
|
269
|
-
)
|
223
|
+
# Create TaskGroupV2 object
|
224
|
+
task_group = TaskGroupV2(**task_group_attrs)
|
225
|
+
db.add(task_group)
|
226
|
+
await db.commit()
|
227
|
+
await db.refresh(task_group)
|
228
|
+
db.expunge(task_group)
|
270
229
|
|
271
230
|
# All checks are OK, proceed with task collection
|
272
|
-
|
231
|
+
collection_state_data = dict(
|
273
232
|
status=CollectionStatusV2.PENDING,
|
274
|
-
|
275
|
-
|
233
|
+
package=task_group.pkg_name,
|
234
|
+
version=task_group.version,
|
235
|
+
path=task_group.path,
|
236
|
+
venv_path=task_group.venv_path,
|
237
|
+
)
|
238
|
+
state = CollectionStateV2(
|
239
|
+
data=collection_state_data, taskgroupv2_id=task_group.id
|
276
240
|
)
|
277
|
-
state = CollectionStateV2(data=collection_status)
|
278
241
|
db.add(state)
|
279
242
|
await db.commit()
|
280
243
|
await db.refresh(state)
|
281
244
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
245
|
+
logger = set_logger(logger_name="collect_tasks_pip")
|
246
|
+
|
247
|
+
# END of SSH/non-SSH common part
|
248
|
+
|
249
|
+
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
250
|
+
# SSH task collection
|
251
|
+
|
252
|
+
from fractal_server.tasks.v2.background_operations_ssh import (
|
253
|
+
background_collect_pip_ssh,
|
254
|
+
)
|
255
|
+
|
256
|
+
# User appropriate FractalSSH object
|
257
|
+
ssh_credentials = dict(
|
258
|
+
user=user_settings.ssh_username,
|
259
|
+
host=user_settings.ssh_host,
|
260
|
+
key_path=user_settings.ssh_private_key_path,
|
261
|
+
)
|
262
|
+
fractal_ssh_list = request.app.state.fractal_ssh_list
|
263
|
+
fractal_ssh = fractal_ssh_list.get(**ssh_credentials)
|
264
|
+
|
265
|
+
background_tasks.add_task(
|
266
|
+
background_collect_pip_ssh,
|
267
|
+
state_id=state.id,
|
268
|
+
task_group=task_group,
|
269
|
+
fractal_ssh=fractal_ssh,
|
270
|
+
tasks_base_dir=user_settings.ssh_tasks_dir,
|
271
|
+
)
|
272
|
+
|
273
|
+
else:
|
274
|
+
# Local task collection
|
275
|
+
background_tasks.add_task(
|
276
|
+
background_collect_pip,
|
277
|
+
state_id=state.id,
|
278
|
+
task_group=task_group,
|
279
|
+
)
|
288
280
|
logger.debug(
|
289
281
|
"Task-collection endpoint: start background collection "
|
290
282
|
"and return state"
|
@@ -292,11 +284,10 @@ async def collect_tasks_pip(
|
|
292
284
|
reset_logger_handlers(logger)
|
293
285
|
info = (
|
294
286
|
"Collecting tasks in the background. "
|
295
|
-
f"GET /task/collect/{state.id} to query collection status"
|
287
|
+
f"GET /task/collect/{state.id}/ to query collection status"
|
296
288
|
)
|
297
289
|
state.data["info"] = info
|
298
290
|
response.status_code = status.HTTP_201_CREATED
|
299
|
-
await db.close()
|
300
291
|
|
301
292
|
return state
|
302
293
|
|
@@ -329,20 +320,17 @@ async def check_collection_status(
|
|
329
320
|
else:
|
330
321
|
# Non-SSH mode
|
331
322
|
# In some cases (i.e. a successful or ongoing task collection),
|
332
|
-
# state.data
|
323
|
+
# state.data["log"] is not set; if so, we collect the current logs.
|
333
324
|
if verbose and not state.data.get("log"):
|
334
|
-
if "
|
325
|
+
if "path" not in state.data.keys():
|
335
326
|
await db.close()
|
336
327
|
raise HTTPException(
|
337
328
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
338
329
|
detail=(
|
339
|
-
f"No '
|
330
|
+
f"No 'path' in CollectionStateV2[{state_id}].data"
|
340
331
|
),
|
341
332
|
)
|
342
|
-
state.data["log"] =
|
343
|
-
Path(state.data["venv_path"])
|
344
|
-
)
|
345
|
-
state.data["venv_path"] = str(state.data["venv_path"])
|
333
|
+
state.data["log"] = get_collection_log_v2(Path(state.data["path"]))
|
346
334
|
|
347
335
|
reset_logger_handlers(logger)
|
348
336
|
await db.close()
|
@@ -1,31 +1,38 @@
|
|
1
1
|
import shlex
|
2
2
|
import subprocess # nosec
|
3
3
|
from pathlib import Path
|
4
|
+
from typing import Optional
|
4
5
|
|
5
6
|
from fastapi import APIRouter
|
6
7
|
from fastapi import Depends
|
7
8
|
from fastapi import HTTPException
|
8
9
|
from fastapi import status
|
9
|
-
from
|
10
|
-
|
11
|
-
from
|
12
|
-
from
|
13
|
-
from
|
14
|
-
from
|
15
|
-
from
|
16
|
-
from
|
17
|
-
from ....models.v2 import TaskV2
|
18
|
-
from ....schemas.v2 import TaskCollectCustomV2
|
19
|
-
from ....schemas.v2 import TaskCreateV2
|
20
|
-
from ....schemas.v2 import TaskReadV2
|
21
|
-
from ...aux.validate_user_settings import verify_user_has_settings
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
11
|
+
|
12
|
+
from ._aux_functions_tasks import _get_valid_user_group_id
|
13
|
+
from ._aux_functions_tasks import _verify_non_duplication_group_constraint
|
14
|
+
from ._aux_functions_tasks import _verify_non_duplication_user_constraint
|
15
|
+
from fractal_server.app.db import DBSyncSession
|
16
|
+
from fractal_server.app.db import get_async_db
|
17
|
+
from fractal_server.app.db import get_sync_db
|
22
18
|
from fractal_server.app.models import UserOAuth
|
19
|
+
from fractal_server.app.models.v2 import TaskGroupV2
|
23
20
|
from fractal_server.app.routes.auth import current_active_verified_user
|
21
|
+
from fractal_server.app.schemas.v2 import TaskCollectCustomV2
|
22
|
+
from fractal_server.app.schemas.v2 import TaskCreateV2
|
23
|
+
from fractal_server.app.schemas.v2 import TaskGroupCreateV2
|
24
|
+
from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
|
25
|
+
from fractal_server.app.schemas.v2 import TaskReadV2
|
26
|
+
from fractal_server.config import get_settings
|
27
|
+
from fractal_server.logger import set_logger
|
24
28
|
from fractal_server.string_tools import validate_cmd
|
25
|
-
from fractal_server.
|
29
|
+
from fractal_server.syringe import Inject
|
26
30
|
from fractal_server.tasks.v2.background_operations import (
|
27
31
|
_prepare_tasks_metadata,
|
28
32
|
)
|
33
|
+
from fractal_server.tasks.v2.database_operations import (
|
34
|
+
create_db_tasks_and_update_task_group,
|
35
|
+
)
|
29
36
|
|
30
37
|
router = APIRouter()
|
31
38
|
|
@@ -37,12 +44,25 @@ logger = set_logger(__name__)
|
|
37
44
|
)
|
38
45
|
async def collect_task_custom(
|
39
46
|
task_collect: TaskCollectCustomV2,
|
47
|
+
private: bool = False,
|
48
|
+
user_group_id: Optional[int] = None,
|
40
49
|
user: UserOAuth = Depends(current_active_verified_user),
|
41
|
-
db:
|
50
|
+
db: AsyncSession = Depends(get_async_db), # FIXME: using both sync/async
|
51
|
+
db_sync: DBSyncSession = Depends(
|
52
|
+
get_sync_db
|
53
|
+
), # FIXME: using both sync/async
|
42
54
|
) -> list[TaskReadV2]:
|
43
55
|
|
44
56
|
settings = Inject(get_settings)
|
45
57
|
|
58
|
+
# Validate query parameters related to user-group ownership
|
59
|
+
user_group_id = await _get_valid_user_group_id(
|
60
|
+
user_group_id=user_group_id,
|
61
|
+
private=private,
|
62
|
+
user_id=user.id,
|
63
|
+
db=db,
|
64
|
+
)
|
65
|
+
|
46
66
|
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
47
67
|
if task_collect.package_root is None:
|
48
68
|
raise HTTPException(
|
@@ -112,65 +132,51 @@ async def collect_task_custom(
|
|
112
132
|
else:
|
113
133
|
package_root = Path(task_collect.package_root)
|
114
134
|
|
115
|
-
# Set task.owner attribute
|
116
|
-
if user.username:
|
117
|
-
owner = user.username
|
118
|
-
else:
|
119
|
-
verify_user_has_settings(user)
|
120
|
-
owner = user.settings.slurm_user
|
121
|
-
if owner is None:
|
122
|
-
raise HTTPException(
|
123
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
124
|
-
detail=(
|
125
|
-
"Cannot add a new task because current user does not "
|
126
|
-
"have `username` or `slurm_user` attributes."
|
127
|
-
),
|
128
|
-
)
|
129
|
-
source = f"{owner}:{task_collect.source}"
|
130
|
-
|
131
135
|
task_list: list[TaskCreateV2] = _prepare_tasks_metadata(
|
132
136
|
package_manifest=task_collect.manifest,
|
133
|
-
package_source=source,
|
134
137
|
python_bin=Path(task_collect.python_interpreter),
|
135
138
|
package_root=package_root,
|
136
139
|
package_version=task_collect.version,
|
137
140
|
)
|
138
|
-
# Verify that source is not already in use (note: this check is only useful
|
139
|
-
# to provide a user-friendly error message, but `task.source` uniqueness is
|
140
|
-
# already guaranteed by a constraint in the table definition).
|
141
|
-
sources = [task.source for task in task_list]
|
142
|
-
stm = select(TaskV2).where(TaskV2.source.in_(sources))
|
143
|
-
res = db.execute(stm)
|
144
|
-
overlapping_sources_v2 = res.scalars().all()
|
145
|
-
if overlapping_sources_v2:
|
146
|
-
overlapping_tasks_v2_source_and_id = [
|
147
|
-
f"TaskV2 with ID {task.id} already has source='{task.source}'"
|
148
|
-
for task in overlapping_sources_v2
|
149
|
-
]
|
150
|
-
raise HTTPException(
|
151
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
152
|
-
detail="\n".join(overlapping_tasks_v2_source_and_id),
|
153
|
-
)
|
154
|
-
stm = select(TaskV1).where(TaskV1.source.in_(sources))
|
155
|
-
res = db.execute(stm)
|
156
|
-
overlapping_sources_v1 = res.scalars().all()
|
157
|
-
if overlapping_sources_v1:
|
158
|
-
overlapping_tasks_v1_source_and_id = [
|
159
|
-
f"TaskV1 with ID {task.id} already has source='{task.source}'\n"
|
160
|
-
for task in overlapping_sources_v1
|
161
|
-
]
|
162
|
-
raise HTTPException(
|
163
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
164
|
-
detail="\n".join(overlapping_tasks_v1_source_and_id),
|
165
|
-
)
|
166
141
|
|
167
|
-
|
168
|
-
|
142
|
+
# Prepare task-group attributes
|
143
|
+
task_group_attrs = dict(
|
144
|
+
origin=TaskGroupV2OriginEnum.OTHER,
|
145
|
+
pkg_name=task_collect.label,
|
146
|
+
user_id=user.id,
|
147
|
+
user_group_id=user_group_id,
|
148
|
+
)
|
149
|
+
TaskGroupCreateV2(**task_group_attrs)
|
150
|
+
|
151
|
+
# Verify non-duplication constraints
|
152
|
+
await _verify_non_duplication_user_constraint(
|
153
|
+
user_id=user.id,
|
154
|
+
pkg_name=task_group_attrs["pkg_name"],
|
155
|
+
version=None,
|
156
|
+
db=db,
|
157
|
+
)
|
158
|
+
await _verify_non_duplication_group_constraint(
|
159
|
+
user_group_id=task_group_attrs["user_group_id"],
|
160
|
+
pkg_name=task_group_attrs["pkg_name"],
|
161
|
+
version=None,
|
162
|
+
db=db,
|
163
|
+
)
|
164
|
+
|
165
|
+
task_group = TaskGroupV2(**task_group_attrs)
|
166
|
+
db.add(task_group)
|
167
|
+
await db.commit()
|
168
|
+
await db.refresh(task_group)
|
169
|
+
db.expunge(task_group)
|
170
|
+
|
171
|
+
task_group = create_db_tasks_and_update_task_group(
|
172
|
+
task_list=task_list,
|
173
|
+
task_group_id=task_group.id,
|
174
|
+
db=db_sync,
|
169
175
|
)
|
170
176
|
|
171
177
|
logger.debug(
|
172
178
|
f"Custom-environment task collection by user {user.email} completed, "
|
173
|
-
f"for package
|
179
|
+
f"for package {task_collect}"
|
174
180
|
)
|
175
181
|
|
176
|
-
return
|
182
|
+
return task_group.task_list
|