fractal-server 2.8.0__py3-none-any.whl → 2.9.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/app/db/__init__.py +2 -35
- fractal_server/app/models/v2/__init__.py +3 -3
- fractal_server/app/models/v2/task.py +0 -72
- fractal_server/app/models/v2/task_group.py +113 -0
- fractal_server/app/routes/admin/v1.py +13 -30
- fractal_server/app/routes/admin/v2/__init__.py +4 -0
- fractal_server/app/routes/admin/v2/job.py +13 -24
- fractal_server/app/routes/admin/v2/task.py +13 -0
- fractal_server/app/routes/admin/v2/task_group.py +75 -14
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +267 -0
- fractal_server/app/routes/api/v1/project.py +7 -19
- fractal_server/app/routes/api/v2/__init__.py +11 -2
- fractal_server/app/routes/api/v2/{_aux_functions_task_collection.py → _aux_functions_task_lifecycle.py} +83 -0
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +27 -17
- fractal_server/app/routes/api/v2/submit.py +19 -24
- fractal_server/app/routes/api/v2/task_collection.py +33 -65
- fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
- fractal_server/app/routes/api/v2/task_group.py +86 -14
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +272 -0
- fractal_server/app/routes/api/v2/workflow.py +1 -1
- fractal_server/app/routes/api/v2/workflow_import.py +2 -2
- fractal_server/app/routes/auth/current_user.py +60 -17
- fractal_server/app/routes/auth/group.py +67 -39
- fractal_server/app/routes/auth/users.py +97 -99
- fractal_server/app/routes/aux/__init__.py +20 -0
- fractal_server/app/runner/executors/slurm/_slurm_config.py +0 -17
- fractal_server/app/runner/executors/slurm/ssh/executor.py +49 -204
- fractal_server/app/runner/executors/slurm/sudo/executor.py +26 -109
- fractal_server/app/runner/executors/slurm/utils_executors.py +58 -0
- fractal_server/app/runner/v2/_local_experimental/executor.py +2 -1
- fractal_server/app/schemas/_validators.py +1 -16
- fractal_server/app/schemas/user.py +16 -10
- fractal_server/app/schemas/user_group.py +0 -11
- fractal_server/app/schemas/v1/applyworkflow.py +0 -8
- fractal_server/app/schemas/v1/dataset.py +0 -5
- fractal_server/app/schemas/v1/project.py +0 -5
- fractal_server/app/schemas/v1/state.py +0 -5
- fractal_server/app/schemas/v1/workflow.py +0 -5
- fractal_server/app/schemas/v2/__init__.py +4 -2
- fractal_server/app/schemas/v2/dataset.py +1 -7
- fractal_server/app/schemas/v2/job.py +0 -8
- fractal_server/app/schemas/v2/project.py +0 -5
- fractal_server/app/schemas/v2/task_collection.py +13 -31
- fractal_server/app/schemas/v2/task_group.py +59 -8
- fractal_server/app/schemas/v2/workflow.py +0 -5
- fractal_server/app/security/__init__.py +17 -0
- fractal_server/config.py +61 -59
- fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py +117 -0
- fractal_server/ssh/_fabric.py +156 -83
- fractal_server/string_tools.py +10 -3
- fractal_server/tasks/utils.py +2 -12
- fractal_server/tasks/v2/local/__init__.py +3 -0
- fractal_server/tasks/v2/local/_utils.py +70 -0
- fractal_server/tasks/v2/local/collect.py +291 -0
- fractal_server/tasks/v2/local/deactivate.py +218 -0
- fractal_server/tasks/v2/local/reactivate.py +159 -0
- fractal_server/tasks/v2/ssh/__init__.py +3 -0
- fractal_server/tasks/v2/ssh/_utils.py +87 -0
- fractal_server/tasks/v2/ssh/collect.py +311 -0
- fractal_server/tasks/v2/ssh/deactivate.py +253 -0
- fractal_server/tasks/v2/ssh/reactivate.py +202 -0
- fractal_server/tasks/v2/templates/{_2_preliminary_pip_operations.sh → 1_create_venv.sh} +6 -7
- fractal_server/tasks/v2/templates/{_3_pip_install.sh → 2_pip_install.sh} +8 -1
- fractal_server/tasks/v2/templates/{_4_pip_freeze.sh → 3_pip_freeze.sh} +0 -7
- fractal_server/tasks/v2/templates/{_5_pip_show.sh → 4_pip_show.sh} +5 -6
- fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +10 -0
- fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +35 -0
- fractal_server/tasks/v2/utils_background.py +42 -127
- fractal_server/tasks/v2/utils_templates.py +32 -2
- fractal_server/utils.py +4 -2
- fractal_server/zip_tools.py +21 -4
- {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/METADATA +3 -5
- {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/RECORD +78 -65
- fractal_server/app/models/v2/collection_state.py +0 -22
- fractal_server/tasks/v2/collection_local.py +0 -357
- fractal_server/tasks/v2/collection_ssh.py +0 -352
- fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -42
- /fractal_server/tasks/v2/{database_operations.py → utils_database.py} +0 -0
- {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/entry_points.txt +0 -0
@@ -1,357 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import shutil
|
3
|
-
from pathlib import Path
|
4
|
-
from tempfile import TemporaryDirectory
|
5
|
-
|
6
|
-
from sqlalchemy.orm.attributes import flag_modified
|
7
|
-
|
8
|
-
from .database_operations import create_db_tasks_and_update_task_group
|
9
|
-
from fractal_server.app.db import get_sync_db
|
10
|
-
from fractal_server.app.models.v2 import CollectionStateV2
|
11
|
-
from fractal_server.app.models.v2 import TaskGroupV2
|
12
|
-
from fractal_server.app.schemas.v2 import CollectionStatusV2
|
13
|
-
from fractal_server.app.schemas.v2 import TaskReadV2
|
14
|
-
from fractal_server.app.schemas.v2.manifest import ManifestV2
|
15
|
-
from fractal_server.config import get_settings
|
16
|
-
from fractal_server.logger import get_logger
|
17
|
-
from fractal_server.logger import set_logger
|
18
|
-
from fractal_server.syringe import Inject
|
19
|
-
from fractal_server.tasks.utils import get_log_path
|
20
|
-
from fractal_server.tasks.v2.utils_background import _handle_failure
|
21
|
-
from fractal_server.tasks.v2.utils_background import _prepare_tasks_metadata
|
22
|
-
from fractal_server.tasks.v2.utils_background import _refresh_logs
|
23
|
-
from fractal_server.tasks.v2.utils_background import (
|
24
|
-
_set_collection_state_data_status,
|
25
|
-
)
|
26
|
-
from fractal_server.tasks.v2.utils_background import check_task_files_exist
|
27
|
-
from fractal_server.tasks.v2.utils_package_names import compare_package_names
|
28
|
-
from fractal_server.tasks.v2.utils_python_interpreter import (
|
29
|
-
get_python_interpreter_v2,
|
30
|
-
)
|
31
|
-
from fractal_server.tasks.v2.utils_templates import customize_template
|
32
|
-
from fractal_server.tasks.v2.utils_templates import parse_script_5_stdout
|
33
|
-
from fractal_server.utils import execute_command_sync
|
34
|
-
|
35
|
-
|
36
|
-
def _customize_and_run_template(
|
37
|
-
script_filename: str,
|
38
|
-
replacements: list[tuple[str, str]],
|
39
|
-
script_dir: str,
|
40
|
-
logger_name: str,
|
41
|
-
) -> str:
|
42
|
-
"""
|
43
|
-
Customize one of the template bash scripts.
|
44
|
-
|
45
|
-
Args:
|
46
|
-
script_filename:
|
47
|
-
replacements:
|
48
|
-
script_dir:
|
49
|
-
logger_name:
|
50
|
-
"""
|
51
|
-
logger = get_logger(logger_name)
|
52
|
-
logger.debug(f"_customize_and_run_template {script_filename} - START")
|
53
|
-
|
54
|
-
script_path_local = Path(script_dir) / script_filename
|
55
|
-
# Read template
|
56
|
-
customize_template(
|
57
|
-
template_name=script_filename,
|
58
|
-
replacements=replacements,
|
59
|
-
script_path=script_path_local,
|
60
|
-
)
|
61
|
-
|
62
|
-
cmd = f"bash {script_path_local}"
|
63
|
-
logger.debug(f"Now run '{cmd}' ")
|
64
|
-
|
65
|
-
stdout = execute_command_sync(command=cmd)
|
66
|
-
|
67
|
-
logger.debug(f"Standard output of '{cmd}':\n{stdout}")
|
68
|
-
logger.debug(f"_customize_and_run_template {script_filename} - END")
|
69
|
-
|
70
|
-
return stdout
|
71
|
-
|
72
|
-
|
73
|
-
def collect_package_local(
|
74
|
-
*,
|
75
|
-
state_id: int,
|
76
|
-
task_group: TaskGroupV2,
|
77
|
-
) -> None:
|
78
|
-
"""
|
79
|
-
Collect a task package.
|
80
|
-
|
81
|
-
This function is run as a background task, therefore exceptions must be
|
82
|
-
handled.
|
83
|
-
|
84
|
-
NOTE: by making this function sync, it will run within a thread - due to
|
85
|
-
starlette/fastapi handling of background tasks (see
|
86
|
-
https://github.com/encode/starlette/blob/master/starlette/background.py).
|
87
|
-
|
88
|
-
|
89
|
-
Arguments:
|
90
|
-
state_id:
|
91
|
-
task_group:
|
92
|
-
"""
|
93
|
-
|
94
|
-
# Create the task_group path
|
95
|
-
with TemporaryDirectory() as tmpdir:
|
96
|
-
|
97
|
-
# Setup logger in tmpdir
|
98
|
-
LOGGER_NAME = "task_collection_local"
|
99
|
-
log_file_path = get_log_path(Path(tmpdir))
|
100
|
-
logger = set_logger(
|
101
|
-
logger_name=LOGGER_NAME,
|
102
|
-
log_file_path=log_file_path,
|
103
|
-
)
|
104
|
-
|
105
|
-
# Log some info
|
106
|
-
logger.debug("START")
|
107
|
-
for key, value in task_group.model_dump().items():
|
108
|
-
logger.debug(f"task_group.{key}: {value}")
|
109
|
-
|
110
|
-
# Open a DB session
|
111
|
-
with next(get_sync_db()) as db:
|
112
|
-
|
113
|
-
# Check that the task_group path does not exist
|
114
|
-
if Path(task_group.path).exists():
|
115
|
-
error_msg = f"{task_group.path} already exists."
|
116
|
-
logger.error(error_msg)
|
117
|
-
_handle_failure(
|
118
|
-
state_id=state_id,
|
119
|
-
logger_name=LOGGER_NAME,
|
120
|
-
log_file_path=log_file_path,
|
121
|
-
exception=FileExistsError(error_msg),
|
122
|
-
db=db,
|
123
|
-
task_group_id=task_group.id,
|
124
|
-
)
|
125
|
-
return
|
126
|
-
|
127
|
-
try:
|
128
|
-
# Prepare replacements for task-collection scripts
|
129
|
-
python_bin = get_python_interpreter_v2(
|
130
|
-
python_version=task_group.python_version
|
131
|
-
)
|
132
|
-
install_string = task_group.pip_install_string
|
133
|
-
settings = Inject(get_settings)
|
134
|
-
replacements = [
|
135
|
-
("__PACKAGE_NAME__", task_group.pkg_name),
|
136
|
-
("__TASK_GROUP_DIR__", task_group.path),
|
137
|
-
("__PACKAGE_ENV_DIR__", task_group.venv_path),
|
138
|
-
("__PYTHON__", python_bin),
|
139
|
-
("__INSTALL_STRING__", install_string),
|
140
|
-
(
|
141
|
-
"__FRACTAL_MAX_PIP_VERSION__",
|
142
|
-
settings.FRACTAL_MAX_PIP_VERSION,
|
143
|
-
),
|
144
|
-
(
|
145
|
-
"__PINNED_PACKAGE_LIST__",
|
146
|
-
task_group.pinned_package_versions_string,
|
147
|
-
),
|
148
|
-
]
|
149
|
-
|
150
|
-
common_args = dict(
|
151
|
-
replacements=replacements,
|
152
|
-
script_dir=task_group.path,
|
153
|
-
logger_name=LOGGER_NAME,
|
154
|
-
)
|
155
|
-
|
156
|
-
logger.debug("installing - START")
|
157
|
-
_set_collection_state_data_status(
|
158
|
-
state_id=state_id,
|
159
|
-
new_status=CollectionStatusV2.INSTALLING,
|
160
|
-
logger_name=LOGGER_NAME,
|
161
|
-
db=db,
|
162
|
-
)
|
163
|
-
|
164
|
-
# Create main path for task group
|
165
|
-
Path(task_group.path).mkdir(parents=True)
|
166
|
-
logger.debug(f"Created {task_group.path}")
|
167
|
-
|
168
|
-
# Create venv
|
169
|
-
logger.debug(
|
170
|
-
(f"START - Create python venv {task_group.venv_path}")
|
171
|
-
)
|
172
|
-
cmd = (
|
173
|
-
f"python{task_group.python_version} -m venv "
|
174
|
-
f"{task_group.venv_path} --copies"
|
175
|
-
)
|
176
|
-
stdout = execute_command_sync(command=cmd)
|
177
|
-
logger.debug(
|
178
|
-
(f"END - Create python venv folder {task_group.venv_path}")
|
179
|
-
)
|
180
|
-
_refresh_logs(
|
181
|
-
state_id=state_id,
|
182
|
-
log_file_path=log_file_path,
|
183
|
-
db=db,
|
184
|
-
)
|
185
|
-
# Close db connections before long pip related operations
|
186
|
-
# Warning this expunge all ORM objects.
|
187
|
-
# https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.close
|
188
|
-
db.close()
|
189
|
-
|
190
|
-
stdout = _customize_and_run_template(
|
191
|
-
script_filename="_2_preliminary_pip_operations.sh",
|
192
|
-
**common_args,
|
193
|
-
)
|
194
|
-
_refresh_logs(
|
195
|
-
state_id=state_id,
|
196
|
-
log_file_path=log_file_path,
|
197
|
-
db=db,
|
198
|
-
)
|
199
|
-
stdout = _customize_and_run_template(
|
200
|
-
script_filename="_3_pip_install.sh",
|
201
|
-
**common_args,
|
202
|
-
)
|
203
|
-
_refresh_logs(
|
204
|
-
state_id=state_id,
|
205
|
-
log_file_path=log_file_path,
|
206
|
-
db=db,
|
207
|
-
)
|
208
|
-
stdout_pip_freeze = _customize_and_run_template(
|
209
|
-
script_filename="_4_pip_freeze.sh",
|
210
|
-
**common_args,
|
211
|
-
)
|
212
|
-
logger.debug("installing - END")
|
213
|
-
_refresh_logs(
|
214
|
-
state_id=state_id,
|
215
|
-
log_file_path=log_file_path,
|
216
|
-
db=db,
|
217
|
-
)
|
218
|
-
|
219
|
-
logger.debug("collecting - START")
|
220
|
-
_set_collection_state_data_status(
|
221
|
-
state_id=state_id,
|
222
|
-
new_status=CollectionStatusV2.COLLECTING,
|
223
|
-
logger_name=LOGGER_NAME,
|
224
|
-
db=db,
|
225
|
-
)
|
226
|
-
_refresh_logs(
|
227
|
-
state_id=state_id,
|
228
|
-
log_file_path=log_file_path,
|
229
|
-
db=db,
|
230
|
-
)
|
231
|
-
|
232
|
-
stdout = _customize_and_run_template(
|
233
|
-
script_filename="_5_pip_show.sh",
|
234
|
-
**common_args,
|
235
|
-
)
|
236
|
-
_refresh_logs(
|
237
|
-
state_id=state_id,
|
238
|
-
log_file_path=log_file_path,
|
239
|
-
db=db,
|
240
|
-
)
|
241
|
-
|
242
|
-
pkg_attrs = parse_script_5_stdout(stdout)
|
243
|
-
for key, value in pkg_attrs.items():
|
244
|
-
logger.debug(
|
245
|
-
f"collecting - parsed from pip-show: {key}={value}"
|
246
|
-
)
|
247
|
-
# Check package_name match between pip show and task-group
|
248
|
-
package_name_pip_show = pkg_attrs.get("package_name")
|
249
|
-
package_name_task_group = task_group.pkg_name
|
250
|
-
compare_package_names(
|
251
|
-
pkg_name_pip_show=package_name_pip_show,
|
252
|
-
pkg_name_task_group=package_name_task_group,
|
253
|
-
logger_name=LOGGER_NAME,
|
254
|
-
)
|
255
|
-
# Extract/drop parsed attributes
|
256
|
-
package_name = package_name_task_group
|
257
|
-
python_bin = pkg_attrs.pop("python_bin")
|
258
|
-
package_root_parent = pkg_attrs.pop("package_root_parent")
|
259
|
-
|
260
|
-
# TODO : Use more robust logic to determine `package_root`.
|
261
|
-
# Examples: use `importlib.util.find_spec`, or parse the output
|
262
|
-
# of `pip show --files {package_name}`.
|
263
|
-
package_name_underscore = package_name.replace("-", "_")
|
264
|
-
package_root = (
|
265
|
-
Path(package_root_parent) / package_name_underscore
|
266
|
-
).as_posix()
|
267
|
-
|
268
|
-
# Read and validate manifest file
|
269
|
-
manifest_path = pkg_attrs.pop("manifest_path")
|
270
|
-
logger.info(f"collecting - now loading {manifest_path=}")
|
271
|
-
with open(manifest_path) as json_data:
|
272
|
-
pkg_manifest_dict = json.load(json_data)
|
273
|
-
logger.info(f"collecting - loaded {manifest_path=}")
|
274
|
-
logger.info("collecting - now validating manifest content")
|
275
|
-
pkg_manifest = ManifestV2(**pkg_manifest_dict)
|
276
|
-
logger.info("collecting - validated manifest content")
|
277
|
-
_refresh_logs(
|
278
|
-
state_id=state_id,
|
279
|
-
log_file_path=log_file_path,
|
280
|
-
db=db,
|
281
|
-
)
|
282
|
-
|
283
|
-
logger.info("collecting - _prepare_tasks_metadata - start")
|
284
|
-
task_list = _prepare_tasks_metadata(
|
285
|
-
package_manifest=pkg_manifest,
|
286
|
-
package_version=task_group.version,
|
287
|
-
package_root=Path(package_root),
|
288
|
-
python_bin=Path(python_bin),
|
289
|
-
)
|
290
|
-
check_task_files_exist(task_list=task_list)
|
291
|
-
logger.info("collecting - _prepare_tasks_metadata - end")
|
292
|
-
_refresh_logs(
|
293
|
-
state_id=state_id,
|
294
|
-
log_file_path=log_file_path,
|
295
|
-
db=db,
|
296
|
-
)
|
297
|
-
|
298
|
-
logger.info(
|
299
|
-
"collecting - create_db_tasks_and_update_task_group - "
|
300
|
-
"start"
|
301
|
-
)
|
302
|
-
task_group = create_db_tasks_and_update_task_group(
|
303
|
-
task_list=task_list,
|
304
|
-
task_group_id=task_group.id,
|
305
|
-
db=db,
|
306
|
-
)
|
307
|
-
logger.info(
|
308
|
-
"collecting - create_db_tasks_and_update_task_group - end"
|
309
|
-
)
|
310
|
-
|
311
|
-
logger.debug("collecting - END")
|
312
|
-
|
313
|
-
# Finalize (write metadata to DB)
|
314
|
-
logger.debug("finalising - START")
|
315
|
-
|
316
|
-
_refresh_logs(
|
317
|
-
state_id=state_id,
|
318
|
-
log_file_path=log_file_path,
|
319
|
-
db=db,
|
320
|
-
)
|
321
|
-
collection_state = db.get(CollectionStateV2, state_id)
|
322
|
-
collection_state.data["freeze"] = stdout_pip_freeze
|
323
|
-
collection_state.data["status"] = CollectionStatusV2.OK
|
324
|
-
# FIXME: The `task_list` key is likely not used by any client,
|
325
|
-
# we should consider dropping it
|
326
|
-
task_read_list = [
|
327
|
-
TaskReadV2(**task.model_dump()).dict()
|
328
|
-
for task in task_group.task_list
|
329
|
-
]
|
330
|
-
collection_state.data["task_list"] = task_read_list
|
331
|
-
flag_modified(collection_state, "data")
|
332
|
-
db.commit()
|
333
|
-
logger.debug("finalising - END")
|
334
|
-
logger.debug("END")
|
335
|
-
|
336
|
-
except Exception as collection_e:
|
337
|
-
# Delete corrupted package dir
|
338
|
-
try:
|
339
|
-
logger.info(f"Now delete folder {task_group.path}")
|
340
|
-
|
341
|
-
shutil.rmtree(task_group.path)
|
342
|
-
logger.info(f"Deleted folder {task_group.path}")
|
343
|
-
except Exception as rm_e:
|
344
|
-
logger.error(
|
345
|
-
"Removing folder failed.\n"
|
346
|
-
f"Original error:\n{str(rm_e)}"
|
347
|
-
)
|
348
|
-
|
349
|
-
_handle_failure(
|
350
|
-
state_id=state_id,
|
351
|
-
logger_name=LOGGER_NAME,
|
352
|
-
log_file_path=log_file_path,
|
353
|
-
exception=collection_e,
|
354
|
-
db=db,
|
355
|
-
task_group_id=task_group.id,
|
356
|
-
)
|
357
|
-
return
|