fractal-server 2.8.0__py3-none-any.whl → 2.9.0a0__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 (54) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/v2/__init__.py +3 -3
  3. fractal_server/app/models/v2/task.py +0 -72
  4. fractal_server/app/models/v2/task_group.py +102 -0
  5. fractal_server/app/routes/admin/v1.py +1 -20
  6. fractal_server/app/routes/admin/v2/job.py +1 -20
  7. fractal_server/app/routes/admin/v2/task_group.py +53 -13
  8. fractal_server/app/routes/api/v2/__init__.py +11 -2
  9. fractal_server/app/routes/api/v2/{_aux_functions_task_collection.py → _aux_functions_task_lifecycle.py} +43 -0
  10. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +21 -14
  11. fractal_server/app/routes/api/v2/task_collection.py +26 -51
  12. fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
  13. fractal_server/app/routes/api/v2/task_group.py +83 -14
  14. fractal_server/app/routes/api/v2/task_group_lifecycle.py +221 -0
  15. fractal_server/app/routes/api/v2/workflow.py +1 -1
  16. fractal_server/app/routes/api/v2/workflow_import.py +2 -2
  17. fractal_server/app/routes/aux/_timestamp.py +25 -0
  18. fractal_server/app/schemas/_validators.py +1 -1
  19. fractal_server/app/schemas/v2/__init__.py +3 -2
  20. fractal_server/app/schemas/v2/dataset.py +1 -1
  21. fractal_server/app/schemas/v2/task_collection.py +13 -31
  22. fractal_server/app/schemas/v2/task_group.py +30 -6
  23. fractal_server/migrations/versions/3082479ac4ea_taskgroup_activity_and_venv_info_to_.py +105 -0
  24. fractal_server/ssh/_fabric.py +18 -0
  25. fractal_server/string_tools.py +10 -3
  26. fractal_server/tasks/utils.py +2 -12
  27. fractal_server/tasks/v2/local/__init__.py +3 -0
  28. fractal_server/tasks/v2/local/collect.py +291 -0
  29. fractal_server/tasks/v2/local/deactivate.py +162 -0
  30. fractal_server/tasks/v2/local/reactivate.py +159 -0
  31. fractal_server/tasks/v2/local/utils_local.py +52 -0
  32. fractal_server/tasks/v2/ssh/__init__.py +0 -0
  33. fractal_server/tasks/v2/ssh/collect.py +387 -0
  34. fractal_server/tasks/v2/ssh/deactivate.py +2 -0
  35. fractal_server/tasks/v2/ssh/reactivate.py +2 -0
  36. fractal_server/tasks/v2/templates/{_2_preliminary_pip_operations.sh → 1_create_venv.sh} +6 -7
  37. fractal_server/tasks/v2/templates/{_3_pip_install.sh → 2_pip_install.sh} +8 -1
  38. fractal_server/tasks/v2/templates/{_4_pip_freeze.sh → 3_pip_freeze.sh} +0 -7
  39. fractal_server/tasks/v2/templates/{_5_pip_show.sh → 4_pip_show.sh} +5 -6
  40. fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +10 -0
  41. fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +35 -0
  42. fractal_server/tasks/v2/utils_background.py +42 -103
  43. fractal_server/tasks/v2/utils_templates.py +32 -2
  44. fractal_server/utils.py +4 -2
  45. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0a0.dist-info}/METADATA +2 -2
  46. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0a0.dist-info}/RECORD +50 -39
  47. fractal_server/app/models/v2/collection_state.py +0 -22
  48. fractal_server/tasks/v2/collection_local.py +0 -357
  49. fractal_server/tasks/v2/collection_ssh.py +0 -352
  50. fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -42
  51. /fractal_server/tasks/v2/{database_operations.py → utils_database.py} +0 -0
  52. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0a0.dist-info}/LICENSE +0 -0
  53. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0a0.dist-info}/WHEEL +0 -0
  54. {fractal_server-2.8.0.dist-info → fractal_server-2.9.0a0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,162 @@
1
+ import logging
2
+ import shutil
3
+ import time
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+
7
+ from ..utils_background import add_commit_refresh
8
+ from ..utils_background import fail_and_cleanup
9
+ from ..utils_templates import get_collection_replacements
10
+ from .utils_local import _customize_and_run_template
11
+ from fractal_server.app.db import get_sync_db
12
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
13
+ from fractal_server.app.models.v2 import TaskGroupV2
14
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
15
+ from fractal_server.app.schemas.v2.task_group import TaskGroupActivityStatusV2
16
+ from fractal_server.logger import set_logger
17
+ from fractal_server.tasks.utils import get_log_path
18
+ from fractal_server.tasks.v2.utils_background import get_current_log
19
+ from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
20
+ from fractal_server.utils import get_timestamp
21
+
22
+ LOGGER_NAME = __name__
23
+
24
+
25
+ def deactivate_local(
26
+ *,
27
+ task_group_activity_id: int,
28
+ task_group_id: int,
29
+ ) -> None:
30
+ """
31
+ Deactivate a task group venv.
32
+
33
+ This function is run as a background task, therefore exceptions must be
34
+ handled.
35
+
36
+ Arguments:
37
+ task_group_id:
38
+ task_group_activity_id:
39
+ """
40
+
41
+ with TemporaryDirectory() as tmpdir:
42
+ log_file_path = get_log_path(Path(tmpdir))
43
+ logger = set_logger(
44
+ logger_name=LOGGER_NAME,
45
+ log_file_path=log_file_path,
46
+ )
47
+
48
+ with next(get_sync_db()) as db:
49
+
50
+ # Get main objects from db
51
+ activity = db.get(TaskGroupActivityV2, task_group_activity_id)
52
+ task_group = db.get(TaskGroupV2, task_group_id)
53
+ if activity is None or task_group is None:
54
+ # Use `logging` directly
55
+ logging.error(
56
+ "Cannot find database rows with "
57
+ f"{task_group_id=} and {task_group_activity_id=}:\n"
58
+ f"{task_group=}\n{activity=}. Exit."
59
+ )
60
+ return
61
+
62
+ # Log some info
63
+ logger.debug("START")
64
+
65
+ for key, value in task_group.model_dump().items():
66
+ logger.debug(f"task_group.{key}: {value}")
67
+
68
+ # Check that the (local) task_group venv_path does exist
69
+ if not Path(task_group.venv_path).exists():
70
+ error_msg = f"{task_group.venv_path} does not exist."
71
+ logger.error(error_msg)
72
+ fail_and_cleanup(
73
+ task_group=task_group,
74
+ task_group_activity=activity,
75
+ logger_name=LOGGER_NAME,
76
+ log_file_path=log_file_path,
77
+ exception=FileNotFoundError(error_msg),
78
+ db=db,
79
+ )
80
+ return
81
+
82
+ try:
83
+
84
+ activity.status = TaskGroupActivityStatusV2.ONGOING
85
+ activity = add_commit_refresh(obj=activity, db=db)
86
+ if task_group.pip_freeze is None:
87
+ logger.warning(
88
+ "Recreate pip-freeze information, since "
89
+ f"{task_group.pip_freeze=}. NOTE: this should only "
90
+ "happen for task groups created before 2.9.0."
91
+ )
92
+ # Prepare replacements for templates
93
+ replacements = get_collection_replacements(
94
+ task_group=task_group,
95
+ python_bin="/not/applicable",
96
+ )
97
+
98
+ # Prepare common arguments for _customize_and_run_template
99
+ common_args = dict(
100
+ replacements=replacements,
101
+ script_dir=(
102
+ Path(task_group.path) / SCRIPTS_SUBFOLDER
103
+ ).as_posix(),
104
+ prefix=(
105
+ f"{int(time.time())}_"
106
+ f"{TaskGroupActivityActionV2.DEACTIVATE}_"
107
+ ),
108
+ logger_name=LOGGER_NAME,
109
+ )
110
+ pip_freeze_stdout = _customize_and_run_template(
111
+ template_filename="3_pip_freeze.sh",
112
+ **common_args,
113
+ )
114
+ # Update pip-freeze data
115
+ logger.info("Add pip freeze stdout to TaskGroupV2 - start")
116
+ activity.log = get_current_log(log_file_path)
117
+ activity = add_commit_refresh(obj=activity, db=db)
118
+ task_group.pip_freeze = pip_freeze_stdout
119
+ task_group = add_commit_refresh(obj=task_group, db=db)
120
+ logger.info("Add pip freeze stdout to TaskGroupV2 - end")
121
+
122
+ if task_group.origin == "wheel" and (
123
+ task_group.wheel_path is None
124
+ or not Path(task_group.wheel_path).exists()
125
+ ):
126
+
127
+ logger.error(
128
+ "Cannot find task_group wheel_path with "
129
+ f"{task_group_id=} :\n"
130
+ f"{task_group=}\n. Exit."
131
+ )
132
+ error_msg = f"{task_group.wheel_path} does not exist."
133
+ logger.error(error_msg)
134
+ fail_and_cleanup(
135
+ task_group=task_group,
136
+ task_group_activity=activity,
137
+ logger_name=LOGGER_NAME,
138
+ log_file_path=log_file_path,
139
+ exception=FileNotFoundError(error_msg),
140
+ db=db,
141
+ )
142
+ return
143
+
144
+ # At this point we are sure that venv_path
145
+ # wheel_path and pip_freeze exist
146
+ shutil.rmtree(task_group.venv_path)
147
+
148
+ activity.log = f"All good, {task_group.venv_path} removed."
149
+ activity.status = TaskGroupActivityStatusV2.OK
150
+ activity.timestamp_ended = get_timestamp()
151
+ activity = add_commit_refresh(obj=activity, db=db)
152
+
153
+ except Exception as e:
154
+ fail_and_cleanup(
155
+ task_group=task_group,
156
+ task_group_activity=activity,
157
+ logger_name=LOGGER_NAME,
158
+ log_file_path=log_file_path,
159
+ exception=e,
160
+ db=db,
161
+ )
162
+ return
@@ -0,0 +1,159 @@
1
+ import logging
2
+ import shutil
3
+ import time
4
+ from pathlib import Path
5
+ from tempfile import TemporaryDirectory
6
+
7
+ from ..utils_background import add_commit_refresh
8
+ from ..utils_background import fail_and_cleanup
9
+ from ..utils_templates import get_collection_replacements
10
+ from .utils_local import _customize_and_run_template
11
+ from fractal_server.app.db import get_sync_db
12
+ from fractal_server.app.models.v2 import TaskGroupActivityV2
13
+ from fractal_server.app.models.v2 import TaskGroupV2
14
+ from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
15
+ from fractal_server.app.schemas.v2.task_group import TaskGroupActivityStatusV2
16
+ from fractal_server.logger import set_logger
17
+ from fractal_server.tasks.utils import get_log_path
18
+ from fractal_server.tasks.v2.utils_background import get_current_log
19
+ from fractal_server.tasks.v2.utils_python_interpreter import (
20
+ get_python_interpreter_v2,
21
+ )
22
+ from fractal_server.tasks.v2.utils_templates import SCRIPTS_SUBFOLDER
23
+ from fractal_server.utils import get_timestamp
24
+
25
+
26
+ LOGGER_NAME = __name__
27
+
28
+
29
+ def reactivate_local(
30
+ *,
31
+ task_group_activity_id: int,
32
+ task_group_id: int,
33
+ ) -> None:
34
+ """
35
+ Reactivate a task group venv.
36
+
37
+ This function is run as a background task, therefore exceptions must be
38
+ handled.
39
+
40
+ Arguments:
41
+ task_group_id:
42
+ task_group_activity_id:
43
+ """
44
+
45
+ with TemporaryDirectory() as tmpdir:
46
+ log_file_path = get_log_path(Path(tmpdir))
47
+ logger = set_logger(
48
+ logger_name=LOGGER_NAME,
49
+ log_file_path=log_file_path,
50
+ )
51
+
52
+ with next(get_sync_db()) as db:
53
+
54
+ # Get main objects from db
55
+ activity = db.get(TaskGroupActivityV2, task_group_activity_id)
56
+ task_group = db.get(TaskGroupV2, task_group_id)
57
+ if activity is None or task_group is None:
58
+ # Use `logging` directly
59
+ logging.error(
60
+ "Cannot find database rows with "
61
+ f"{task_group_id=} and {task_group_activity_id=}:\n"
62
+ f"{task_group=}\n{activity=}. Exit."
63
+ )
64
+ return
65
+
66
+ # Log some info
67
+ logger.debug("START")
68
+
69
+ for key, value in task_group.model_dump().items():
70
+ logger.debug(f"task_group.{key}: {value}")
71
+
72
+ # Check that the (local) task_group venv_path does not exist
73
+ if Path(task_group.venv_path).exists():
74
+ error_msg = f"{task_group.venv_path} already exists."
75
+ logger.error(error_msg)
76
+ fail_and_cleanup(
77
+ task_group=task_group,
78
+ task_group_activity=activity,
79
+ logger_name=LOGGER_NAME,
80
+ log_file_path=log_file_path,
81
+ exception=FileExistsError(error_msg),
82
+ db=db,
83
+ )
84
+ return
85
+
86
+ try:
87
+ activity.status = TaskGroupActivityStatusV2.ONGOING
88
+ activity = add_commit_refresh(obj=activity, db=db)
89
+
90
+ # Prepare replacements for templates
91
+ replacements = get_collection_replacements(
92
+ task_group=task_group,
93
+ python_bin=get_python_interpreter_v2(
94
+ python_version=task_group.python_version
95
+ ),
96
+ )
97
+ with open(f"{tmpdir}/pip_freeze.txt", "w") as f:
98
+ f.write(task_group.pip_freeze)
99
+ replacements.append(
100
+ ("__PIP_FREEZE_FILE__", f"{tmpdir}/pip_freeze.txt")
101
+ )
102
+ # Prepare common arguments for `_customize_and_run_template``
103
+ common_args = dict(
104
+ replacements=replacements,
105
+ script_dir=(
106
+ Path(task_group.path) / SCRIPTS_SUBFOLDER
107
+ ).as_posix(),
108
+ prefix=(
109
+ f"{int(time.time())}_"
110
+ f"{TaskGroupActivityActionV2.REACTIVATE}_"
111
+ ),
112
+ logger_name=LOGGER_NAME,
113
+ )
114
+
115
+ logger.debug("start - create venv")
116
+ _customize_and_run_template(
117
+ template_filename="1_create_venv.sh",
118
+ **common_args,
119
+ )
120
+ logger.debug("end - create venv")
121
+ activity.log = get_current_log(log_file_path)
122
+ activity.timestamp_ended = get_timestamp()
123
+ activity = add_commit_refresh(obj=activity, db=db)
124
+
125
+ logger.debug("start - install from pip freeze")
126
+ _customize_and_run_template(
127
+ template_filename="6_pip_install_from_freeze.sh",
128
+ **common_args,
129
+ )
130
+ logger.debug("end - install from pip freeze")
131
+ activity.log = get_current_log(log_file_path)
132
+ activity.status = TaskGroupActivityStatusV2.OK
133
+ activity.timestamp_ended = get_timestamp()
134
+ activity = add_commit_refresh(obj=activity, db=db)
135
+ task_group.active = True
136
+ task_group = add_commit_refresh(obj=task_group, db=db)
137
+ logger.debug("END")
138
+
139
+ except Exception as reactivate_e:
140
+ # Delete corrupted venv_path
141
+ try:
142
+ logger.info(f"Now delete folder {task_group.venv_path}")
143
+ shutil.rmtree(task_group.venv_path)
144
+ logger.info(f"Deleted folder {task_group.venv_path}")
145
+ except Exception as rm_e:
146
+ logger.error(
147
+ "Removing folder failed.\n"
148
+ f"Original error:\n{str(rm_e)}"
149
+ )
150
+
151
+ fail_and_cleanup(
152
+ task_group=task_group,
153
+ task_group_activity=activity,
154
+ logger_name=LOGGER_NAME,
155
+ log_file_path=log_file_path,
156
+ exception=reactivate_e,
157
+ db=db,
158
+ )
159
+ return
@@ -0,0 +1,52 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+
4
+ from fractal_server.logger import get_logger
5
+ from fractal_server.tasks.v2.utils_templates import customize_template
6
+ from fractal_server.utils import execute_command_sync
7
+
8
+
9
+ def _customize_and_run_template(
10
+ template_filename: str,
11
+ replacements: list[tuple[str, str]],
12
+ script_dir: str,
13
+ logger_name: str,
14
+ prefix: Optional[int] = None,
15
+ ) -> str:
16
+ """
17
+ Customize one of the template bash scripts.
18
+
19
+ Args:
20
+ template_filename: Filename of the template file (ends with ".sh").
21
+ replacements: Dictionary of replacements.
22
+ script_dir: Local folder where the script will be placed.
23
+ prefix: Prefix for the script filename.
24
+ """
25
+ logger = get_logger(logger_name=logger_name)
26
+ logger.debug(f"_customize_and_run_template {template_filename} - START")
27
+
28
+ # Prepare name and path of script
29
+ if not template_filename.endswith(".sh"):
30
+ raise ValueError(
31
+ f"Invalid {template_filename=} (it must end with '.sh')."
32
+ )
33
+
34
+ template_filename_stripped = template_filename[:-3]
35
+
36
+ if prefix is not None:
37
+ script_filename = f"{prefix}{template_filename_stripped}"
38
+ else:
39
+ script_filename = template_filename_stripped
40
+ script_path_local = Path(script_dir) / script_filename
41
+ # Read template
42
+ customize_template(
43
+ template_name=template_filename,
44
+ replacements=replacements,
45
+ script_path=script_path_local,
46
+ )
47
+ cmd = f"bash {script_path_local}"
48
+ logger.debug(f"Now run '{cmd}' ")
49
+ stdout = execute_command_sync(command=cmd, logger_name=logger_name)
50
+ logger.debug(f"Standard output of '{cmd}':\n{stdout}")
51
+ logger.debug(f"_customize_and_run_template {template_filename} - END")
52
+ return stdout
File without changes