fractal-server 2.12.1__py3-none-any.whl → 2.13.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 (73) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/security.py +9 -12
  3. fractal_server/app/models/v2/dataset.py +2 -2
  4. fractal_server/app/models/v2/job.py +11 -9
  5. fractal_server/app/models/v2/task.py +2 -3
  6. fractal_server/app/models/v2/task_group.py +6 -2
  7. fractal_server/app/models/v2/workflowtask.py +15 -8
  8. fractal_server/app/routes/admin/v2/task.py +1 -1
  9. fractal_server/app/routes/admin/v2/task_group.py +1 -1
  10. fractal_server/app/routes/api/v2/dataset.py +4 -4
  11. fractal_server/app/routes/api/v2/images.py +11 -11
  12. fractal_server/app/routes/api/v2/project.py +2 -2
  13. fractal_server/app/routes/api/v2/status.py +1 -1
  14. fractal_server/app/routes/api/v2/submit.py +8 -6
  15. fractal_server/app/routes/api/v2/task.py +4 -2
  16. fractal_server/app/routes/api/v2/task_collection.py +3 -2
  17. fractal_server/app/routes/api/v2/task_group.py +2 -2
  18. fractal_server/app/routes/api/v2/workflow.py +3 -3
  19. fractal_server/app/routes/api/v2/workflow_import.py +3 -3
  20. fractal_server/app/routes/api/v2/workflowtask.py +3 -1
  21. fractal_server/app/routes/auth/_aux_auth.py +4 -1
  22. fractal_server/app/routes/auth/current_user.py +3 -5
  23. fractal_server/app/routes/auth/group.py +1 -1
  24. fractal_server/app/routes/auth/users.py +2 -4
  25. fractal_server/app/routes/aux/_runner.py +1 -1
  26. fractal_server/app/routes/aux/validate_user_settings.py +1 -2
  27. fractal_server/app/runner/executors/_job_states.py +13 -0
  28. fractal_server/app/runner/executors/slurm/_slurm_config.py +26 -18
  29. fractal_server/app/runner/executors/slurm/ssh/__init__.py +0 -3
  30. fractal_server/app/runner/executors/slurm/ssh/_executor_wait_thread.py +31 -22
  31. fractal_server/app/runner/executors/slurm/ssh/_slurm_job.py +2 -5
  32. fractal_server/app/runner/executors/slurm/ssh/executor.py +21 -27
  33. fractal_server/app/runner/executors/slurm/sudo/__init__.py +0 -3
  34. fractal_server/app/runner/executors/slurm/sudo/_check_jobs_status.py +1 -2
  35. fractal_server/app/runner/executors/slurm/sudo/_executor_wait_thread.py +37 -47
  36. fractal_server/app/runner/executors/slurm/sudo/executor.py +25 -24
  37. fractal_server/app/runner/v2/__init__.py +0 -9
  38. fractal_server/app/runner/v2/_local/_local_config.py +5 -4
  39. fractal_server/app/runner/v2/_slurm_common/get_slurm_config.py +4 -4
  40. fractal_server/app/runner/v2/_slurm_sudo/__init__.py +2 -2
  41. fractal_server/app/runner/v2/deduplicate_list.py +1 -1
  42. fractal_server/app/runner/v2/runner.py +9 -4
  43. fractal_server/app/runner/v2/task_interface.py +15 -7
  44. fractal_server/app/schemas/_filter_validators.py +6 -3
  45. fractal_server/app/schemas/_validators.py +7 -5
  46. fractal_server/app/schemas/user.py +23 -18
  47. fractal_server/app/schemas/user_group.py +25 -11
  48. fractal_server/app/schemas/user_settings.py +31 -24
  49. fractal_server/app/schemas/v2/dataset.py +48 -35
  50. fractal_server/app/schemas/v2/dumps.py +16 -14
  51. fractal_server/app/schemas/v2/job.py +49 -29
  52. fractal_server/app/schemas/v2/manifest.py +32 -28
  53. fractal_server/app/schemas/v2/project.py +18 -8
  54. fractal_server/app/schemas/v2/task.py +86 -75
  55. fractal_server/app/schemas/v2/task_collection.py +41 -30
  56. fractal_server/app/schemas/v2/task_group.py +39 -20
  57. fractal_server/app/schemas/v2/workflow.py +24 -12
  58. fractal_server/app/schemas/v2/workflowtask.py +63 -61
  59. fractal_server/app/security/__init__.py +1 -1
  60. fractal_server/config.py +32 -25
  61. fractal_server/images/models.py +18 -12
  62. fractal_server/main.py +1 -1
  63. fractal_server/tasks/v2/utils_background.py +1 -1
  64. fractal_server/tasks/v2/utils_database.py +1 -1
  65. {fractal_server-2.12.1.dist-info → fractal_server-2.13.0.dist-info}/METADATA +9 -10
  66. {fractal_server-2.12.1.dist-info → fractal_server-2.13.0.dist-info}/RECORD +69 -72
  67. fractal_server/app/runner/v2/_local_experimental/__init__.py +0 -121
  68. fractal_server/app/runner/v2/_local_experimental/_local_config.py +0 -108
  69. fractal_server/app/runner/v2/_local_experimental/_submit_setup.py +0 -42
  70. fractal_server/app/runner/v2/_local_experimental/executor.py +0 -157
  71. {fractal_server-2.12.1.dist-info → fractal_server-2.13.0.dist-info}/LICENSE +0 -0
  72. {fractal_server-2.12.1.dist-info → fractal_server-2.13.0.dist-info}/WHEEL +0 -0
  73. {fractal_server-2.12.1.dist-info → fractal_server-2.13.0.dist-info}/entry_points.txt +0 -0
@@ -1,121 +0,0 @@
1
- from concurrent.futures.process import BrokenProcessPool
2
- from pathlib import Path
3
- from typing import Optional
4
-
5
- from ....models.v2 import DatasetV2
6
- from ....models.v2 import WorkflowV2
7
- from ...exceptions import JobExecutionError
8
- from ...filenames import SHUTDOWN_FILENAME
9
- from ...set_start_and_last_task_index import set_start_and_last_task_index
10
- from ..runner import execute_tasks_v2
11
- from ._submit_setup import _local_submit_setup
12
- from .executor import FractalProcessPoolExecutor
13
- from fractal_server.images.models import AttributeFiltersType
14
-
15
-
16
- def process_workflow(
17
- *,
18
- workflow: WorkflowV2,
19
- dataset: DatasetV2,
20
- workflow_dir_local: Path,
21
- workflow_dir_remote: Optional[Path] = None,
22
- first_task_index: Optional[int] = None,
23
- last_task_index: Optional[int] = None,
24
- logger_name: str,
25
- job_attribute_filters: AttributeFiltersType,
26
- # Slurm-specific
27
- user_cache_dir: Optional[str] = None,
28
- slurm_user: Optional[str] = None,
29
- slurm_account: Optional[str] = None,
30
- worker_init: Optional[str] = None,
31
- ) -> None:
32
- """
33
- Run a workflow
34
-
35
- This function is responsible for running a workflow on some input data,
36
- saving the output and taking care of any exception raised during the run.
37
-
38
- NOTE: This is the `local_experimental` backend's public interface,
39
- which also works as a reference implementation for other backends.
40
-
41
- Args:
42
- workflow:
43
- The workflow to be run
44
- dataset:
45
- Initial dataset.
46
- workflow_dir_local:
47
- Working directory for this run.
48
- workflow_dir_remote:
49
- Working directory for this run, on the user side. This argument is
50
- present for compatibility with the standard backend interface, but
51
- for the `local` backend it cannot be different from
52
- `workflow_dir_local`.
53
- first_task_index:
54
- Positional index of the first task to execute; if `None`, start
55
- from `0`.
56
- last_task_index:
57
- Positional index of the last task to execute; if `None`, proceed
58
- until the last task.
59
- logger_name: Logger name
60
- slurm_user:
61
- Username to impersonate to run the workflow. This argument is
62
- present for compatibility with the standard backend interface, but
63
- is ignored in the `local` backend.
64
- slurm_account:
65
- SLURM account to use when running the workflow. This argument is
66
- present for compatibility with the standard backend interface, but
67
- is ignored in the `local` backend.
68
- user_cache_dir:
69
- Cache directory of the user who will run the workflow. This
70
- argument is present for compatibility with the standard backend
71
- interface, but is ignored in the `local` backend.
72
- worker_init:
73
- Any additional, usually backend specific, information to be passed
74
- to the backend executor. This argument is present for compatibility
75
- with the standard backend interface, but is ignored in the `local`
76
- backend.
77
-
78
- Raises:
79
- TaskExecutionError: wrapper for errors raised during tasks' execution
80
- (positive exit codes).
81
- JobExecutionError: wrapper for errors raised by the tasks' executors
82
- (negative exit codes).
83
- """
84
-
85
- if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
86
- raise NotImplementedError(
87
- "LocalExperimental backend does not support different directories "
88
- f"{workflow_dir_local=} and {workflow_dir_remote=}"
89
- )
90
-
91
- # Set values of first_task_index and last_task_index
92
- num_tasks = len(workflow.task_list)
93
- first_task_index, last_task_index = set_start_and_last_task_index(
94
- num_tasks,
95
- first_task_index=first_task_index,
96
- last_task_index=last_task_index,
97
- )
98
-
99
- with FractalProcessPoolExecutor(
100
- shutdown_file=workflow_dir_local / SHUTDOWN_FILENAME
101
- ) as executor:
102
- try:
103
- execute_tasks_v2(
104
- wf_task_list=workflow.task_list[
105
- first_task_index : (last_task_index + 1)
106
- ],
107
- dataset=dataset,
108
- executor=executor,
109
- workflow_dir_local=workflow_dir_local,
110
- workflow_dir_remote=workflow_dir_local,
111
- logger_name=logger_name,
112
- submit_setup_call=_local_submit_setup,
113
- job_attribute_filters=job_attribute_filters,
114
- )
115
- except BrokenProcessPool as e:
116
- raise JobExecutionError(
117
- info=(
118
- "Job failed with BrokenProcessPool error, likely due to "
119
- f"an executor shutdown.\nOriginal error:\n{e.args[0]}"
120
- )
121
- )
@@ -1,108 +0,0 @@
1
- """
2
- Submodule to handle the local-backend configuration for a WorkflowTask
3
- """
4
- import json
5
- from pathlib import Path
6
- from typing import Literal
7
- from typing import Optional
8
-
9
- from pydantic import BaseModel
10
- from pydantic import Extra
11
- from pydantic.error_wrappers import ValidationError
12
-
13
- from .....config import get_settings
14
- from .....syringe import Inject
15
- from ....models.v2 import WorkflowTaskV2
16
-
17
-
18
- class LocalBackendConfigError(ValueError):
19
- """
20
- Local-backend configuration error
21
- """
22
-
23
- pass
24
-
25
-
26
- class LocalBackendConfig(BaseModel, extra=Extra.forbid):
27
- """
28
- Specifications of the local-backend configuration
29
-
30
- Attributes:
31
- parallel_tasks_per_job:
32
- Maximum number of tasks to be run in parallel as part of a call to
33
- `FractalProcessPoolExecutor.map`; if `None`, then all tasks will
34
- start at the same time.
35
- """
36
-
37
- parallel_tasks_per_job: Optional[int]
38
-
39
-
40
- def get_default_local_backend_config():
41
- """
42
- Return a default `LocalBackendConfig` configuration object
43
- """
44
- return LocalBackendConfig(parallel_tasks_per_job=None)
45
-
46
-
47
- def get_local_backend_config(
48
- wftask: WorkflowTaskV2,
49
- which_type: Literal["non_parallel", "parallel"],
50
- config_path: Optional[Path] = None,
51
- ) -> LocalBackendConfig:
52
- """
53
- Prepare a `LocalBackendConfig` configuration object
54
-
55
- The sources for `parallel_tasks_per_job` attributes, starting from the
56
- highest-priority one, are
57
-
58
- 1. Properties in `wftask.meta_parallel` or `wftask.meta_non_parallel`
59
- (depending on `which_type`);
60
- 2. The general content of the local-backend configuration file;
61
- 3. The default value (`None`).
62
-
63
- Arguments:
64
- wftask:
65
- WorkflowTaskV2 for which the backend configuration should
66
- be prepared.
67
- config_path:
68
- Path of local-backend configuration file; if `None`, use
69
- `FRACTAL_LOCAL_CONFIG_FILE` variable from settings.
70
-
71
- Returns:
72
- A local-backend configuration object
73
- """
74
-
75
- key = "parallel_tasks_per_job"
76
- default_value = None
77
-
78
- if which_type == "non_parallel":
79
- wftask_meta = wftask.meta_non_parallel
80
- elif which_type == "parallel":
81
- wftask_meta = wftask.meta_parallel
82
- else:
83
- raise ValueError(
84
- "`get_local_backend_config` received an invalid argument"
85
- f" {which_type=}."
86
- )
87
-
88
- if wftask_meta and key in wftask_meta:
89
- parallel_tasks_per_job = wftask_meta[key]
90
- else:
91
- if not config_path:
92
- settings = Inject(get_settings)
93
- config_path = settings.FRACTAL_LOCAL_CONFIG_FILE
94
- if config_path is None:
95
- parallel_tasks_per_job = default_value
96
- else:
97
- with config_path.open("r") as f:
98
- env = json.load(f)
99
- try:
100
- _ = LocalBackendConfig(**env)
101
- except ValidationError as e:
102
- raise LocalBackendConfigError(
103
- f"Error while loading {config_path=}. "
104
- f"Original error:\n{str(e)}"
105
- )
106
-
107
- parallel_tasks_per_job = env.get(key, default_value)
108
- return LocalBackendConfig(parallel_tasks_per_job=parallel_tasks_per_job)
@@ -1,42 +0,0 @@
1
- """
2
- Submodule to define _local_submit_setup
3
- """
4
- from pathlib import Path
5
- from typing import Literal
6
- from typing import Optional
7
-
8
- from ....models.v2 import WorkflowTaskV2
9
- from ._local_config import get_local_backend_config
10
-
11
-
12
- def _local_submit_setup(
13
- *,
14
- wftask: WorkflowTaskV2,
15
- workflow_dir_local: Optional[Path] = None,
16
- workflow_dir_remote: Optional[Path] = None,
17
- which_type: Literal["non_parallel", "parallel"],
18
- ) -> dict[str, object]:
19
- """
20
- Collect WorfklowTask-specific configuration parameters from different
21
- sources, and inject them for execution.
22
-
23
- Arguments:
24
- wftask:
25
- WorkflowTask for which the configuration is to be assembled
26
- workflow_dir_local:
27
- Not used in this function.
28
- workflow_dir_remote:
29
- Not used in this function.
30
-
31
- Returns:
32
- submit_setup_dict:
33
- A dictionary that will be passed on to
34
- `FractalProcessPoolExecutor.submit` and
35
- `FractalProcessPoolExecutor.map`, so as to set extra options.
36
- """
37
-
38
- local_backend_config = get_local_backend_config(
39
- wftask=wftask, which_type=which_type
40
- )
41
-
42
- return dict(local_backend_config=local_backend_config)
@@ -1,157 +0,0 @@
1
- """
2
- Custom version of Python
3
- [ProcessPoolExecutor](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor)).
4
- """
5
- import multiprocessing as mp
6
- import threading
7
- import time
8
- from concurrent.futures import ProcessPoolExecutor
9
- from concurrent.futures.process import BrokenProcessPool
10
- from pathlib import Path
11
- from typing import Callable
12
- from typing import Iterable
13
- from typing import Optional
14
- from typing import Sequence
15
-
16
- import psutil
17
-
18
- from ._local_config import get_default_local_backend_config
19
- from ._local_config import LocalBackendConfig
20
- from fractal_server.app.runner.exceptions import JobExecutionError
21
- from fractal_server.logger import set_logger
22
-
23
- logger = set_logger("FractalProcessPoolExecutor")
24
-
25
-
26
- class FractalProcessPoolExecutor(ProcessPoolExecutor):
27
-
28
- shutdown_file: Path
29
- interval: float
30
- _shutdown: bool
31
- _shutdown_file_thread: threading.Thread
32
-
33
- def __init__(
34
- self, shutdown_file: Path, interval: float = 1.0, *args, **kwargs
35
- ):
36
- super().__init__(*args, **kwargs, mp_context=mp.get_context("spawn"))
37
- self.shutdown_file = Path(shutdown_file)
38
- self.interval = float(interval)
39
- logger.debug(
40
- f"Start monitoring {shutdown_file} every {interval} seconds"
41
- )
42
- self._shutdown = False
43
- self._shutdown_file_thread = threading.Thread(
44
- target=self._run, daemon=True
45
- )
46
- self._shutdown_file_thread.start()
47
-
48
- def _run(self):
49
- """
50
- Running on '_shutdown_file_thread'.
51
- """
52
- while True:
53
- if self.shutdown_file.exists() or self._shutdown:
54
- try:
55
- self._terminate_processes()
56
- except Exception as e:
57
- logger.error(
58
- "Terminate processes failed. "
59
- f"Original error: {str(e)}."
60
- )
61
- finally:
62
- return
63
- time.sleep(self.interval)
64
-
65
- def _terminate_processes(self):
66
- """
67
- Running on '_shutdown_file_thread'.
68
- """
69
-
70
- logger.info("Start terminating FractalProcessPoolExecutor processes.")
71
- # We use 'psutil' in order to easily access the PIDs of the children.
72
- if self._processes is not None:
73
- for pid in self._processes.keys():
74
- parent = psutil.Process(pid)
75
- children = parent.children(recursive=True)
76
- for child in children:
77
- child.kill()
78
- parent.kill()
79
- logger.info(f"Process {pid} and its children terminated.")
80
- logger.info("FractalProcessPoolExecutor processes terminated.")
81
-
82
- def shutdown(self, *args, **kwargs) -> None:
83
- self._shutdown = True
84
- self._shutdown_file_thread.join()
85
- return super().shutdown(*args, **kwargs)
86
-
87
- def submit(
88
- self,
89
- *args,
90
- local_backend_config: Optional[LocalBackendConfig] = None,
91
- **kwargs,
92
- ):
93
- """
94
- Compared to the `ProcessPoolExecutor` method, here we accept an
95
- additional keyword argument (`local_backend_config`), which is then
96
- simply ignored.
97
- """
98
- return super().submit(*args, **kwargs)
99
-
100
- def map(
101
- self,
102
- fn: Callable,
103
- *iterables: Sequence[Iterable],
104
- local_backend_config: Optional[LocalBackendConfig] = None,
105
- ):
106
- """
107
- Custom version of the `Executor.map` method
108
-
109
- The main change with the respect to the original `map` method is that
110
- the list of tasks to be executed is split into chunks, and then
111
- `super().map` is called (sequentially) on each chunk. The goal of this
112
- change is to limit parallelism, e.g. due to limited computational
113
- resources.
114
-
115
- Other changes from the `concurrent.futures` `map` method:
116
-
117
- 1. Removed `timeout` argument;
118
- 2. Removed `chunksize`;
119
- 3. All iterators (both inputs and output ones) are transformed into
120
- lists.
121
-
122
- Args:
123
- fn: A callable function.
124
- iterables: The argument iterables (one iterable per argument of
125
- `fn`).
126
- local_backend_config: The backend configuration, needed to extract
127
- `parallel_tasks_per_job`.
128
- """
129
- # Preliminary check
130
- iterable_lengths = [len(it) for it in iterables]
131
- if not len(set(iterable_lengths)) == 1:
132
- raise ValueError("Iterables have different lengths.")
133
- # Set total number of arguments
134
- n_elements = len(iterables[0])
135
-
136
- # Set parallel_tasks_per_job
137
- if local_backend_config is None:
138
- local_backend_config = get_default_local_backend_config()
139
- parallel_tasks_per_job = local_backend_config.parallel_tasks_per_job
140
- if parallel_tasks_per_job is None:
141
- parallel_tasks_per_job = n_elements
142
-
143
- # Execute tasks, in chunks of size parallel_tasks_per_job
144
- results = []
145
- for ind_chunk in range(0, n_elements, parallel_tasks_per_job):
146
- chunk_iterables = [
147
- it[ind_chunk : ind_chunk + parallel_tasks_per_job] # noqa
148
- for it in iterables
149
- ]
150
- map_iter = super().map(fn, *chunk_iterables)
151
-
152
- try:
153
- results.extend(list(map_iter))
154
- except BrokenProcessPool as e:
155
- raise JobExecutionError(info=e.args[0])
156
-
157
- return iter(results)