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,17 +1,16 @@
|
|
1
|
-
import json
|
2
1
|
import os
|
3
2
|
from pathlib import Path
|
4
3
|
from tempfile import TemporaryDirectory
|
5
4
|
|
6
5
|
from sqlalchemy.orm.attributes import flag_modified
|
7
6
|
|
8
|
-
from ...app.models.v2 import CollectionStateV2
|
9
|
-
from ._TaskCollectPip import _TaskCollectPip
|
10
7
|
from .background_operations import _handle_failure
|
11
|
-
from .background_operations import _insert_tasks
|
12
8
|
from .background_operations import _prepare_tasks_metadata
|
13
9
|
from .background_operations import _set_collection_state_data_status
|
10
|
+
from .database_operations import create_db_tasks_and_update_task_group
|
14
11
|
from fractal_server.app.db import get_sync_db
|
12
|
+
from fractal_server.app.models.v2 import CollectionStateV2
|
13
|
+
from fractal_server.app.models.v2 import TaskGroupV2
|
15
14
|
from fractal_server.app.schemas.v2 import CollectionStatusV2
|
16
15
|
from fractal_server.app.schemas.v2.manifest import ManifestV2
|
17
16
|
from fractal_server.config import get_settings
|
@@ -108,8 +107,9 @@ def _customize_and_run_template(
|
|
108
107
|
|
109
108
|
|
110
109
|
def background_collect_pip_ssh(
|
110
|
+
*,
|
111
111
|
state_id: int,
|
112
|
-
|
112
|
+
task_group: TaskGroupV2,
|
113
113
|
fractal_ssh: FractalSSH,
|
114
114
|
tasks_base_dir: str,
|
115
115
|
) -> None:
|
@@ -126,7 +126,7 @@ def background_collect_pip_ssh(
|
|
126
126
|
|
127
127
|
Arguments:
|
128
128
|
state_id:
|
129
|
-
|
129
|
+
task_group:
|
130
130
|
fractal_ssh:
|
131
131
|
tasks_base_dir:
|
132
132
|
"""
|
@@ -139,47 +139,30 @@ def background_collect_pip_ssh(
|
|
139
139
|
logger_name=LOGGER_NAME,
|
140
140
|
log_file_path=log_file_path,
|
141
141
|
)
|
142
|
-
|
143
142
|
logger.debug("START")
|
144
|
-
for key, value in
|
145
|
-
logger.debug(f"
|
143
|
+
for key, value in task_group.model_dump().items():
|
144
|
+
logger.debug(f"task_group.{key}: {value}")
|
145
|
+
|
146
|
+
# `remove_venv_folder_upon_failure` is set to True only if
|
147
|
+
# script 1 goes through, which means that the remote folder
|
148
|
+
# `package_env_dir` did not already exist. If this remote
|
149
|
+
# folder already existed, then script 1 fails and the boolean
|
150
|
+
# flag `remove_venv_folder_upon_failure` remains false.
|
151
|
+
remove_venv_folder_upon_failure = False
|
146
152
|
|
147
153
|
# Open a DB session soon, since it is needed for updating `state`
|
148
154
|
with next(get_sync_db()) as db:
|
149
155
|
try:
|
150
156
|
# Prepare replacements for task-collection scripts
|
151
157
|
python_bin = get_python_interpreter_v2(
|
152
|
-
python_version=
|
153
|
-
)
|
154
|
-
package_version = (
|
155
|
-
""
|
156
|
-
if task_pkg.package_version is None
|
157
|
-
else task_pkg.package_version
|
158
|
+
python_version=task_group.python_version
|
158
159
|
)
|
159
|
-
|
160
|
-
install_string = task_pkg.package
|
161
|
-
if task_pkg.package_extras is not None:
|
162
|
-
install_string = (
|
163
|
-
f"{install_string}[{task_pkg.package_extras}]"
|
164
|
-
)
|
165
|
-
if (
|
166
|
-
task_pkg.package_version is not None
|
167
|
-
and not task_pkg.is_local_package
|
168
|
-
):
|
169
|
-
install_string = (
|
170
|
-
f"{install_string}=={task_pkg.package_version}"
|
171
|
-
)
|
172
|
-
package_env_dir = (
|
173
|
-
Path(tasks_base_dir)
|
174
|
-
/ ".fractal"
|
175
|
-
/ f"{task_pkg.package_name}{package_version}"
|
176
|
-
).as_posix()
|
177
|
-
logger.debug(f"{package_env_dir=}")
|
160
|
+
install_string = task_group.pip_install_string
|
178
161
|
settings = Inject(get_settings)
|
179
162
|
replacements = [
|
180
|
-
("__PACKAGE_NAME__",
|
181
|
-
("
|
182
|
-
("
|
163
|
+
("__PACKAGE_NAME__", task_group.pkg_name),
|
164
|
+
("__TASK_GROUP_DIR__", task_group.path),
|
165
|
+
("__PACKAGE_ENV_DIR__", task_group.venv_path),
|
183
166
|
("__PYTHON__", python_bin),
|
184
167
|
("__INSTALL_STRING__", install_string),
|
185
168
|
(
|
@@ -210,12 +193,11 @@ def background_collect_pip_ssh(
|
|
210
193
|
# long operations that do not use the db
|
211
194
|
db.close()
|
212
195
|
|
213
|
-
#
|
214
|
-
#
|
215
|
-
# `
|
216
|
-
|
217
|
-
|
218
|
-
remove_venv_folder_upon_failure = False
|
196
|
+
# Create remote folder (note that because of `parents=True` we
|
197
|
+
# are in the `no error if existing, make parent directories as
|
198
|
+
# needed` scenario)
|
199
|
+
fractal_ssh.mkdir(folder=tasks_base_dir, parents=True)
|
200
|
+
|
219
201
|
stdout = _customize_and_run_template(
|
220
202
|
script_filename="_1_create_venv.sh",
|
221
203
|
**common_args,
|
@@ -223,7 +205,7 @@ def background_collect_pip_ssh(
|
|
223
205
|
remove_venv_folder_upon_failure = True
|
224
206
|
|
225
207
|
stdout = _customize_and_run_template(
|
226
|
-
script_filename="
|
208
|
+
script_filename="_2_preliminary_pip_operations.sh",
|
227
209
|
**common_args,
|
228
210
|
)
|
229
211
|
stdout = _customize_and_run_template(
|
@@ -258,13 +240,13 @@ def background_collect_pip_ssh(
|
|
258
240
|
f"collecting - parsed from pip-show: {key}={value}"
|
259
241
|
)
|
260
242
|
# Check package_name match
|
261
|
-
# FIXME SSH: Does this work for non-canonical
|
243
|
+
# FIXME SSH: Does this work well for non-canonical names?
|
262
244
|
package_name_pip_show = pkg_attrs.get("package_name")
|
263
|
-
|
264
|
-
if package_name_pip_show !=
|
245
|
+
package_name_task_group = task_group.pkg_name
|
246
|
+
if package_name_pip_show != package_name_task_group:
|
265
247
|
error_msg = (
|
266
248
|
f"`package_name` mismatch: "
|
267
|
-
f"{
|
249
|
+
f"{package_name_task_group=} but "
|
268
250
|
f"{package_name_pip_show=}"
|
269
251
|
)
|
270
252
|
logger.error(error_msg)
|
@@ -286,31 +268,35 @@ def background_collect_pip_ssh(
|
|
286
268
|
).as_posix()
|
287
269
|
|
288
270
|
# Read and validate remote manifest file
|
289
|
-
|
290
|
-
|
271
|
+
pkg_manifest_dict = fractal_ssh.read_remote_json_file(
|
272
|
+
manifest_path_remote
|
273
|
+
)
|
291
274
|
logger.info(f"collecting - loaded {manifest_path_remote=}")
|
292
|
-
ManifestV2(**
|
275
|
+
pkg_manifest = ManifestV2(**pkg_manifest_dict)
|
293
276
|
logger.info("collecting - manifest is a valid ManifestV2")
|
294
277
|
|
295
|
-
|
296
|
-
new_pkg = _TaskCollectPip(
|
297
|
-
**task_pkg.dict(
|
298
|
-
exclude={"package_version", "package_name"},
|
299
|
-
exclude_unset=True,
|
300
|
-
exclude_none=True,
|
301
|
-
),
|
302
|
-
package_manifest=manifest,
|
303
|
-
**pkg_attrs,
|
304
|
-
)
|
305
|
-
|
278
|
+
logger.info("collecting - _prepare_tasks_metadata - start")
|
306
279
|
task_list = _prepare_tasks_metadata(
|
307
|
-
package_manifest=
|
308
|
-
package_version=
|
309
|
-
package_source=new_pkg.package_source,
|
280
|
+
package_manifest=pkg_manifest,
|
281
|
+
package_version=task_group.version,
|
310
282
|
package_root=Path(package_root_remote),
|
311
283
|
python_bin=Path(python_bin),
|
312
284
|
)
|
313
|
-
|
285
|
+
logger.info("collecting - _prepare_tasks_metadata - end")
|
286
|
+
|
287
|
+
logger.info(
|
288
|
+
"collecting - create_db_tasks_and_update_task_group - "
|
289
|
+
"start"
|
290
|
+
)
|
291
|
+
create_db_tasks_and_update_task_group(
|
292
|
+
task_list=task_list,
|
293
|
+
task_group_id=task_group.id,
|
294
|
+
db=db,
|
295
|
+
)
|
296
|
+
logger.info(
|
297
|
+
"collecting - create_db_tasks_and_update_task_group - end"
|
298
|
+
)
|
299
|
+
|
314
300
|
logger.debug("collecting - END")
|
315
301
|
|
316
302
|
# Finalize (write metadata to DB)
|
@@ -333,18 +319,19 @@ def background_collect_pip_ssh(
|
|
333
319
|
logger_name=LOGGER_NAME,
|
334
320
|
exception=e,
|
335
321
|
db=db,
|
322
|
+
task_group_id=task_group.id,
|
336
323
|
)
|
337
324
|
if remove_venv_folder_upon_failure:
|
338
325
|
try:
|
339
326
|
logger.info(
|
340
|
-
f"Now delete remote folder {
|
327
|
+
f"Now delete remote folder {task_group.path}"
|
341
328
|
)
|
342
329
|
fractal_ssh.remove_folder(
|
343
|
-
folder=
|
330
|
+
folder=task_group.path,
|
344
331
|
safe_root=tasks_base_dir,
|
345
332
|
)
|
346
333
|
logger.info(
|
347
|
-
f"Deleted remoted folder {
|
334
|
+
f"Deleted remoted folder {task_group.path}"
|
348
335
|
)
|
349
336
|
except Exception as e:
|
350
337
|
logger.error(
|
@@ -354,6 +341,6 @@ def background_collect_pip_ssh(
|
|
354
341
|
else:
|
355
342
|
logger.info(
|
356
343
|
"Not trying to remove remote folder "
|
357
|
-
f"{
|
344
|
+
f"{task_group.path}."
|
358
345
|
)
|
359
346
|
return
|
@@ -0,0 +1,44 @@
|
|
1
|
+
from sqlalchemy.orm import Session as DBSyncSession
|
2
|
+
|
3
|
+
from fractal_server.app.models.v2 import TaskGroupV2
|
4
|
+
from fractal_server.app.models.v2 import TaskV2
|
5
|
+
from fractal_server.app.schemas.v2 import TaskCreateV2
|
6
|
+
|
7
|
+
|
8
|
+
def _get_task_type(task: TaskCreateV2) -> str:
|
9
|
+
if task.command_non_parallel is None:
|
10
|
+
return "parallel"
|
11
|
+
elif task.command_parallel is None:
|
12
|
+
return "non_parallel"
|
13
|
+
else:
|
14
|
+
return "compound"
|
15
|
+
|
16
|
+
|
17
|
+
def create_db_tasks_and_update_task_group(
|
18
|
+
*,
|
19
|
+
task_group_id: int,
|
20
|
+
task_list: list[TaskCreateV2],
|
21
|
+
db: DBSyncSession,
|
22
|
+
) -> TaskGroupV2:
|
23
|
+
"""
|
24
|
+
Create a `TaskGroupV2` with N `TaskV2`s, and insert them into the database.
|
25
|
+
|
26
|
+
Arguments:
|
27
|
+
task_group: ID of an existing TaskGroupV2 object.
|
28
|
+
task_list: A list of TaskCreateV2 objects to be inserted into the db.
|
29
|
+
db: A synchronous database session
|
30
|
+
"""
|
31
|
+
actual_task_list = [
|
32
|
+
TaskV2(
|
33
|
+
**task.dict(),
|
34
|
+
type=_get_task_type(task),
|
35
|
+
)
|
36
|
+
for task in task_list
|
37
|
+
]
|
38
|
+
task_group = db.get(TaskGroupV2, task_group_id)
|
39
|
+
task_group.task_list = actual_task_list
|
40
|
+
db.add(task_group)
|
41
|
+
db.commit()
|
42
|
+
db.refresh(task_group)
|
43
|
+
|
44
|
+
return task_group
|
@@ -1,136 +1,124 @@
|
|
1
|
-
import json
|
2
|
-
from pathlib import Path
|
3
|
-
from typing import Literal
|
4
1
|
from typing import Optional
|
5
|
-
from typing import Union
|
6
|
-
from zipfile import ZipFile
|
7
2
|
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from
|
12
|
-
from fractal_server.config import get_settings
|
13
|
-
from fractal_server.logger import get_logger
|
14
|
-
from fractal_server.syringe import Inject
|
15
|
-
from fractal_server.utils import execute_command
|
3
|
+
from fastapi import HTTPException
|
4
|
+
from fastapi import status
|
5
|
+
from httpx import AsyncClient
|
6
|
+
from httpx import TimeoutException
|
16
7
|
|
8
|
+
from fractal_server.logger import set_logger
|
17
9
|
|
18
|
-
FRACTAL_PUBLIC_TASK_SUBDIR = ".fractal"
|
19
10
|
|
20
|
-
|
21
|
-
async def download_package(
|
22
|
-
*,
|
23
|
-
task_pkg: _TaskCollectPip,
|
24
|
-
dest: Union[str, Path],
|
25
|
-
) -> Path:
|
26
|
-
"""
|
27
|
-
Download package to destination and return wheel-file path.
|
28
|
-
"""
|
29
|
-
interpreter = get_python_interpreter_v2(
|
30
|
-
python_version=task_pkg.python_version
|
31
|
-
)
|
32
|
-
pip = f"{interpreter} -m pip"
|
33
|
-
if task_pkg.package_version is None:
|
34
|
-
package_and_version = f"{task_pkg.package_name}"
|
35
|
-
else:
|
36
|
-
package_and_version = (
|
37
|
-
f"{task_pkg.package_name}=={task_pkg.package_version}"
|
38
|
-
)
|
39
|
-
cmd = f"{pip} download --no-deps {package_and_version} -d {dest}"
|
40
|
-
stdout = await execute_command(command=cmd, cwd=Path("."))
|
41
|
-
pkg_file = next(
|
42
|
-
line.split()[-1] for line in stdout.split("\n") if "Saved" in line
|
43
|
-
)
|
44
|
-
return Path(pkg_file)
|
11
|
+
logger = set_logger(__name__)
|
45
12
|
|
46
13
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
namelist = wheel.namelist()
|
52
|
-
try:
|
53
|
-
manifest = next(
|
54
|
-
name for name in namelist if "__FRACTAL_MANIFEST__.json" in name
|
55
|
-
)
|
56
|
-
except StopIteration:
|
57
|
-
msg = f"{path.as_posix()} does not include __FRACTAL_MANIFEST__.json"
|
58
|
-
logger.error(msg)
|
59
|
-
raise ValueError(msg)
|
60
|
-
with wheel.open(manifest) as manifest_fd:
|
61
|
-
manifest_dict = json.load(manifest_fd)
|
62
|
-
manifest_version = str(manifest_dict["manifest_version"])
|
63
|
-
if manifest_version == "2":
|
64
|
-
pkg_manifest = ManifestV2(**manifest_dict)
|
65
|
-
return pkg_manifest
|
66
|
-
else:
|
67
|
-
msg = f"Manifest version {manifest_version=} not supported"
|
68
|
-
logger.error(msg)
|
69
|
-
raise ValueError(msg)
|
70
|
-
|
71
|
-
|
72
|
-
def inspect_package(
|
73
|
-
path: Path, logger_name: Optional[str] = None
|
74
|
-
) -> dict[Literal["pkg_version", "pkg_manifest"], str]:
|
14
|
+
async def get_package_version_from_pypi(
|
15
|
+
name: str,
|
16
|
+
version: Optional[str] = None,
|
17
|
+
) -> str:
|
75
18
|
"""
|
76
|
-
|
19
|
+
Make a GET call to PyPI JSON API and get latest *compatible* version.
|
20
|
+
|
21
|
+
There are three cases:
|
77
22
|
|
78
|
-
|
79
|
-
|
80
|
-
|
23
|
+
1. `version` is set and it is found on PyPI as-is.
|
24
|
+
2. `version` is set but it is not found on PyPI as-is.
|
25
|
+
3. `version` is unset, and we query `PyPI` for latest.
|
81
26
|
|
82
|
-
|
83
|
-
path: Path of the package wheel file.
|
84
|
-
logger_name:
|
27
|
+
Ref https://warehouse.pypa.io/api-reference/json.html.
|
85
28
|
|
86
|
-
|
87
|
-
|
29
|
+
Arguments:
|
30
|
+
name: Package name.
|
31
|
+
version:
|
32
|
+
Could be a correct version (`1.3.0`), an incomplete one
|
33
|
+
(`1.3`) or `None`.
|
88
34
|
"""
|
89
35
|
|
90
|
-
|
36
|
+
url = f"https://pypi.org/pypi/{name}/json"
|
37
|
+
hint = f"Hint: specify the required version for '{name}'."
|
91
38
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
39
|
+
# Make request to PyPI
|
40
|
+
try:
|
41
|
+
async with AsyncClient(timeout=5.0) as client:
|
42
|
+
res = await client.get(url)
|
43
|
+
except TimeoutException as e:
|
44
|
+
error_msg = (
|
45
|
+
f"A TimeoutException occurred while getting {url}.\n"
|
46
|
+
f"Original error: {str(e)}."
|
47
|
+
)
|
48
|
+
logger.error(error_msg)
|
49
|
+
raise HTTPException(
|
50
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
51
|
+
detail=error_msg,
|
52
|
+
)
|
53
|
+
except BaseException as e:
|
54
|
+
error_msg = (
|
55
|
+
f"An unknown error occurred while getting {url}. "
|
56
|
+
f"Original error: {str(e)}."
|
57
|
+
)
|
58
|
+
logger.error(error_msg)
|
59
|
+
raise HTTPException(
|
60
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
61
|
+
detail=error_msg,
|
96
62
|
)
|
97
63
|
|
98
|
-
#
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
64
|
+
# Parse response
|
65
|
+
if res.status_code != 200:
|
66
|
+
raise HTTPException(
|
67
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
68
|
+
detail=(
|
69
|
+
f"Could not get {url} (status_code {res.status_code})."
|
70
|
+
f"\n{hint}"
|
71
|
+
),
|
72
|
+
)
|
73
|
+
try:
|
74
|
+
response_data = res.json()
|
75
|
+
latest_version = response_data["info"]["version"]
|
76
|
+
available_releases = response_data["releases"].keys()
|
77
|
+
except KeyError as e:
|
78
|
+
logger.error(
|
79
|
+
f"A KeyError occurred while getting {url}. "
|
80
|
+
f"Original error: {str(e)}."
|
81
|
+
)
|
82
|
+
raise HTTPException(
|
83
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
84
|
+
detail=f"A KeyError error occurred while getting {url}.\n{hint}",
|
107
85
|
)
|
108
|
-
logger.debug("Manifest read correctly.")
|
109
86
|
|
110
|
-
info
|
111
|
-
|
112
|
-
|
87
|
+
logger.info(
|
88
|
+
f"Obtained data from {url}: "
|
89
|
+
f"{len(available_releases)} releases, "
|
90
|
+
f"latest={latest_version}."
|
113
91
|
)
|
114
|
-
return info
|
115
|
-
|
116
92
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
93
|
+
if version is not None:
|
94
|
+
if version in available_releases:
|
95
|
+
logger.info(f"Requested {version=} available on PyPI.")
|
96
|
+
# Case 1: `version` is set and it is found on PyPI as-is
|
97
|
+
return version
|
98
|
+
else:
|
99
|
+
# Case 2: `version` is set but it is not found on PyPI as-is
|
100
|
+
# Filter using `version` as prefix, and sort
|
101
|
+
matching_versions = [
|
102
|
+
v for v in available_releases if v.startswith(version)
|
103
|
+
]
|
104
|
+
logger.info(
|
105
|
+
f"Requested {version=} not available on PyPI, "
|
106
|
+
f"found {len(matching_versions)} versions matching "
|
107
|
+
f"`{version}*`."
|
108
|
+
)
|
109
|
+
if len(matching_versions) == 0:
|
110
|
+
logger.info(f"No version starting with {version} found.")
|
111
|
+
raise HTTPException(
|
112
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
113
|
+
detail=(
|
114
|
+
f"No version starting with {version} found.\n"
|
115
|
+
f"{hint}"
|
116
|
+
),
|
117
|
+
)
|
118
|
+
else:
|
119
|
+
latest_matching_version = sorted(matching_versions)[-1]
|
120
|
+
return latest_matching_version
|
121
|
+
else:
|
122
|
+
# Case 3: `version` is unset and we use latest
|
123
|
+
logger.info(f"No version requested, returning {latest_version=}.")
|
124
|
+
return latest_version
|
@@ -7,16 +7,20 @@ write_log(){
|
|
7
7
|
|
8
8
|
|
9
9
|
# Variables to be filled within fractal-server
|
10
|
+
TASK_GROUP_DIR=__TASK_GROUP_DIR__
|
10
11
|
PACKAGE_ENV_DIR=__PACKAGE_ENV_DIR__
|
11
12
|
PYTHON=__PYTHON__
|
12
13
|
|
13
14
|
TIME_START=$(date +%s)
|
14
15
|
|
15
|
-
# Check that
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
# Check that task-group and venv folders do not exist
|
17
|
+
for DIR_TO_BE_CHECKED in "$TASK_GROUP_DIR" "$PACKAGE_ENV_DIR";
|
18
|
+
do
|
19
|
+
if [ -d "$DIR_TO_BE_CHECKED" ]; then
|
20
|
+
write_log "ERROR: Folder $DIR_TO_BE_CHECKED already exists. Exit."
|
21
|
+
exit 1
|
22
|
+
fi
|
23
|
+
done
|
20
24
|
|
21
25
|
write_log "START mkdir -p $PACKAGE_ENV_DIR"
|
22
26
|
mkdir -p $PACKAGE_ENV_DIR
|
fractal_server/tasks/v2/utils.py
CHANGED
@@ -45,6 +45,11 @@ def _parse_wheel_filename(wheel_filename: str) -> dict[str, str]:
|
|
45
45
|
Note that we transform exceptions in `ValueError`s, since this function is
|
46
46
|
also used within Pydantic validators.
|
47
47
|
"""
|
48
|
+
if "/" in wheel_filename:
|
49
|
+
raise ValueError(
|
50
|
+
"[_parse_wheel_filename] Input must be a filename, not a full "
|
51
|
+
f"path (given: {wheel_filename})."
|
52
|
+
)
|
48
53
|
try:
|
49
54
|
parts = wheel_filename.split("-")
|
50
55
|
return dict(distribution=parts[0], version=parts[1])
|
fractal_server/utils.py
CHANGED
@@ -32,8 +32,8 @@ def get_timestamp() -> datetime:
|
|
32
32
|
|
33
33
|
async def execute_command(
|
34
34
|
*,
|
35
|
-
cwd: Path,
|
36
35
|
command: str,
|
36
|
+
cwd: Optional[Path] = None,
|
37
37
|
logger_name: Optional[str] = None,
|
38
38
|
) -> str:
|
39
39
|
"""
|
@@ -60,12 +60,13 @@ async def execute_command(
|
|
60
60
|
cmd, *args = command_split
|
61
61
|
|
62
62
|
logger = get_logger(logger_name)
|
63
|
+
cwd_kwarg = dict() if cwd is None else dict(cwd=cwd)
|
63
64
|
proc = await asyncio.create_subprocess_exec(
|
64
65
|
cmd,
|
65
66
|
*args,
|
66
67
|
stdout=asyncio.subprocess.PIPE,
|
67
68
|
stderr=asyncio.subprocess.PIPE,
|
68
|
-
|
69
|
+
**cwd_kwarg,
|
69
70
|
)
|
70
71
|
stdout, stderr = await proc.communicate()
|
71
72
|
logger.debug(f"Subprocess call to: {command}")
|
@@ -1,34 +1,30 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fractal-server
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.7.0
|
4
4
|
Summary: Server component of the Fractal analytics platform
|
5
5
|
Home-page: https://github.com/fractal-analytics-platform/fractal-server
|
6
6
|
License: BSD-3-Clause
|
7
7
|
Author: Tommaso Comparin
|
8
8
|
Author-email: tommaso.comparin@exact-lab.it
|
9
|
-
Requires-Python: >=3.
|
9
|
+
Requires-Python: >=3.10,<4.0
|
10
10
|
Classifier: License :: OSI Approved :: BSD License
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
13
12
|
Classifier: Programming Language :: Python :: 3.10
|
14
13
|
Classifier: Programming Language :: Python :: 3.11
|
15
14
|
Classifier: Programming Language :: Python :: 3.12
|
16
15
|
Provides-Extra: gunicorn
|
17
|
-
Provides-Extra: postgres
|
18
16
|
Provides-Extra: postgres-psycopg-binary
|
19
17
|
Requires-Dist: aiosqlite (>=0.19.0,<0.20.0)
|
20
18
|
Requires-Dist: alembic (>=1.13.1,<2.0.0)
|
21
|
-
Requires-Dist: asyncpg (>=0.29.0,<0.30.0) ; extra == "postgres"
|
22
19
|
Requires-Dist: bcrypt (==4.0.1)
|
23
20
|
Requires-Dist: cloudpickle (>=3.0.0,<3.1.0)
|
24
21
|
Requires-Dist: clusterfutures (>=0.5,<0.6)
|
25
22
|
Requires-Dist: fabric (>=3.2.2,<4.0.0)
|
26
|
-
Requires-Dist: fastapi (>=0.
|
23
|
+
Requires-Dist: fastapi (>=0.115.0,<0.116.0)
|
27
24
|
Requires-Dist: fastapi-users[oauth] (>=12.1.0,<13.0.0)
|
28
25
|
Requires-Dist: gunicorn (>=21.2,<23.0) ; extra == "gunicorn"
|
29
26
|
Requires-Dist: packaging (>=23.2,<24.0)
|
30
27
|
Requires-Dist: psutil (>=5.9.8,<6.0.0)
|
31
|
-
Requires-Dist: psycopg2 (>=2.9.5,<3.0.0) ; extra == "postgres"
|
32
28
|
Requires-Dist: psycopg[binary] (>=3.1.0,<4.0.0) ; extra == "postgres-psycopg-binary"
|
33
29
|
Requires-Dist: pydantic (>=1.10.8,<2)
|
34
30
|
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|