ms-flow 0.1.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.
- ms_flow/__init__.py +26 -0
- ms_flow/_api_impl/__init__.py +9 -0
- ms_flow/_api_impl/advanced.py +64 -0
- ms_flow/_api_impl/job_api.py +579 -0
- ms_flow/_api_impl/project_runtime.py +886 -0
- ms_flow/advanced.py +5 -0
- ms_flow/api.py +79 -0
- ms_flow/artifacts.py +329 -0
- ms_flow/core/__init__.py +0 -0
- ms_flow/core/app_settings.py +133 -0
- ms_flow/core/apps.py +173 -0
- ms_flow/core/callable_refs.py +66 -0
- ms_flow/core/configuration.py +365 -0
- ms_flow/core/data/__init__.py +53 -0
- ms_flow/core/data/backends.py +708 -0
- ms_flow/core/data/bridge.py +212 -0
- ms_flow/core/data/contracts.py +534 -0
- ms_flow/core/data/planner.py +75 -0
- ms_flow/core/data/project_output.py +27 -0
- ms_flow/core/data/runtime.py +72 -0
- ms_flow/core/data/shard.py +556 -0
- ms_flow/core/database/__init__.py +33 -0
- ms_flow/core/database/base.py +229 -0
- ms_flow/core/database/executor.py +218 -0
- ms_flow/core/database/executor_models.py +112 -0
- ms_flow/core/database/master.py +168 -0
- ms_flow/core/database/master_models.py +40 -0
- ms_flow/core/database/project.py +678 -0
- ms_flow/core/database/project_models.py +59 -0
- ms_flow/core/database/project_records.py +100 -0
- ms_flow/core/database/project_store_ops.py +958 -0
- ms_flow/core/database/sqlite_utils.py +208 -0
- ms_flow/core/events.py +5 -0
- ms_flow/core/executor/__init__.py +62 -0
- ms_flow/core/executor/backend_selector.py +254 -0
- ms_flow/core/executor/command_inbox.py +40 -0
- ms_flow/core/executor/dispatch_model.py +71 -0
- ms_flow/core/executor/dispatch_service.py +397 -0
- ms_flow/core/executor/executor_registry.py +236 -0
- ms_flow/core/executor/hpc_adapter.py +436 -0
- ms_flow/core/executor/hpc_runtime.py +124 -0
- ms_flow/core/executor/job_monitoring.py +666 -0
- ms_flow/core/executor/job_snapshot.py +124 -0
- ms_flow/core/executor/lifecycle_controller.py +369 -0
- ms_flow/core/executor/local_adapters.py +497 -0
- ms_flow/core/executor/local_compute_scheduler.py +116 -0
- ms_flow/core/executor/local_scheduler.py +169 -0
- ms_flow/core/executor/manager/__init__.py +15 -0
- ms_flow/core/executor/manager/_manager.py +1904 -0
- ms_flow/core/executor/provisioning.py +443 -0
- ms_flow/core/executor/ray_adapter.py +452 -0
- ms_flow/core/executor/resource_manager.py +65 -0
- ms_flow/core/executor/result_handlers.py +696 -0
- ms_flow/core/executor/runner_refs.py +152 -0
- ms_flow/core/executor/runtime_state.py +209 -0
- ms_flow/core/executor/runtime_status_service.py +408 -0
- ms_flow/core/executor/services/__init__.py +0 -0
- ms_flow/core/executor/services/dispatch_pool.py +230 -0
- ms_flow/core/executor/services/event_recorder.py +155 -0
- ms_flow/core/executor/services/feeding.py +274 -0
- ms_flow/core/executor/services/job_commands.py +258 -0
- ms_flow/core/executor/services/job_status.py +481 -0
- ms_flow/core/executor/services/job_store.py +350 -0
- ms_flow/core/executor/services/payload_store.py +166 -0
- ms_flow/core/executor/services/persistence_coordinator.py +240 -0
- ms_flow/core/executor/services/sink_writer_pool.py +122 -0
- ms_flow/core/executor/staging_manager.py +131 -0
- ms_flow/core/executor/submission_service.py +604 -0
- ms_flow/core/executor/utils.py +40 -0
- ms_flow/core/project/__init__.py +21 -0
- ms_flow/core/project/catalog.py +343 -0
- ms_flow/core/project/context.py +45 -0
- ms_flow/core/project/manager.py +193 -0
- ms_flow/core/project/repository.py +264 -0
- ms_flow/core/project/resources.py +150 -0
- ms_flow/core/project/state.py +45 -0
- ms_flow/core/settings/__init__.py +0 -0
- ms_flow/core/settings/defaults.toml +42 -0
- ms_flow/core/settings/manager.py +397 -0
- ms_flow/core/settings/models.py +412 -0
- ms_flow/job_templates.py +171 -0
- ms_flow/logger.py +316 -0
- ms_flow/main.py +164 -0
- ms_flow/progress.py +70 -0
- ms_flow/query.py +255 -0
- ms_flow/runtime.py +208 -0
- ms_flow/selection.py +70 -0
- ms_flow/sinks.py +180 -0
- ms_flow/specs/__init__.py +42 -0
- ms_flow/specs/input.py +260 -0
- ms_flow/specs/output.py +73 -0
- ms_flow/specs/processor.py +30 -0
- ms_flow/specs/workflow.py +176 -0
- ms_flow/tasking.py +771 -0
- ms_flow-0.1.0.dist-info/METADATA +77 -0
- ms_flow-0.1.0.dist-info/RECORD +99 -0
- ms_flow-0.1.0.dist-info/WHEEL +5 -0
- ms_flow-0.1.0.dist-info/licenses/LICENSE +21 -0
- ms_flow-0.1.0.dist-info/top_level.txt +1 -0
ms_flow/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Main MolSuite package.
|
|
2
|
+
|
|
3
|
+
Recommended path:
|
|
4
|
+
- `molsuite.api` for the normal public API
|
|
5
|
+
|
|
6
|
+
Specialised surfaces:
|
|
7
|
+
- `molsuite.tasking` for the full declarative layer
|
|
8
|
+
- `molsuite.query` for query helpers
|
|
9
|
+
- `molsuite.core.*` for internal/advanced extensions
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ms_flow.api import AppRuntime, MolSuite
|
|
13
|
+
from ms_flow.runtime import BaseRuntime
|
|
14
|
+
from ms_flow.selection import MultipleResultsFound, NoResultFound, Selection
|
|
15
|
+
from ms_flow.progress import watch_job, watch_jobs
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AppRuntime",
|
|
19
|
+
"BaseRuntime",
|
|
20
|
+
"MolSuite",
|
|
21
|
+
"MultipleResultsFound",
|
|
22
|
+
"NoResultFound",
|
|
23
|
+
"Selection",
|
|
24
|
+
"watch_job",
|
|
25
|
+
"watch_jobs",
|
|
26
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from ms_flow._api_impl.advanced import MolSuiteAdvancedAccess
|
|
2
|
+
from ms_flow._api_impl.job_api import MolSuiteJobApiMixin
|
|
3
|
+
from ms_flow._api_impl.project_runtime import MolSuiteProjectRuntimeMixin
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"MolSuiteAdvancedAccess",
|
|
7
|
+
"MolSuiteJobApiMixin",
|
|
8
|
+
"MolSuiteProjectRuntimeMixin",
|
|
9
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ms_flow.core.project import ProjectDataContext
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ms_flow.core.database import ExecutorDB, ProjectStore
|
|
9
|
+
from ms_flow.core.executor.manager import ExecutorManager
|
|
10
|
+
from ms_flow.core.project.context import ProjectContext
|
|
11
|
+
from ms_flow.main import MolSuite
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MolSuiteAdvancedAccess:
|
|
15
|
+
"""
|
|
16
|
+
Explicit door to engine internals.
|
|
17
|
+
|
|
18
|
+
The happy path should use `molsuite.api`.
|
|
19
|
+
If explicit typing/imports are needed, the advanced public surface lives in
|
|
20
|
+
`molsuite.advanced.MolSuiteAdvancedAccess`.
|
|
21
|
+
|
|
22
|
+
This adapter exists for advanced scenarios that genuinely need to touch
|
|
23
|
+
`ExecutorManager`, `ProjectStore` or the active runtime's DB handles.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, molsuite: "MolSuite"):
|
|
27
|
+
self._molsuite = molsuite
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def active_context(self) -> "ProjectContext | None":
|
|
31
|
+
return self._molsuite.active_context
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def executor_db(self) -> "ExecutorDB | None":
|
|
35
|
+
return self._molsuite.executor_db
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def executor_manager(self) -> "ExecutorManager | None":
|
|
39
|
+
return self._molsuite.executor_manager
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def project_store(self) -> "ProjectStore":
|
|
43
|
+
self._molsuite._require_runtime()
|
|
44
|
+
store = self._molsuite.project_store
|
|
45
|
+
if store is None:
|
|
46
|
+
raise RuntimeError("ProjectStore is not initialised. Activate a project first.")
|
|
47
|
+
return store
|
|
48
|
+
|
|
49
|
+
def project_data_context(self) -> ProjectDataContext:
|
|
50
|
+
return ProjectDataContext(
|
|
51
|
+
molsuite=self._molsuite,
|
|
52
|
+
active_context=self._molsuite.active_context,
|
|
53
|
+
project_store_handle=self.project_store,
|
|
54
|
+
project_resources=self._molsuite.get_project_resource_map(),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def entity_loader_context(self) -> ProjectDataContext:
|
|
58
|
+
return self.project_data_context()
|
|
59
|
+
|
|
60
|
+
def runtime_healthcheck(self) -> dict[str, object]:
|
|
61
|
+
return self._molsuite.get_runtime_healthcheck()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = ["MolSuiteAdvancedAccess"]
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import time
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from ms_flow.core.data import TableOutputSpec
|
|
9
|
+
from ms_flow.core.executor.dispatch_model import DispatchPolicy
|
|
10
|
+
from ms_flow.core.executor.submission_service import executor_is_supported
|
|
11
|
+
from ms_flow.core.executor.job_snapshot import JobSnapshot
|
|
12
|
+
from ms_flow.job_templates import batch_job, streaming_job
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ms_flow.tasking import JobDefinition, TaskDefinition
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_OPERATIONAL_PROFILE_PRESETS: dict[str, dict[str, Any]] = {
|
|
19
|
+
"strict": {
|
|
20
|
+
"max_inflight_tasks": 8,
|
|
21
|
+
"max_inflight_items": 128,
|
|
22
|
+
"output_flush_every": 100,
|
|
23
|
+
},
|
|
24
|
+
"balanced": {
|
|
25
|
+
"max_inflight_tasks": None,
|
|
26
|
+
"max_inflight_items": None,
|
|
27
|
+
"output_flush_every": None,
|
|
28
|
+
},
|
|
29
|
+
"throughput": {
|
|
30
|
+
"max_inflight_tasks": 64,
|
|
31
|
+
"max_inflight_items": 2048,
|
|
32
|
+
"output_flush_every": 1000,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _normalize_operational_profile(value: Any) -> str:
|
|
38
|
+
profile = str(value or "balanced").strip().lower()
|
|
39
|
+
if profile not in _OPERATIONAL_PROFILE_PRESETS:
|
|
40
|
+
raise ValueError("operational_profile must be 'strict', 'balanced' or 'throughput'.")
|
|
41
|
+
return profile
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _has_inline_workflow_arguments(
|
|
45
|
+
*,
|
|
46
|
+
name: str | None,
|
|
47
|
+
input: Any,
|
|
48
|
+
process: Callable[..., Any] | Any | None,
|
|
49
|
+
output: Any,
|
|
50
|
+
description: str,
|
|
51
|
+
task_name: str | None,
|
|
52
|
+
setup: Optional[Callable[..., Any]],
|
|
53
|
+
stage: Optional[Callable[..., Any]],
|
|
54
|
+
finalize: Optional[Callable[..., Any]],
|
|
55
|
+
result_handler: Optional[Callable[..., Any]],
|
|
56
|
+
flush_every: int,
|
|
57
|
+
executor: str,
|
|
58
|
+
supported_executors,
|
|
59
|
+
cpu_required: int | None,
|
|
60
|
+
stage_fail_policy: str,
|
|
61
|
+
max_stage_failures: int,
|
|
62
|
+
store_results: bool,
|
|
63
|
+
tags,
|
|
64
|
+
) -> bool:
|
|
65
|
+
return any(
|
|
66
|
+
(
|
|
67
|
+
name is not None,
|
|
68
|
+
input is not None,
|
|
69
|
+
process is not None,
|
|
70
|
+
output is not None,
|
|
71
|
+
bool(description),
|
|
72
|
+
task_name is not None,
|
|
73
|
+
setup is not None,
|
|
74
|
+
stage is not None,
|
|
75
|
+
finalize is not None,
|
|
76
|
+
result_handler is not None,
|
|
77
|
+
flush_every != 500,
|
|
78
|
+
bool(str(executor or "").strip()),
|
|
79
|
+
bool(tuple(supported_executors or ())),
|
|
80
|
+
cpu_required is not None,
|
|
81
|
+
stage_fail_policy != "fail_fast",
|
|
82
|
+
int(max_stage_failures) != 0,
|
|
83
|
+
bool(store_results),
|
|
84
|
+
bool(tuple(tags or ())),
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class MolSuiteJobApiMixin:
|
|
90
|
+
def run(
|
|
91
|
+
self,
|
|
92
|
+
spec=None,
|
|
93
|
+
*,
|
|
94
|
+
name: str | None = None,
|
|
95
|
+
input=None,
|
|
96
|
+
process: Callable[..., Any] | Any | None = None,
|
|
97
|
+
output=None,
|
|
98
|
+
description: str = "",
|
|
99
|
+
task_name: str | None = None,
|
|
100
|
+
setup: Optional[Callable[..., Any]] = None,
|
|
101
|
+
stage: Optional[Callable[..., Any]] = None,
|
|
102
|
+
finalize: Optional[Callable[..., Any]] = None,
|
|
103
|
+
result_handler: Optional[Callable[..., Any]] = None,
|
|
104
|
+
flush_every: int = 500,
|
|
105
|
+
executor: str = "",
|
|
106
|
+
supported_executors=(),
|
|
107
|
+
cpu_required: int | None = None,
|
|
108
|
+
stage_fail_policy: str = "fail_fast",
|
|
109
|
+
max_stage_failures: int = 0,
|
|
110
|
+
store_results: bool = False,
|
|
111
|
+
tags=(),
|
|
112
|
+
params: dict[str, Any] | None = None,
|
|
113
|
+
config: dict[str, Any] | None = None,
|
|
114
|
+
queue_policy: str = "fifo",
|
|
115
|
+
priority: int = 0,
|
|
116
|
+
) -> str:
|
|
117
|
+
from ms_flow.specs.workflow import WorkflowLauncher, WorkflowSpec, workflow
|
|
118
|
+
|
|
119
|
+
if spec is None:
|
|
120
|
+
if input is None or process is None:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
"run(...) requires spec=... or the triple input=..., process=..., output=...."
|
|
123
|
+
)
|
|
124
|
+
resolved_name = str(name or "").strip()
|
|
125
|
+
if not resolved_name:
|
|
126
|
+
raise ValueError("run(name=...) cannot be empty when building the workflow inline.")
|
|
127
|
+
spec = workflow(
|
|
128
|
+
name=resolved_name,
|
|
129
|
+
input=input,
|
|
130
|
+
process=process,
|
|
131
|
+
output=output,
|
|
132
|
+
description=description,
|
|
133
|
+
task_name=task_name,
|
|
134
|
+
setup=setup,
|
|
135
|
+
stage=stage,
|
|
136
|
+
finalize=finalize,
|
|
137
|
+
result_handler=result_handler,
|
|
138
|
+
flush_every=flush_every,
|
|
139
|
+
executor=executor,
|
|
140
|
+
supported_executors=tuple(supported_executors or ()),
|
|
141
|
+
cpu_required=cpu_required,
|
|
142
|
+
stage_fail_policy=stage_fail_policy,
|
|
143
|
+
max_stage_failures=max_stage_failures,
|
|
144
|
+
store_results=store_results,
|
|
145
|
+
tags=tuple(tags or ()),
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
if _has_inline_workflow_arguments(
|
|
149
|
+
name=name,
|
|
150
|
+
input=input,
|
|
151
|
+
process=process,
|
|
152
|
+
output=output,
|
|
153
|
+
description=description,
|
|
154
|
+
task_name=task_name,
|
|
155
|
+
setup=setup,
|
|
156
|
+
stage=stage,
|
|
157
|
+
finalize=finalize,
|
|
158
|
+
result_handler=result_handler,
|
|
159
|
+
flush_every=flush_every,
|
|
160
|
+
executor=executor,
|
|
161
|
+
supported_executors=supported_executors,
|
|
162
|
+
cpu_required=cpu_required,
|
|
163
|
+
stage_fail_policy=stage_fail_policy,
|
|
164
|
+
max_stage_failures=max_stage_failures,
|
|
165
|
+
store_results=store_results,
|
|
166
|
+
tags=tags,
|
|
167
|
+
):
|
|
168
|
+
raise ValueError(
|
|
169
|
+
"run(spec=...) does not accept inline workflow parameters. "
|
|
170
|
+
"Use spec=... alone, or the simple path name=..., input=..., process=..., output=...."
|
|
171
|
+
)
|
|
172
|
+
if not isinstance(spec, WorkflowSpec):
|
|
173
|
+
raise TypeError(
|
|
174
|
+
"run(spec=...) requires a WorkflowSpec. "
|
|
175
|
+
"For a JobDefinition use job_def.submit(...), job_def.run(...) or submit_job(job_def, ...)."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return WorkflowLauncher(self).submit(
|
|
179
|
+
spec,
|
|
180
|
+
params=params,
|
|
181
|
+
config=config,
|
|
182
|
+
queue_policy=queue_policy,
|
|
183
|
+
priority=priority,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def task(self, name: str, **kwargs):
|
|
187
|
+
"""Decorator to define a task."""
|
|
188
|
+
|
|
189
|
+
def decorator(func):
|
|
190
|
+
from ms_flow.tasking import task
|
|
191
|
+
|
|
192
|
+
t = task(name=name, handler=func, **kwargs)
|
|
193
|
+
return t
|
|
194
|
+
|
|
195
|
+
return decorator
|
|
196
|
+
|
|
197
|
+
def job(self, name: str, task: Any, **kwargs):
|
|
198
|
+
"""Decorator to define a job."""
|
|
199
|
+
|
|
200
|
+
def decorator(func):
|
|
201
|
+
from ms_flow.tasking import job
|
|
202
|
+
|
|
203
|
+
j = job(name=name, task=task, chunker=func, **kwargs)
|
|
204
|
+
return j
|
|
205
|
+
|
|
206
|
+
return decorator
|
|
207
|
+
|
|
208
|
+
def _resolve_sink(self, sink: Any) -> Any:
|
|
209
|
+
"""Resolve sink object. If string, convert to TableOutputSpec."""
|
|
210
|
+
if isinstance(sink, str):
|
|
211
|
+
return TableOutputSpec(table_name=sink)
|
|
212
|
+
return sink
|
|
213
|
+
|
|
214
|
+
def submit_job(
|
|
215
|
+
self,
|
|
216
|
+
job: "JobDefinition",
|
|
217
|
+
*,
|
|
218
|
+
params: dict[str, Any] | Any | None = None,
|
|
219
|
+
chunks=None,
|
|
220
|
+
config: dict[str, Any] | None = None,
|
|
221
|
+
project_id: str | UUID | None = None,
|
|
222
|
+
depends_on: list[str] | None = None,
|
|
223
|
+
queue_policy: str = "fifo",
|
|
224
|
+
priority: int = 0,
|
|
225
|
+
executor_name: str | None = None,
|
|
226
|
+
cpu_required: int | None = None,
|
|
227
|
+
max_job_cpu: int | None = None,
|
|
228
|
+
max_inflight_tasks: int | None = None,
|
|
229
|
+
max_inflight_items: int | None = None,
|
|
230
|
+
total_chunks: int | None = None,
|
|
231
|
+
chunk_fail_fast_min_processed: int | None = None,
|
|
232
|
+
chunk_fail_fast_max_failed_ratio: float | None = None,
|
|
233
|
+
chunk_fail_fast_max_consecutive_failures: int | None = None,
|
|
234
|
+
store_results: bool | None = None,
|
|
235
|
+
output_spec: Any = None,
|
|
236
|
+
output_flush_every: int | None = None,
|
|
237
|
+
operational_profile: str | None = None,
|
|
238
|
+
result_handler=None,
|
|
239
|
+
result_handler_args: tuple[Any, ...] = (),
|
|
240
|
+
result_handler_kwargs: dict[str, Any] | None = None,
|
|
241
|
+
) -> str:
|
|
242
|
+
if self.active_context is None:
|
|
243
|
+
if project_id is None:
|
|
244
|
+
raise RuntimeError(
|
|
245
|
+
f"There is no active project to run job '{job.name}'. "
|
|
246
|
+
"Use create_or_open_project()/open_project(), or pass project_id."
|
|
247
|
+
)
|
|
248
|
+
self.open_project(project_id)
|
|
249
|
+
|
|
250
|
+
self._require_runtime()
|
|
251
|
+
if self.executor_manager is None:
|
|
252
|
+
raise RuntimeError("ExecutorManager no inicializado.")
|
|
253
|
+
|
|
254
|
+
submit_config = dict(config or {})
|
|
255
|
+
effective_project_id = project_id
|
|
256
|
+
if effective_project_id is None and self.active_context is not None:
|
|
257
|
+
effective_project_id = str(self.active_context.id)
|
|
258
|
+
|
|
259
|
+
selected_executor = str(executor_name or job.executor).strip() or job.executor
|
|
260
|
+
if not executor_is_supported(
|
|
261
|
+
self.executor_manager, selected_executor, job.supported_executors
|
|
262
|
+
):
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"Executor '{selected_executor}' is not supported by job '{job.name}'. "
|
|
265
|
+
f"Compatible: {job.supported_executors}."
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
prepared_chunks = chunks
|
|
269
|
+
deferred_chunk_build = False
|
|
270
|
+
resolved_total_chunks = total_chunks
|
|
271
|
+
if prepared_chunks is None:
|
|
272
|
+
if params is None:
|
|
273
|
+
raise ValueError(
|
|
274
|
+
f"Job '{job.name}' requires params or chunks. "
|
|
275
|
+
"Pass params=... so MolSuite builds the chunks, or chunks=... if you already materialised them."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
job_config = dict(submit_config)
|
|
279
|
+
if "project_store" not in job_config:
|
|
280
|
+
job_config["project_store"] = self.project_store
|
|
281
|
+
if "project_db" not in job_config:
|
|
282
|
+
job_config["project_db"] = self.project_db
|
|
283
|
+
if "project_context" not in job_config:
|
|
284
|
+
job_config["project_context"] = self.active_context
|
|
285
|
+
if "project_resources" not in job_config:
|
|
286
|
+
resource_map = self.get_project_resource_map(project_id=effective_project_id)
|
|
287
|
+
if resource_map:
|
|
288
|
+
job_config["project_resources"] = resource_map
|
|
289
|
+
|
|
290
|
+
if depends_on and job.chunker_ref:
|
|
291
|
+
prepared_chunks = iter(())
|
|
292
|
+
deferred_chunk_build = True
|
|
293
|
+
else:
|
|
294
|
+
prepared_chunks = job.build_chunks(params, config=job_config)
|
|
295
|
+
if resolved_total_chunks is None:
|
|
296
|
+
resolved_total_chunks = job.count_chunks(params, config=job_config)
|
|
297
|
+
|
|
298
|
+
handler_instance = result_handler
|
|
299
|
+
if handler_instance is None:
|
|
300
|
+
handler_instance = job.build_result_handler(
|
|
301
|
+
*result_handler_args,
|
|
302
|
+
**(result_handler_kwargs or {}),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
effective_output_spec = self._resolve_sink(output_spec if output_spec is not None else job.output_spec)
|
|
306
|
+
limits = self.settings_manager.settings.operational_limits
|
|
307
|
+
profile = _normalize_operational_profile(
|
|
308
|
+
operational_profile
|
|
309
|
+
if operational_profile is not None
|
|
310
|
+
else submit_config.get("operational_profile", limits.operational_profile)
|
|
311
|
+
)
|
|
312
|
+
preset = _OPERATIONAL_PROFILE_PRESETS[profile]
|
|
313
|
+
effective_max_inflight_tasks = (
|
|
314
|
+
max(1, int(max_inflight_tasks))
|
|
315
|
+
if max_inflight_tasks is not None
|
|
316
|
+
else int(preset["max_inflight_tasks"] or limits.default_max_inflight_tasks)
|
|
317
|
+
)
|
|
318
|
+
effective_max_inflight_items = (
|
|
319
|
+
max(1, int(max_inflight_items))
|
|
320
|
+
if max_inflight_items is not None
|
|
321
|
+
else int(preset["max_inflight_items"] or limits.default_max_inflight_items)
|
|
322
|
+
)
|
|
323
|
+
effective_output_flush_every = (
|
|
324
|
+
int(output_flush_every)
|
|
325
|
+
if output_flush_every is not None
|
|
326
|
+
else int(preset["output_flush_every"] or job.output_flush_every)
|
|
327
|
+
)
|
|
328
|
+
operational_policy = {
|
|
329
|
+
"profile": profile,
|
|
330
|
+
"max_inflight_tasks": effective_max_inflight_tasks,
|
|
331
|
+
"max_inflight_items": effective_max_inflight_items,
|
|
332
|
+
"output_flush_every": effective_output_flush_every,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return self.executor_manager.submit_job(
|
|
336
|
+
executor_name=selected_executor,
|
|
337
|
+
chunks=prepared_chunks,
|
|
338
|
+
run_chunk=job.task.handler_ref,
|
|
339
|
+
project_id=effective_project_id,
|
|
340
|
+
origin_id=self.app_id,
|
|
341
|
+
task_type=job.name,
|
|
342
|
+
priority=priority,
|
|
343
|
+
depends_on=depends_on,
|
|
344
|
+
queue_policy=queue_policy,
|
|
345
|
+
default_cpu_required=cpu_required or job.cpu_required,
|
|
346
|
+
max_job_cpu=max_job_cpu,
|
|
347
|
+
max_inflight_tasks=effective_max_inflight_tasks,
|
|
348
|
+
max_inflight_items=effective_max_inflight_items,
|
|
349
|
+
job_payload={
|
|
350
|
+
"job_name": job.name,
|
|
351
|
+
"task_name": job.task.name,
|
|
352
|
+
"_operational_profile": operational_policy,
|
|
353
|
+
"_supported_executors": list(job.supported_executors),
|
|
354
|
+
"_chunker_ref": job.chunker_ref,
|
|
355
|
+
# The handler instance dies with the process; its factory and the arguments
|
|
356
|
+
# that survive a round-trip through JSON are what lets a reattached job
|
|
357
|
+
# rebuild it. Live objects (the project store) are re-injected on restore.
|
|
358
|
+
"_result_handler_ref": "" if result_handler is not None else job.result_handler_ref,
|
|
359
|
+
"_result_handler_kwargs": {
|
|
360
|
+
key: value
|
|
361
|
+
for key, value in (result_handler_kwargs or {}).items()
|
|
362
|
+
if isinstance(value, (str, int, float, bool, list, dict, type(None)))
|
|
363
|
+
},
|
|
364
|
+
"_chunk_counter_ref": job.chunk_counter_ref,
|
|
365
|
+
"_chunker_params": params,
|
|
366
|
+
"_dispatch_policy": DispatchPolicy(
|
|
367
|
+
max_inflight_tasks=effective_max_inflight_tasks,
|
|
368
|
+
max_inflight_items=effective_max_inflight_items,
|
|
369
|
+
).to_mapping(),
|
|
370
|
+
"_default_cpu_required": cpu_required or job.cpu_required,
|
|
371
|
+
"_deferred_chunk_build_pending": deferred_chunk_build,
|
|
372
|
+
"_data_context": {
|
|
373
|
+
"project_path": str(self.active_context.path) if self.active_context is not None else "",
|
|
374
|
+
"project_db_path": (
|
|
375
|
+
str(self.project_db.db_path)
|
|
376
|
+
if self.project_db is not None and self.project_db.db_path is not None
|
|
377
|
+
else ""
|
|
378
|
+
),
|
|
379
|
+
"executor_db_path": (
|
|
380
|
+
str(self.executor_db.db_path)
|
|
381
|
+
if self.executor_db is not None and self.executor_db.db_path is not None
|
|
382
|
+
else ""
|
|
383
|
+
),
|
|
384
|
+
# Deferred chunk builds (depends_on + chunker) rebuild config from this
|
|
385
|
+
# context, so persist the JSON-safe resource map too — otherwise jobs that
|
|
386
|
+
# resolve a project resource at chunk-build time (e.g. chemistry's 'molecules'
|
|
387
|
+
# storage dir) fail with "Missing project resource" once they're deferred.
|
|
388
|
+
"project_resources": self.get_project_resource_map(project_id=effective_project_id),
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
result_handler=handler_instance,
|
|
392
|
+
store_results=job.store_results if store_results is None else bool(store_results),
|
|
393
|
+
setup_ref=job.setup_ref,
|
|
394
|
+
stage_ref=job.stage_chunk_ref,
|
|
395
|
+
finalize_ref=job.finalize_ref,
|
|
396
|
+
stage_fail_policy=job.stage_fail_policy,
|
|
397
|
+
max_stage_failures=job.max_stage_failures,
|
|
398
|
+
output_spec=effective_output_spec,
|
|
399
|
+
output_flush_every=effective_output_flush_every,
|
|
400
|
+
total_chunks=(
|
|
401
|
+
max(0, int(resolved_total_chunks))
|
|
402
|
+
if resolved_total_chunks is not None
|
|
403
|
+
else len(prepared_chunks) if isinstance(prepared_chunks, list) else None
|
|
404
|
+
),
|
|
405
|
+
chunk_fail_fast_min_processed=chunk_fail_fast_min_processed,
|
|
406
|
+
chunk_fail_fast_max_failed_ratio=chunk_fail_fast_max_failed_ratio,
|
|
407
|
+
chunk_fail_fast_max_consecutive_failures=chunk_fail_fast_max_consecutive_failures,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def wait_for_job(
|
|
411
|
+
self,
|
|
412
|
+
job_id: str,
|
|
413
|
+
*,
|
|
414
|
+
poll_s: float = 0.25,
|
|
415
|
+
progress_cb: Callable[[JobSnapshot], None] | None = None,
|
|
416
|
+
) -> JobSnapshot:
|
|
417
|
+
self._require_runtime()
|
|
418
|
+
if self.executor_manager is None:
|
|
419
|
+
raise RuntimeError("ExecutorManager no inicializado.")
|
|
420
|
+
|
|
421
|
+
terminal = {"completed", "failed", "canceled"}
|
|
422
|
+
while True:
|
|
423
|
+
row = self.executor_manager.get_job(job_id)
|
|
424
|
+
if row is None:
|
|
425
|
+
raise RuntimeError(f"No se encontro job: {job_id}")
|
|
426
|
+
if progress_cb is not None:
|
|
427
|
+
progress_cb(row)
|
|
428
|
+
if row.status in terminal:
|
|
429
|
+
return row
|
|
430
|
+
time.sleep(max(0.05, float(poll_s)))
|
|
431
|
+
|
|
432
|
+
def get_executor_job(self, job_id: str) -> JobSnapshot | None:
|
|
433
|
+
"""Return the current executor snapshot without waiting."""
|
|
434
|
+
self._require_runtime()
|
|
435
|
+
if self.executor_manager is None:
|
|
436
|
+
raise RuntimeError("ExecutorManager no inicializado.")
|
|
437
|
+
return self.executor_manager.get_job(str(job_id))
|
|
438
|
+
|
|
439
|
+
def purge_job_history(self, *, older_than_days: float = 30.0) -> dict[str, int]:
|
|
440
|
+
"""Prune chunks and events of finished jobs. Runs by itself when a project opens."""
|
|
441
|
+
self._require_runtime()
|
|
442
|
+
access = getattr(self, "executor_access", None)
|
|
443
|
+
if access is None:
|
|
444
|
+
self._require_executor_db()
|
|
445
|
+
raise RuntimeError("ExecutorStore is not available for the active project.")
|
|
446
|
+
return access.purge_finished_jobs(older_than_days=older_than_days)
|
|
447
|
+
|
|
448
|
+
def get_job_outputs(self, job_id: str) -> list[dict[str, Any]]:
|
|
449
|
+
self._require_runtime()
|
|
450
|
+
access = getattr(self, "executor_access", None)
|
|
451
|
+
if access is None:
|
|
452
|
+
self._require_executor_db()
|
|
453
|
+
raise RuntimeError("ExecutorStore is not available for the active project.")
|
|
454
|
+
return access.list_job_outputs(job_id)
|
|
455
|
+
|
|
456
|
+
def get_job_chunks(
|
|
457
|
+
self,
|
|
458
|
+
*,
|
|
459
|
+
job_id: str | None = None,
|
|
460
|
+
job_ids: list[str] | None = None,
|
|
461
|
+
statuses: tuple[str, ...] | list[str] | None = None,
|
|
462
|
+
limit: int | None = None,
|
|
463
|
+
include_payload: bool = False,
|
|
464
|
+
) -> list[dict[str, Any]]:
|
|
465
|
+
self._require_runtime()
|
|
466
|
+
access = getattr(self, "executor_access", None)
|
|
467
|
+
if access is None:
|
|
468
|
+
self._require_executor_db()
|
|
469
|
+
raise RuntimeError("ExecutorStore is not available for the active project.")
|
|
470
|
+
return access.list_job_chunks(
|
|
471
|
+
job_id=job_id,
|
|
472
|
+
job_ids=job_ids,
|
|
473
|
+
statuses=statuses,
|
|
474
|
+
limit=limit,
|
|
475
|
+
include_payload=include_payload,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
def get_job_event_records(
|
|
479
|
+
self,
|
|
480
|
+
*,
|
|
481
|
+
after_event_id: int = 0,
|
|
482
|
+
limit: int = 200,
|
|
483
|
+
) -> list[dict[str, Any]]:
|
|
484
|
+
self._require_runtime()
|
|
485
|
+
access = getattr(self, "executor_access", None)
|
|
486
|
+
if access is None:
|
|
487
|
+
self._require_executor_db()
|
|
488
|
+
raise RuntimeError("ExecutorStore is not available for the active project.")
|
|
489
|
+
return access.list_job_events(
|
|
490
|
+
after_event_id=after_event_id,
|
|
491
|
+
limit=limit,
|
|
492
|
+
ascending=True,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def get_job_events(self, job_id: str, limit: int | None = None) -> list[dict[str, Any]]:
|
|
496
|
+
self._require_runtime()
|
|
497
|
+
access = getattr(self, "executor_access", None)
|
|
498
|
+
if access is None:
|
|
499
|
+
self._require_executor_db()
|
|
500
|
+
raise RuntimeError("ExecutorStore is not available for the active project.")
|
|
501
|
+
rows = access.list_job_events(job_id=job_id, limit=limit, ascending=True)
|
|
502
|
+
return [
|
|
503
|
+
{
|
|
504
|
+
"level": row["level"],
|
|
505
|
+
"type": row["event_type"],
|
|
506
|
+
"message": row["message"],
|
|
507
|
+
"created_at": row["created_at"].isoformat(),
|
|
508
|
+
}
|
|
509
|
+
for row in rows
|
|
510
|
+
]
|
|
511
|
+
|
|
512
|
+
def cancel_job(self, job_id: str) -> None:
|
|
513
|
+
self._require_runtime()
|
|
514
|
+
if self.executor_manager is None:
|
|
515
|
+
raise RuntimeError("ExecutorManager no inicializado.")
|
|
516
|
+
self.executor_manager.cancel_job(job_id)
|
|
517
|
+
|
|
518
|
+
def resubmit_job(
|
|
519
|
+
self,
|
|
520
|
+
job_id: str,
|
|
521
|
+
*,
|
|
522
|
+
executor_name: str | None = None,
|
|
523
|
+
cpu_required: int | None = None,
|
|
524
|
+
queue_policy: str | None = None,
|
|
525
|
+
priority: int | None = None,
|
|
526
|
+
project_id: str | UUID | None = None,
|
|
527
|
+
store_results: bool | None = None,
|
|
528
|
+
output_spec: Any = None,
|
|
529
|
+
output_flush_every: int | None = None,
|
|
530
|
+
) -> str:
|
|
531
|
+
self._require_runtime()
|
|
532
|
+
if self.executor_manager is None:
|
|
533
|
+
raise RuntimeError("ExecutorManager no inicializado.")
|
|
534
|
+
return self.executor_manager.resubmit_job(
|
|
535
|
+
job_id,
|
|
536
|
+
executor_name=executor_name,
|
|
537
|
+
cpu_required=cpu_required,
|
|
538
|
+
queue_policy=queue_policy,
|
|
539
|
+
priority=priority,
|
|
540
|
+
project_id=project_id,
|
|
541
|
+
store_results=store_results,
|
|
542
|
+
output_spec=output_spec,
|
|
543
|
+
output_flush_every=output_flush_every,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
def get_executor_status(self) -> dict[str, Any]:
|
|
547
|
+
self._require_runtime()
|
|
548
|
+
if self.executor_manager is None:
|
|
549
|
+
raise RuntimeError("ExecutorManager no inicializado.")
|
|
550
|
+
return self.executor_manager.get_status()
|
|
551
|
+
|
|
552
|
+
def get_executor_capability_matrix(self) -> dict[str, dict[str, Any]]:
|
|
553
|
+
self._require_runtime()
|
|
554
|
+
if self.executor_manager is None:
|
|
555
|
+
raise RuntimeError("ExecutorManager no inicializado.")
|
|
556
|
+
return self.executor_manager.get_executor_capability_matrix()
|
|
557
|
+
|
|
558
|
+
def activate_compute_backend(self, backend: str, **kwargs) -> dict[str, Any]:
|
|
559
|
+
"""Switch the logical ``compute`` executor between ``loky`` (local) and
|
|
560
|
+
``ray`` (cluster). Returns the resulting backend status dict."""
|
|
561
|
+
self._require_runtime()
|
|
562
|
+
if self.executor_manager is None:
|
|
563
|
+
raise RuntimeError("ExecutorManager no inicializado.")
|
|
564
|
+
return self.executor_manager.activate_compute_backend(backend, **kwargs)
|
|
565
|
+
|
|
566
|
+
def compute_backend_status(self) -> dict[str, Any]:
|
|
567
|
+
self._require_runtime()
|
|
568
|
+
if self.executor_manager is None:
|
|
569
|
+
raise RuntimeError("ExecutorManager no inicializado.")
|
|
570
|
+
status = dict(self.executor_manager.compute_backend_status())
|
|
571
|
+
# Why the configured backend may not be the live one (unreachable ray, etc).
|
|
572
|
+
status["fallback_reason"] = getattr(self, "compute_backend_fallback", "")
|
|
573
|
+
return status
|
|
574
|
+
|
|
575
|
+
def get_project_store(self):
|
|
576
|
+
return self.advanced.project_store
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
__all__ = ["MolSuiteJobApiMixin", "batch_job", "streaming_job"]
|