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.
Files changed (82) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/db/__init__.py +2 -35
  3. fractal_server/app/models/v2/__init__.py +3 -3
  4. fractal_server/app/models/v2/task.py +0 -72
  5. fractal_server/app/models/v2/task_group.py +113 -0
  6. fractal_server/app/routes/admin/v1.py +13 -30
  7. fractal_server/app/routes/admin/v2/__init__.py +4 -0
  8. fractal_server/app/routes/admin/v2/job.py +13 -24
  9. fractal_server/app/routes/admin/v2/task.py +13 -0
  10. fractal_server/app/routes/admin/v2/task_group.py +75 -14
  11. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +267 -0
  12. fractal_server/app/routes/api/v1/project.py +7 -19
  13. fractal_server/app/routes/api/v2/__init__.py +11 -2
  14. fractal_server/app/routes/api/v2/{_aux_functions_task_collection.py → _aux_functions_task_lifecycle.py} +83 -0
  15. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +27 -17
  16. fractal_server/app/routes/api/v2/submit.py +19 -24
  17. fractal_server/app/routes/api/v2/task_collection.py +33 -65
  18. fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
  19. fractal_server/app/routes/api/v2/task_group.py +86 -14
  20. fractal_server/app/routes/api/v2/task_group_lifecycle.py +272 -0
  21. fractal_server/app/routes/api/v2/workflow.py +1 -1
  22. fractal_server/app/routes/api/v2/workflow_import.py +2 -2
  23. fractal_server/app/routes/auth/current_user.py +60 -17
  24. fractal_server/app/routes/auth/group.py +67 -39
  25. fractal_server/app/routes/auth/users.py +97 -99
  26. fractal_server/app/routes/aux/__init__.py +20 -0
  27. fractal_server/app/runner/executors/slurm/_slurm_config.py +0 -17
  28. fractal_server/app/runner/executors/slurm/ssh/executor.py +49 -204
  29. fractal_server/app/runner/executors/slurm/sudo/executor.py +26 -109
  30. fractal_server/app/runner/executors/slurm/utils_executors.py +58 -0
  31. fractal_server/app/runner/v2/_local_experimental/executor.py +2 -1
  32. fractal_server/app/schemas/_validators.py +1 -16
  33. fractal_server/app/schemas/user.py +16 -10
  34. fractal_server/app/schemas/user_group.py +0 -11
  35. fractal_server/app/schemas/v1/applyworkflow.py +0 -8
  36. fractal_server/app/schemas/v1/dataset.py +0 -5
  37. fractal_server/app/schemas/v1/project.py +0 -5
  38. fractal_server/app/schemas/v1/state.py +0 -5
  39. fractal_server/app/schemas/v1/workflow.py +0 -5
  40. fractal_server/app/schemas/v2/__init__.py +4 -2
  41. fractal_server/app/schemas/v2/dataset.py +1 -7
  42. fractal_server/app/schemas/v2/job.py +0 -8
  43. fractal_server/app/schemas/v2/project.py +0 -5
  44. fractal_server/app/schemas/v2/task_collection.py +13 -31
  45. fractal_server/app/schemas/v2/task_group.py +59 -8
  46. fractal_server/app/schemas/v2/workflow.py +0 -5
  47. fractal_server/app/security/__init__.py +17 -0
  48. fractal_server/config.py +61 -59
  49. fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py +117 -0
  50. fractal_server/ssh/_fabric.py +156 -83
  51. fractal_server/string_tools.py +10 -3
  52. fractal_server/tasks/utils.py +2 -12
  53. fractal_server/tasks/v2/local/__init__.py +3 -0
  54. fractal_server/tasks/v2/local/_utils.py +70 -0
  55. fractal_server/tasks/v2/local/collect.py +291 -0
  56. fractal_server/tasks/v2/local/deactivate.py +218 -0
  57. fractal_server/tasks/v2/local/reactivate.py +159 -0
  58. fractal_server/tasks/v2/ssh/__init__.py +3 -0
  59. fractal_server/tasks/v2/ssh/_utils.py +87 -0
  60. fractal_server/tasks/v2/ssh/collect.py +311 -0
  61. fractal_server/tasks/v2/ssh/deactivate.py +253 -0
  62. fractal_server/tasks/v2/ssh/reactivate.py +202 -0
  63. fractal_server/tasks/v2/templates/{_2_preliminary_pip_operations.sh → 1_create_venv.sh} +6 -7
  64. fractal_server/tasks/v2/templates/{_3_pip_install.sh → 2_pip_install.sh} +8 -1
  65. fractal_server/tasks/v2/templates/{_4_pip_freeze.sh → 3_pip_freeze.sh} +0 -7
  66. fractal_server/tasks/v2/templates/{_5_pip_show.sh → 4_pip_show.sh} +5 -6
  67. fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +10 -0
  68. fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +35 -0
  69. fractal_server/tasks/v2/utils_background.py +42 -127
  70. fractal_server/tasks/v2/utils_templates.py +32 -2
  71. fractal_server/utils.py +4 -2
  72. fractal_server/zip_tools.py +21 -4
  73. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/METADATA +3 -5
  74. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/RECORD +78 -65
  75. fractal_server/app/models/v2/collection_state.py +0 -22
  76. fractal_server/tasks/v2/collection_local.py +0 -357
  77. fractal_server/tasks/v2/collection_ssh.py +0 -352
  78. fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -42
  79. /fractal_server/tasks/v2/{database_operations.py → utils_database.py} +0 -0
  80. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/LICENSE +0 -0
  81. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/WHEEL +0 -0
  82. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0.dist-info}/entry_points.txt +0 -0
@@ -1,352 +0,0 @@
1
- import os
2
- from pathlib import Path
3
- from tempfile import TemporaryDirectory
4
-
5
- from sqlalchemy.orm.attributes import flag_modified
6
-
7
- from .database_operations import create_db_tasks_and_update_task_group
8
- from .utils_background import _handle_failure
9
- from .utils_background import _prepare_tasks_metadata
10
- from .utils_background import _set_collection_state_data_status
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
14
- from fractal_server.app.schemas.v2 import CollectionStatusV2
15
- from fractal_server.app.schemas.v2.manifest import ManifestV2
16
- from fractal_server.config import get_settings
17
- from fractal_server.logger import get_logger
18
- from fractal_server.logger import set_logger
19
- from fractal_server.ssh._fabric import FractalSSH
20
- from fractal_server.syringe import Inject
21
- from fractal_server.tasks.v2.utils_background import _refresh_logs
22
- from fractal_server.tasks.v2.utils_package_names import compare_package_names
23
- from fractal_server.tasks.v2.utils_python_interpreter import (
24
- get_python_interpreter_v2,
25
- )
26
- from fractal_server.tasks.v2.utils_templates import customize_template
27
- from fractal_server.tasks.v2.utils_templates import parse_script_5_stdout
28
-
29
-
30
- def _customize_and_run_template(
31
- *,
32
- template_name: str,
33
- replacements: list[tuple[str, str]],
34
- script_dir: str,
35
- logger_name: str,
36
- fractal_ssh: FractalSSH,
37
- tasks_base_dir: str,
38
- ) -> str:
39
- """
40
- Customize one of the template bash scripts, transfer it to the remote host
41
- via SFTP and then run it via SSH.
42
-
43
- Args:
44
- script_filename:
45
- replacements:
46
- tmpdir:
47
- logger_name:
48
- fractal_ssh:
49
- """
50
- logger = get_logger(logger_name)
51
- logger.debug(f"_customize_and_run_template {template_name} - START")
52
-
53
- script_path_local = Path(script_dir) / template_name
54
-
55
- customize_template(
56
- template_name=template_name,
57
- replacements=replacements,
58
- script_path=script_path_local,
59
- )
60
-
61
- # Transfer script to remote host
62
- script_path_remote = os.path.join(
63
- tasks_base_dir,
64
- f"script_{abs(hash(script_dir))}{template_name}",
65
- )
66
- logger.debug(f"Now transfer {script_path_local=} over SSH.")
67
- fractal_ssh.send_file(
68
- local=script_path_local,
69
- remote=script_path_remote,
70
- )
71
-
72
- # Execute script remotely
73
- cmd = f"bash {script_path_remote}"
74
- logger.debug(f"Now run '{cmd}' over SSH.")
75
- stdout = fractal_ssh.run_command(cmd=cmd)
76
- logger.debug(f"Standard output of '{cmd}':\n{stdout}")
77
-
78
- logger.debug(f"_customize_and_run_template {template_name} - END")
79
- return stdout
80
-
81
-
82
- def collect_package_ssh(
83
- *,
84
- state_id: int,
85
- task_group: TaskGroupV2,
86
- fractal_ssh: FractalSSH,
87
- tasks_base_dir: str,
88
- ) -> None:
89
- """
90
- Collect a task package over SSH
91
-
92
- This function is run as a background task, therefore exceptions must be
93
- handled.
94
-
95
- NOTE: by making this function sync, it will run within a thread - due to
96
- starlette/fastapi handling of background tasks (see
97
- https://github.com/encode/starlette/blob/master/starlette/background.py).
98
-
99
-
100
- Arguments:
101
- state_id:
102
- task_group:
103
- fractal_ssh:
104
- tasks_base_dir:
105
- """
106
-
107
- # Work within a temporary folder, where also logs will be placed
108
- with TemporaryDirectory() as tmpdir:
109
- LOGGER_NAME = "task_collection_ssh"
110
- log_file_path = Path(tmpdir) / "log"
111
- logger = set_logger(
112
- logger_name=LOGGER_NAME,
113
- log_file_path=log_file_path,
114
- )
115
- logger.debug("START")
116
- for key, value in task_group.model_dump().items():
117
- logger.debug(f"task_group.{key}: {value}")
118
-
119
- # `remove_venv_folder_upon_failure` is set to True only if
120
- # script 1 goes through, which means that the remote folder
121
- # `package_env_dir` did not already exist. If this remote
122
- # folder already existed, then script 1 fails and the boolean
123
- # flag `remove_venv_folder_upon_failure` remains false.
124
- remove_venv_folder_upon_failure = False
125
-
126
- # Open a DB session soon, since it is needed for updating `state`
127
- with next(get_sync_db()) as db:
128
- try:
129
- # Prepare replacements for task-collection scripts
130
- python_bin = get_python_interpreter_v2(
131
- python_version=task_group.python_version
132
- )
133
- install_string = task_group.pip_install_string
134
- settings = Inject(get_settings)
135
- replacements = [
136
- ("__PACKAGE_NAME__", task_group.pkg_name),
137
- ("__TASK_GROUP_DIR__", task_group.path),
138
- ("__PACKAGE_ENV_DIR__", task_group.venv_path),
139
- ("__PYTHON__", python_bin),
140
- ("__INSTALL_STRING__", install_string),
141
- (
142
- "__FRACTAL_MAX_PIP_VERSION__",
143
- settings.FRACTAL_MAX_PIP_VERSION,
144
- ),
145
- (
146
- "__PINNED_PACKAGE_LIST__",
147
- task_group.pinned_package_versions_string,
148
- ),
149
- ]
150
-
151
- common_args = dict(
152
- replacements=replacements,
153
- script_dir=tmpdir,
154
- logger_name=LOGGER_NAME,
155
- fractal_ssh=fractal_ssh,
156
- tasks_base_dir=tasks_base_dir,
157
- )
158
-
159
- fractal_ssh.check_connection()
160
-
161
- logger.debug("installing - START")
162
- _set_collection_state_data_status(
163
- state_id=state_id,
164
- new_status=CollectionStatusV2.INSTALLING,
165
- logger_name=LOGGER_NAME,
166
- db=db,
167
- )
168
- _refresh_logs(
169
- state_id=state_id,
170
- log_file_path=log_file_path,
171
- db=db,
172
- )
173
- db.close()
174
- # Create remote folder (note that because of `parents=True` we
175
- # are in the `no error if existing, make parent directories as
176
- # needed` scenario)
177
- fractal_ssh.mkdir(folder=tasks_base_dir, parents=True)
178
-
179
- stdout = _customize_and_run_template(
180
- template_name="_1_create_venv.sh",
181
- **common_args,
182
- )
183
- remove_venv_folder_upon_failure = True
184
- _refresh_logs(
185
- state_id=state_id,
186
- log_file_path=log_file_path,
187
- db=db,
188
- )
189
-
190
- stdout = _customize_and_run_template(
191
- template_name="_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
- template_name="_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
- template_name="_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
- logger.debug("collecting - START")
219
- _set_collection_state_data_status(
220
- state_id=state_id,
221
- new_status=CollectionStatusV2.COLLECTING,
222
- logger_name=LOGGER_NAME,
223
- db=db,
224
- )
225
- _refresh_logs(
226
- state_id=state_id,
227
- log_file_path=log_file_path,
228
- db=db,
229
- )
230
-
231
- stdout = _customize_and_run_template(
232
- template_name="_5_pip_show.sh",
233
- **common_args,
234
- )
235
- pkg_attrs = parse_script_5_stdout(stdout)
236
- for key, value in pkg_attrs.items():
237
- logger.debug(
238
- f"collecting - parsed from pip-show: {key}={value}"
239
- )
240
- # Check package_name match between pip show and task-group
241
- package_name_pip_show = pkg_attrs.get("package_name")
242
- package_name_task_group = task_group.pkg_name
243
- compare_package_names(
244
- pkg_name_pip_show=package_name_pip_show,
245
- pkg_name_task_group=package_name_task_group,
246
- logger_name=LOGGER_NAME,
247
- )
248
-
249
- _refresh_logs(
250
- state_id=state_id,
251
- log_file_path=log_file_path,
252
- db=db,
253
- )
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_remote = pkg_attrs.pop(
259
- "package_root_parent"
260
- )
261
- manifest_path_remote = pkg_attrs.pop("manifest_path")
262
-
263
- # FIXME SSH: Use more robust logic to determine `package_root`.
264
- # Examples: use `importlib.util.find_spec`, or parse the output
265
- # of `pip show --files {package_name}`.
266
- package_name_underscore = package_name.replace("-", "_")
267
- package_root_remote = (
268
- Path(package_root_parent_remote) / package_name_underscore
269
- ).as_posix()
270
-
271
- # Read and validate remote manifest file
272
- pkg_manifest_dict = fractal_ssh.read_remote_json_file(
273
- manifest_path_remote
274
- )
275
- logger.info(f"collecting - loaded {manifest_path_remote=}")
276
- pkg_manifest = ManifestV2(**pkg_manifest_dict)
277
- logger.info("collecting - manifest is a valid ManifestV2")
278
-
279
- logger.info("collecting - _prepare_tasks_metadata - start")
280
- task_list = _prepare_tasks_metadata(
281
- package_manifest=pkg_manifest,
282
- package_version=task_group.version,
283
- package_root=Path(package_root_remote),
284
- python_bin=Path(python_bin),
285
- )
286
- logger.info("collecting - _prepare_tasks_metadata - end")
287
-
288
- logger.info(
289
- "collecting - create_db_tasks_and_update_task_group - "
290
- "start"
291
- )
292
- create_db_tasks_and_update_task_group(
293
- task_list=task_list,
294
- task_group_id=task_group.id,
295
- db=db,
296
- )
297
- logger.info(
298
- "collecting - create_db_tasks_and_update_task_group - end"
299
- )
300
-
301
- logger.debug("collecting - END")
302
- _refresh_logs(
303
- state_id=state_id,
304
- log_file_path=log_file_path,
305
- db=db,
306
- )
307
-
308
- # Finalize (write metadata to DB)
309
- logger.debug("finalising - START")
310
-
311
- collection_state = db.get(CollectionStateV2, state_id)
312
- collection_state.data["log"] = log_file_path.open("r").read()
313
- collection_state.data["freeze"] = stdout_pip_freeze
314
- collection_state.data["status"] = CollectionStatusV2.OK
315
- flag_modified(collection_state, "data")
316
- db.commit()
317
- logger.debug("finalising - END")
318
- logger.debug("END")
319
-
320
- except Exception as collection_e:
321
- # Delete corrupted package dir
322
- if remove_venv_folder_upon_failure:
323
- try:
324
- logger.info(
325
- f"Now delete remote folder {task_group.path}"
326
- )
327
- fractal_ssh.remove_folder(
328
- folder=task_group.path,
329
- safe_root=tasks_base_dir,
330
- )
331
- logger.info(
332
- f"Deleted remoted folder {task_group.path}"
333
- )
334
- except Exception as e_rm:
335
- logger.error(
336
- "Removing folder failed. "
337
- f"Original error:\n{str(e_rm)}"
338
- )
339
- else:
340
- logger.info(
341
- "Not trying to remove remote folder "
342
- f"{task_group.path}."
343
- )
344
- _handle_failure(
345
- state_id=state_id,
346
- log_file_path=log_file_path,
347
- logger_name=LOGGER_NAME,
348
- exception=collection_e,
349
- db=db,
350
- task_group_id=task_group.id,
351
- )
352
- return
@@ -1,42 +0,0 @@
1
- set -e
2
-
3
- write_log(){
4
- TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
5
- echo "[collect-task, $TIMESTAMP] $1"
6
- }
7
-
8
-
9
- # Variables to be filled within fractal-server
10
- TASK_GROUP_DIR=__TASK_GROUP_DIR__
11
- PACKAGE_ENV_DIR=__PACKAGE_ENV_DIR__
12
- PYTHON=__PYTHON__
13
-
14
- TIME_START=$(date +%s)
15
-
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
24
-
25
- write_log "START mkdir -p $PACKAGE_ENV_DIR"
26
- mkdir -p $PACKAGE_ENV_DIR
27
- write_log "END mkdir -p $PACKAGE_ENV_DIR"
28
- echo
29
-
30
-
31
- # Create venv
32
- write_log "START create venv in ${PACKAGE_ENV_DIR}"
33
- "$PYTHON" -m venv "$PACKAGE_ENV_DIR" --copies
34
- write_log "END create venv in ${PACKAGE_ENV_DIR}"
35
- echo
36
-
37
- # End
38
- TIME_END=$(date +%s)
39
- write_log "All good up to here."
40
- write_log "Elapsed: $((TIME_END - TIME_START)) seconds"
41
- write_log "Exit."
42
- echo