fractal-server 2.6.4__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.
Files changed (71) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +1 -1
  3. fractal_server/app/models/linkusergroup.py +11 -0
  4. fractal_server/app/models/v2/__init__.py +2 -0
  5. fractal_server/app/models/v2/collection_state.py +1 -0
  6. fractal_server/app/models/v2/task.py +67 -2
  7. fractal_server/app/routes/admin/v2/__init__.py +16 -0
  8. fractal_server/app/routes/admin/{v2.py → v2/job.py} +20 -191
  9. fractal_server/app/routes/admin/v2/project.py +43 -0
  10. fractal_server/app/routes/admin/v2/task.py +133 -0
  11. fractal_server/app/routes/admin/v2/task_group.py +162 -0
  12. fractal_server/app/routes/api/v1/task_collection.py +4 -4
  13. fractal_server/app/routes/api/v2/__init__.py +8 -0
  14. fractal_server/app/routes/api/v2/_aux_functions.py +1 -68
  15. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +343 -0
  16. fractal_server/app/routes/api/v2/submit.py +16 -35
  17. fractal_server/app/routes/api/v2/task.py +85 -110
  18. fractal_server/app/routes/api/v2/task_collection.py +184 -196
  19. fractal_server/app/routes/api/v2/task_collection_custom.py +70 -64
  20. fractal_server/app/routes/api/v2/task_group.py +173 -0
  21. fractal_server/app/routes/api/v2/workflow.py +39 -102
  22. fractal_server/app/routes/api/v2/workflow_import.py +360 -0
  23. fractal_server/app/routes/api/v2/workflowtask.py +4 -8
  24. fractal_server/app/routes/auth/_aux_auth.py +86 -40
  25. fractal_server/app/routes/auth/current_user.py +5 -5
  26. fractal_server/app/routes/auth/group.py +73 -23
  27. fractal_server/app/routes/auth/router.py +0 -2
  28. fractal_server/app/routes/auth/users.py +8 -7
  29. fractal_server/app/runner/executors/slurm/ssh/executor.py +82 -63
  30. fractal_server/app/runner/v2/__init__.py +13 -7
  31. fractal_server/app/runner/v2/task_interface.py +4 -9
  32. fractal_server/app/schemas/user.py +1 -2
  33. fractal_server/app/schemas/v2/__init__.py +7 -0
  34. fractal_server/app/schemas/v2/dataset.py +2 -7
  35. fractal_server/app/schemas/v2/dumps.py +1 -2
  36. fractal_server/app/schemas/v2/job.py +1 -1
  37. fractal_server/app/schemas/v2/manifest.py +25 -1
  38. fractal_server/app/schemas/v2/project.py +1 -1
  39. fractal_server/app/schemas/v2/task.py +95 -36
  40. fractal_server/app/schemas/v2/task_collection.py +8 -6
  41. fractal_server/app/schemas/v2/task_group.py +85 -0
  42. fractal_server/app/schemas/v2/workflow.py +7 -2
  43. fractal_server/app/schemas/v2/workflowtask.py +9 -6
  44. fractal_server/app/security/__init__.py +8 -1
  45. fractal_server/config.py +8 -28
  46. fractal_server/data_migrations/2_7_0.py +323 -0
  47. fractal_server/images/models.py +2 -4
  48. fractal_server/main.py +1 -1
  49. fractal_server/migrations/versions/034a469ec2eb_task_groups.py +184 -0
  50. fractal_server/ssh/_fabric.py +186 -73
  51. fractal_server/string_tools.py +6 -2
  52. fractal_server/tasks/utils.py +19 -5
  53. fractal_server/tasks/v1/_TaskCollectPip.py +1 -1
  54. fractal_server/tasks/v1/background_operations.py +5 -5
  55. fractal_server/tasks/v1/get_collection_data.py +2 -2
  56. fractal_server/tasks/v2/_venv_pip.py +67 -70
  57. fractal_server/tasks/v2/background_operations.py +180 -69
  58. fractal_server/tasks/v2/background_operations_ssh.py +57 -70
  59. fractal_server/tasks/v2/database_operations.py +44 -0
  60. fractal_server/tasks/v2/endpoint_operations.py +104 -116
  61. fractal_server/tasks/v2/templates/_1_create_venv.sh +9 -5
  62. fractal_server/tasks/v2/templates/{_2_upgrade_pip.sh → _2_preliminary_pip_operations.sh} +1 -0
  63. fractal_server/tasks/v2/utils.py +5 -0
  64. fractal_server/utils.py +3 -2
  65. {fractal_server-2.6.4.dist-info → fractal_server-2.7.0.dist-info}/METADATA +3 -7
  66. {fractal_server-2.6.4.dist-info → fractal_server-2.7.0.dist-info}/RECORD +69 -60
  67. fractal_server/app/routes/auth/group_names.py +0 -34
  68. fractal_server/tasks/v2/_TaskCollectPip.py +0 -132
  69. {fractal_server-2.6.4.dist-info → fractal_server-2.7.0.dist-info}/LICENSE +0 -0
  70. {fractal_server-2.6.4.dist-info → fractal_server-2.7.0.dist-info}/WHEEL +0 -0
  71. {fractal_server-2.6.4.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
- task_pkg: _TaskCollectPip,
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
- task_pkg:
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 task_pkg.dict(exclude={"package_manifest"}).items():
145
- logger.debug(f"task_pkg.{key}: {value}")
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=task_pkg.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__", task_pkg.package_name),
181
- ("__PACKAGE_ENV_DIR__", package_env_dir),
182
- ("__PACKAGE__", task_pkg.package),
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
- # `remove_venv_folder_upon_failure` is set to True only if
214
- # script 1 goes through, which means that the remote folder
215
- # `package_env_dir` did not already exist. If this remote
216
- # folder already existed, then script 1 fails and the boolean
217
- # flag `remove_venv_folder_upon_failure` remains false.
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="_2_upgrade_pip.sh",
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 `package_name`?
243
+ # FIXME SSH: Does this work well for non-canonical names?
262
244
  package_name_pip_show = pkg_attrs.get("package_name")
263
- package_name_task_pkg = task_pkg.package_name
264
- if package_name_pip_show != package_name_task_pkg:
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"{package_name_task_pkg=} but "
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
- with fractal_ssh.sftp().open(manifest_path_remote, "r") as f:
290
- manifest = json.load(f)
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(**manifest)
275
+ pkg_manifest = ManifestV2(**pkg_manifest_dict)
293
276
  logger.info("collecting - manifest is a valid ManifestV2")
294
277
 
295
- # Create new _TaskCollectPip object
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=new_pkg.package_manifest,
308
- package_version=new_pkg.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
- _insert_tasks(task_list=task_list, db=db)
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 {package_env_dir}"
327
+ f"Now delete remote folder {task_group.path}"
341
328
  )
342
329
  fractal_ssh.remove_folder(
343
- folder=package_env_dir,
330
+ folder=task_group.path,
344
331
  safe_root=tasks_base_dir,
345
332
  )
346
333
  logger.info(
347
- f"Deleted remoted folder {package_env_dir}"
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"{package_env_dir}."
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 ._TaskCollectPip import _TaskCollectPip
9
- from .utils import _parse_wheel_filename
10
- from .utils import get_python_interpreter_v2
11
- from fractal_server.app.schemas.v2 import ManifestV2
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 _load_manifest_from_wheel(
48
- path: Path, wheel: ZipFile, logger_name: Optional[str] = None
49
- ) -> ManifestV2:
50
- logger = get_logger(logger_name)
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
- Inspect task package to extract version and manifest
19
+ Make a GET call to PyPI JSON API and get latest *compatible* version.
20
+
21
+ There are three cases:
77
22
 
78
- Note that this only works with wheel files, which have a well-defined
79
- dist-info section. If we need to generalize to to tar.gz archives, we would
80
- need to go and look for `PKG-INFO`.
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
- Args:
83
- path: Path of the package wheel file.
84
- logger_name:
27
+ Ref https://warehouse.pypa.io/api-reference/json.html.
85
28
 
86
- Returns:
87
- A dictionary with keys `pkg_version` and `pkg_manifest`.
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
- logger = get_logger(logger_name)
36
+ url = f"https://pypi.org/pypi/{name}/json"
37
+ hint = f"Hint: specify the required version for '{name}'."
91
38
 
92
- if not path.as_posix().endswith(".whl"):
93
- raise ValueError(
94
- "Only wheel packages are supported in Fractal "
95
- f"(given {path.name})."
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
- # Extract package name and version from wheel filename
99
- _info = _parse_wheel_filename(wheel_filename=path.name)
100
- pkg_version = _info["version"]
101
-
102
- # Read and validate task manifest
103
- logger.debug(f"Now reading manifest for {path.as_posix()}")
104
- with ZipFile(path) as wheel:
105
- pkg_manifest = _load_manifest_from_wheel(
106
- path, wheel, logger_name=logger_name
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 = dict(
111
- pkg_version=pkg_version,
112
- pkg_manifest=pkg_manifest,
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
- def create_package_dir_pip(
118
- *,
119
- task_pkg: _TaskCollectPip,
120
- create: bool = True,
121
- ) -> Path:
122
- """
123
- Create venv folder for a task package and return corresponding Path object
124
- """
125
- settings = Inject(get_settings)
126
- user = FRACTAL_PUBLIC_TASK_SUBDIR
127
- if task_pkg.package_version is None:
128
- raise ValueError(
129
- f"Cannot create venv folder for package `{task_pkg.package}` "
130
- "with `version=None`."
131
- )
132
- package_dir = f"{task_pkg.package_name}{task_pkg.package_version}"
133
- venv_path = settings.FRACTAL_TASKS_DIR / user / package_dir
134
- if create:
135
- venv_path.mkdir(exist_ok=False, parents=True)
136
- return venv_path
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 package folder does not exist
16
- if [ -d "$PACKAGE_ENV_DIR" ]; then
17
- write_log "ERROR: Folder $PACKAGE_ENV_DIR already exists. Exit."
18
- exit 1
19
- fi
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
@@ -15,6 +15,7 @@ VENVPYTHON=${PACKAGE_ENV_DIR}/bin/python
15
15
  # Upgrade pip
16
16
  write_log "START upgrade pip"
17
17
  "$VENVPYTHON" -m pip install "pip<=__FRACTAL_MAX_PIP_VERSION__" --upgrade
18
+ "$VENVPYTHON" -m pip install setuptools
18
19
  write_log "END upgrade pip"
19
20
  echo
20
21
 
@@ -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
- cwd=cwd,
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.6.4
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,<4.0
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.112.0,<0.113.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)