fractal-server 2.7.0a2__py3-none-any.whl → 2.7.0a4__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 (49) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +3 -9
  3. fractal_server/app/models/v2/collection_state.py +1 -0
  4. fractal_server/app/models/v2/task.py +27 -3
  5. fractal_server/app/routes/admin/v2/task.py +5 -13
  6. fractal_server/app/routes/admin/v2/task_group.py +21 -0
  7. fractal_server/app/routes/api/v1/task_collection.py +2 -2
  8. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +75 -2
  9. fractal_server/app/routes/api/v2/task.py +16 -42
  10. fractal_server/app/routes/api/v2/task_collection.py +148 -187
  11. fractal_server/app/routes/api/v2/task_collection_custom.py +31 -58
  12. fractal_server/app/routes/api/v2/task_group.py +25 -1
  13. fractal_server/app/routes/api/v2/workflow.py +18 -34
  14. fractal_server/app/routes/auth/_aux_auth.py +15 -12
  15. fractal_server/app/routes/auth/group.py +46 -23
  16. fractal_server/app/runner/v2/task_interface.py +4 -9
  17. fractal_server/app/schemas/v2/dataset.py +2 -7
  18. fractal_server/app/schemas/v2/dumps.py +1 -1
  19. fractal_server/app/schemas/v2/job.py +1 -1
  20. fractal_server/app/schemas/v2/project.py +1 -1
  21. fractal_server/app/schemas/v2/task.py +5 -5
  22. fractal_server/app/schemas/v2/task_collection.py +8 -6
  23. fractal_server/app/schemas/v2/task_group.py +31 -3
  24. fractal_server/app/schemas/v2/workflow.py +2 -2
  25. fractal_server/app/schemas/v2/workflowtask.py +2 -2
  26. fractal_server/data_migrations/2_7_0.py +1 -11
  27. fractal_server/images/models.py +2 -4
  28. fractal_server/main.py +1 -1
  29. fractal_server/migrations/versions/034a469ec2eb_task_groups.py +184 -0
  30. fractal_server/string_tools.py +6 -2
  31. fractal_server/tasks/v1/_TaskCollectPip.py +1 -1
  32. fractal_server/tasks/v1/background_operations.py +2 -2
  33. fractal_server/tasks/v2/_venv_pip.py +62 -70
  34. fractal_server/tasks/v2/background_operations.py +168 -49
  35. fractal_server/tasks/v2/background_operations_ssh.py +35 -77
  36. fractal_server/tasks/v2/database_operations.py +7 -17
  37. fractal_server/tasks/v2/endpoint_operations.py +0 -134
  38. fractal_server/tasks/v2/templates/_1_create_venv.sh +9 -5
  39. fractal_server/tasks/v2/utils.py +5 -0
  40. fractal_server/utils.py +3 -2
  41. {fractal_server-2.7.0a2.dist-info → fractal_server-2.7.0a4.dist-info}/METADATA +1 -1
  42. {fractal_server-2.7.0a2.dist-info → fractal_server-2.7.0a4.dist-info}/RECORD +45 -48
  43. fractal_server/migrations/versions/742b74e1cc6e_revamp_taskv2_and_taskgroupv2.py +0 -101
  44. fractal_server/migrations/versions/7cf1baae8fb4_task_group_v2.py +0 -66
  45. fractal_server/migrations/versions/df7cc3501bf7_linkusergroup_timestamp_created.py +0 -42
  46. fractal_server/tasks/v2/_TaskCollectPip.py +0 -132
  47. {fractal_server-2.7.0a2.dist-info → fractal_server-2.7.0a4.dist-info}/LICENSE +0 -0
  48. {fractal_server-2.7.0a2.dist-info → fractal_server-2.7.0a4.dist-info}/WHEEL +0 -0
  49. {fractal_server-2.7.0a2.dist-info → fractal_server-2.7.0a4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,184 @@
1
+ """task groups
2
+
3
+ Revision ID: 034a469ec2eb
4
+ Revises: da2cb2ac4255
5
+ Create Date: 2024-10-10 16:14:13.976231
6
+
7
+ """
8
+ from datetime import datetime
9
+ from datetime import timezone
10
+
11
+ import sqlalchemy as sa
12
+ import sqlmodel
13
+ from alembic import op
14
+
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision = "034a469ec2eb"
18
+ down_revision = "da2cb2ac4255"
19
+ branch_labels = None
20
+ depends_on = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ op.create_table(
25
+ "taskgroupv2",
26
+ sa.Column("id", sa.Integer(), nullable=False),
27
+ sa.Column("user_id", sa.Integer(), nullable=False),
28
+ sa.Column("user_group_id", sa.Integer(), nullable=True),
29
+ sa.Column(
30
+ "origin", sqlmodel.sql.sqltypes.AutoString(), nullable=False
31
+ ),
32
+ sa.Column(
33
+ "pkg_name", sqlmodel.sql.sqltypes.AutoString(), nullable=False
34
+ ),
35
+ sa.Column(
36
+ "version", sqlmodel.sql.sqltypes.AutoString(), nullable=True
37
+ ),
38
+ sa.Column(
39
+ "python_version", sqlmodel.sql.sqltypes.AutoString(), nullable=True
40
+ ),
41
+ sa.Column("path", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
42
+ sa.Column(
43
+ "venv_path", sqlmodel.sql.sqltypes.AutoString(), nullable=True
44
+ ),
45
+ sa.Column(
46
+ "wheel_path", sqlmodel.sql.sqltypes.AutoString(), nullable=True
47
+ ),
48
+ sa.Column(
49
+ "pip_extras", sqlmodel.sql.sqltypes.AutoString(), nullable=True
50
+ ),
51
+ sa.Column(
52
+ "pinned_package_versions",
53
+ sa.JSON(),
54
+ server_default="{}",
55
+ nullable=True,
56
+ ),
57
+ sa.Column("active", sa.Boolean(), nullable=False),
58
+ sa.Column(
59
+ "timestamp_created", sa.DateTime(timezone=True), nullable=False
60
+ ),
61
+ sa.ForeignKeyConstraint(
62
+ ["user_group_id"],
63
+ ["usergroup.id"],
64
+ name=op.f("fk_taskgroupv2_user_group_id_usergroup"),
65
+ ),
66
+ sa.ForeignKeyConstraint(
67
+ ["user_id"],
68
+ ["user_oauth.id"],
69
+ name=op.f("fk_taskgroupv2_user_id_user_oauth"),
70
+ ),
71
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_taskgroupv2")),
72
+ )
73
+ with op.batch_alter_table("collectionstatev2", schema=None) as batch_op:
74
+ batch_op.add_column(
75
+ sa.Column("taskgroupv2_id", sa.Integer(), nullable=True)
76
+ )
77
+ batch_op.create_foreign_key(
78
+ batch_op.f("fk_collectionstatev2_taskgroupv2_id_taskgroupv2"),
79
+ "taskgroupv2",
80
+ ["taskgroupv2_id"],
81
+ ["id"],
82
+ )
83
+
84
+ with op.batch_alter_table("linkusergroup", schema=None) as batch_op:
85
+ batch_op.add_column(
86
+ sa.Column(
87
+ "timestamp_created",
88
+ sa.DateTime(timezone=True),
89
+ nullable=False,
90
+ server_default=str(datetime(2000, 1, 1, tzinfo=timezone.utc)),
91
+ )
92
+ )
93
+
94
+ with op.batch_alter_table("taskv2", schema=None) as batch_op:
95
+ batch_op.add_column(
96
+ sa.Column("taskgroupv2_id", sa.Integer(), nullable=True)
97
+ )
98
+ batch_op.add_column(
99
+ sa.Column(
100
+ "category", sqlmodel.sql.sqltypes.AutoString(), nullable=True
101
+ )
102
+ )
103
+ batch_op.add_column(
104
+ sa.Column(
105
+ "modality", sqlmodel.sql.sqltypes.AutoString(), nullable=True
106
+ )
107
+ )
108
+ batch_op.add_column(
109
+ sa.Column(
110
+ "authors", sqlmodel.sql.sqltypes.AutoString(), nullable=True
111
+ )
112
+ )
113
+ batch_op.add_column(
114
+ sa.Column("tags", sa.JSON(), server_default="[]", nullable=False)
115
+ )
116
+ batch_op.alter_column(
117
+ "source", existing_type=sa.VARCHAR(), nullable=True
118
+ )
119
+
120
+ try:
121
+ with op.batch_alter_table("taskv2", schema=None) as batch_op:
122
+ batch_op.drop_constraint("uq_taskv2_source", type_="unique")
123
+ except BaseException as e:
124
+ if op.get_bind().dialect.name != "sqlite":
125
+ raise e
126
+ import sqlite3
127
+ import logging
128
+
129
+ logger = logging.getLogger("alembic.runtime.migration")
130
+ logger.warning(
131
+ f"Using sqlite, with {sqlite3.version=} and "
132
+ f"{sqlite3.sqlite_version=}"
133
+ )
134
+
135
+ logger.warning(
136
+ "Could not drop 'uq_taskv2_source' constraint; this is expected "
137
+ "when the database was created before the naming convention "
138
+ "was added."
139
+ )
140
+ logger.warning(
141
+ "As a workaround, we recreate the constraint before dropping it."
142
+ )
143
+ with op.batch_alter_table("taskv2", schema=None) as batch_op:
144
+ batch_op.create_unique_constraint("uq_taskv2_source", ["source"])
145
+ batch_op.drop_constraint("uq_taskv2_source", type_="unique")
146
+
147
+ with op.batch_alter_table("taskv2", schema=None) as batch_op:
148
+ batch_op.create_foreign_key(
149
+ batch_op.f("fk_taskv2_taskgroupv2_id_taskgroupv2"),
150
+ "taskgroupv2",
151
+ ["taskgroupv2_id"],
152
+ ["id"],
153
+ )
154
+
155
+
156
+ def downgrade() -> None:
157
+ # ### commands auto generated by Alembic - please adjust! ###
158
+ with op.batch_alter_table("taskv2", schema=None) as batch_op:
159
+ batch_op.drop_constraint(
160
+ batch_op.f("fk_taskv2_taskgroupv2_id_taskgroupv2"),
161
+ type_="foreignkey",
162
+ )
163
+ batch_op.create_unique_constraint("uq_taskv2_source", ["source"])
164
+ batch_op.alter_column(
165
+ "source", existing_type=sa.VARCHAR(), nullable=False
166
+ )
167
+ batch_op.drop_column("tags")
168
+ batch_op.drop_column("authors")
169
+ batch_op.drop_column("modality")
170
+ batch_op.drop_column("category")
171
+ batch_op.drop_column("taskgroupv2_id")
172
+
173
+ with op.batch_alter_table("linkusergroup", schema=None) as batch_op:
174
+ batch_op.drop_column("timestamp_created")
175
+
176
+ with op.batch_alter_table("collectionstatev2", schema=None) as batch_op:
177
+ batch_op.drop_constraint(
178
+ batch_op.f("fk_collectionstatev2_taskgroupv2_id_taskgroupv2"),
179
+ type_="foreignkey",
180
+ )
181
+ batch_op.drop_column("taskgroupv2_id")
182
+
183
+ op.drop_table("taskgroupv2")
184
+ # ### end Alembic commands ###
@@ -33,14 +33,18 @@ def sanitize_string(value: str) -> str:
33
33
  return new_value
34
34
 
35
35
 
36
- def slugify_task_name_for_source(task_name: str) -> str:
36
+ def slugify_task_name_for_source_v1(task_name: str) -> str:
37
37
  """
38
38
  NOTE: this function is used upon creation of tasks' sources, therefore
39
39
  for the moment we cannot replace it with its more comprehensive version
40
40
  from `fractal_server.string_tools.sanitize_string`, nor we can remove it.
41
- As 2.3.1, we are renaming it to `slugify_task_name_for_source`, to make
41
+
42
+ As of 2.3.1, we are renaming it to `slugify_task_name_for_source`, to make
42
43
  it clear that it should not be used for other purposes.
43
44
 
45
+ As of 2.7.0, we are renaming it to `slugify_task_name_for_source_v1`, to
46
+ make it clear that it is not used for v2.
47
+
44
48
  Args:
45
49
  task_name:
46
50
 
@@ -77,7 +77,7 @@ class _TaskCollectPip(TaskCollectPipV1):
77
77
  if self.python_version:
78
78
  python_version = f"py{self.python_version}"
79
79
  else:
80
- python_version = "" # FIXME: can we allow this?
80
+ python_version = ""
81
81
 
82
82
  source = ":".join(
83
83
  (
@@ -6,7 +6,7 @@ import json
6
6
  from pathlib import Path
7
7
  from shutil import rmtree as shell_rmtree
8
8
 
9
- from ...string_tools import slugify_task_name_for_source
9
+ from ...string_tools import slugify_task_name_for_source_v1
10
10
  from ..utils import _normalize_package_name
11
11
  from ..utils import get_collection_log
12
12
  from ..utils import get_collection_path
@@ -215,7 +215,7 @@ async def create_package_environment_pip(
215
215
  # Fill in attributes for TaskCreate
216
216
  task_executable = package_root / t.executable
217
217
  cmd = f"{python_bin.as_posix()} {task_executable.as_posix()}"
218
- task_name_slug = slugify_task_name_for_source(t.name)
218
+ task_name_slug = slugify_task_name_for_source_v1(t.name)
219
219
  task_source = f"{task_pkg.package_source}:{task_name_slug}"
220
220
  if not task_executable.exists():
221
221
  raise FileNotFoundError(
@@ -2,17 +2,48 @@ from pathlib import Path
2
2
  from typing import Optional
3
3
 
4
4
  from ..utils import COLLECTION_FREEZE_FILENAME
5
+ from fractal_server.app.models.v2 import TaskGroupV2
5
6
  from fractal_server.config import get_settings
6
7
  from fractal_server.logger import get_logger
7
8
  from fractal_server.syringe import Inject
8
- from fractal_server.tasks.v2._TaskCollectPip import _TaskCollectPip
9
9
  from fractal_server.tasks.v2.utils import get_python_interpreter_v2
10
10
  from fractal_server.utils import execute_command
11
11
 
12
12
 
13
- async def _pip_install(
13
+ async def _init_venv_v2(
14
+ *,
14
15
  venv_path: Path,
15
- task_pkg: _TaskCollectPip,
16
+ python_version: Optional[str] = None,
17
+ logger_name: str,
18
+ ) -> Path:
19
+ """
20
+ Set a virtual environment at `path/venv`
21
+
22
+ Args:
23
+ path : Path
24
+ path to the venv actual directory (not its parent).
25
+ python_version : default=None
26
+ Python version the virtual environment will be based upon
27
+
28
+ Returns:
29
+ python_bin : Path
30
+ path to python interpreter
31
+ """
32
+ logger = get_logger(logger_name)
33
+ logger.debug(f"[_init_venv_v2] {venv_path=}")
34
+ interpreter = get_python_interpreter_v2(python_version=python_version)
35
+ logger.debug(f"[_init_venv_v2] {interpreter=}")
36
+ await execute_command(
37
+ command=f"{interpreter} -m venv {venv_path}",
38
+ logger_name=logger_name,
39
+ )
40
+ python_bin = venv_path / "bin/python"
41
+ logger.debug(f"[_init_venv_v2] {python_bin=}")
42
+ return python_bin
43
+
44
+
45
+ async def _pip_install(
46
+ task_group: TaskGroupV2,
16
47
  logger_name: str,
17
48
  ) -> Path:
18
49
  """
@@ -30,48 +61,40 @@ async def _pip_install(
30
61
 
31
62
  logger = get_logger(logger_name)
32
63
 
33
- pip = venv_path / "venv/bin/pip"
34
-
35
- extras = f"[{task_pkg.package_extras}]" if task_pkg.package_extras else ""
36
-
37
- if task_pkg.is_local_package:
38
- pip_install_str = f"{task_pkg.package_path.as_posix()}{extras}"
39
- else:
40
- version_string = (
41
- f"=={task_pkg.package_version}" if task_pkg.package_version else ""
42
- )
43
- pip_install_str = f"{task_pkg.package_name}{extras}{version_string}"
64
+ python_bin = Path(task_group.venv_path) / "bin/python"
65
+ pip_install_str = task_group.pip_install_string
66
+ logger.info(f"{pip_install_str=}")
44
67
 
45
68
  await execute_command(
46
- cwd=venv_path,
69
+ cwd=Path(task_group.venv_path),
47
70
  command=(
48
- f"{pip} install --upgrade "
71
+ f"{python_bin} -m pip install --upgrade "
49
72
  f"'pip<={settings.FRACTAL_MAX_PIP_VERSION}'"
50
73
  ),
51
74
  logger_name=logger_name,
52
75
  )
53
76
  await execute_command(
54
- cwd=venv_path,
55
- command=f"{pip} install {pip_install_str}",
77
+ cwd=Path(task_group.venv_path),
78
+ command=f"{python_bin} -m pip install {pip_install_str}",
56
79
  logger_name=logger_name,
57
80
  )
58
- if task_pkg.pinned_package_versions:
81
+
82
+ if task_group.pinned_package_versions:
59
83
  for (
60
84
  pinned_pkg_name,
61
85
  pinned_pkg_version,
62
- ) in task_pkg.pinned_package_versions.items():
63
-
86
+ ) in task_group.pinned_package_versions.items():
64
87
  logger.debug(
65
88
  "Specific version required: "
66
89
  f"{pinned_pkg_name}=={pinned_pkg_version}"
67
90
  )
68
91
  logger.debug(
69
92
  "Preliminary check: verify that "
70
- f"{pinned_pkg_version} is already installed"
93
+ f"{pinned_pkg_name} is already installed"
71
94
  )
72
95
  stdout_show = await execute_command(
73
- cwd=venv_path,
74
- command=f"{pip} show {pinned_pkg_name}",
96
+ cwd=Path(task_group.venv_path),
97
+ command=f"{python_bin} -m pip show {pinned_pkg_name}",
75
98
  logger_name=logger_name,
76
99
  )
77
100
  current_version = next(
@@ -87,9 +110,9 @@ async def _pip_install(
87
110
  f"install version {pinned_pkg_version}."
88
111
  )
89
112
  await execute_command(
90
- cwd=venv_path,
113
+ cwd=Path(task_group.venv_path),
91
114
  command=(
92
- f"{pip} install "
115
+ f"{python_bin} -m pip install "
93
116
  f"{pinned_pkg_name}=={pinned_pkg_version}"
94
117
  ),
95
118
  logger_name=logger_name,
@@ -102,8 +125,8 @@ async def _pip_install(
102
125
 
103
126
  # Extract package installation path from `pip show`
104
127
  stdout_show = await execute_command(
105
- cwd=venv_path,
106
- command=f"{pip} show {task_pkg.package_name}",
128
+ cwd=Path(task_group.venv_path),
129
+ command=f"{python_bin} -m pip show {task_group.pkg_name}",
107
130
  logger_name=logger_name,
108
131
  )
109
132
 
@@ -124,58 +147,26 @@ async def _pip_install(
124
147
  # characters with underscore (_) characters, so the .dist-info directory
125
148
  # always has exactly one dash (-) character in its stem, separating the
126
149
  # name and version fields.
127
- package_root = location / (task_pkg.package_name.replace("-", "_"))
150
+ package_root = location / (task_group.pkg_name.replace("-", "_"))
128
151
  logger.debug(f"[_pip install] {location=}")
129
- logger.debug(f"[_pip install] {task_pkg.package_name=}")
152
+ logger.debug(f"[_pip install] {task_group.pkg_name=}")
130
153
  logger.debug(f"[_pip install] {package_root=}")
131
154
 
132
155
  # Run `pip freeze --all` and store its output
133
156
  stdout_freeze = await execute_command(
134
- cwd=venv_path, command=f"{pip} freeze --all", logger_name=logger_name
157
+ cwd=Path(task_group.venv_path),
158
+ command=f"{python_bin} -m pip freeze --all",
159
+ logger_name=logger_name,
135
160
  )
136
- with (venv_path / COLLECTION_FREEZE_FILENAME).open("w") as f:
161
+ with (Path(task_group.path) / COLLECTION_FREEZE_FILENAME).open("w") as f:
137
162
  f.write(stdout_freeze)
138
163
 
139
164
  return package_root
140
165
 
141
166
 
142
- async def _init_venv_v2(
143
- *,
144
- path: Path,
145
- python_version: Optional[str] = None,
146
- logger_name: str,
147
- ) -> Path:
148
- """
149
- Set a virtual environment at `path/venv`
150
-
151
- Args:
152
- path : Path
153
- path to directory in which to set up the virtual environment
154
- python_version : default=None
155
- Python version the virtual environment will be based upon
156
-
157
- Returns:
158
- python_bin : Path
159
- path to python interpreter
160
- """
161
- logger = get_logger(logger_name)
162
- logger.debug(f"[_init_venv] {path=}")
163
- interpreter = get_python_interpreter_v2(python_version=python_version)
164
- logger.debug(f"[_init_venv] {interpreter=}")
165
- await execute_command(
166
- cwd=path,
167
- command=f"{interpreter} -m venv venv",
168
- logger_name=logger_name,
169
- )
170
- python_bin = path / "venv/bin/python"
171
- logger.debug(f"[_init_venv] {python_bin=}")
172
- return python_bin
173
-
174
-
175
167
  async def _create_venv_install_package_pip(
176
168
  *,
177
- task_pkg: _TaskCollectPip,
178
- path: Path,
169
+ task_group: TaskGroupV2,
179
170
  logger_name: str,
180
171
  ) -> tuple[Path, Path]:
181
172
  """
@@ -191,11 +182,12 @@ async def _create_venv_install_package_pip(
191
182
  package_root: the location of the package manifest
192
183
  """
193
184
  python_bin = await _init_venv_v2(
194
- path=path,
195
- python_version=task_pkg.python_version,
185
+ venv_path=Path(task_group.venv_path),
186
+ python_version=task_group.python_version,
196
187
  logger_name=logger_name,
197
188
  )
198
189
  package_root = await _pip_install(
199
- venv_path=path, task_pkg=task_pkg, logger_name=logger_name
190
+ task_group=task_group,
191
+ logger_name=logger_name,
200
192
  )
201
193
  return python_bin, package_root