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.
Files changed (99) hide show
  1. ms_flow/__init__.py +26 -0
  2. ms_flow/_api_impl/__init__.py +9 -0
  3. ms_flow/_api_impl/advanced.py +64 -0
  4. ms_flow/_api_impl/job_api.py +579 -0
  5. ms_flow/_api_impl/project_runtime.py +886 -0
  6. ms_flow/advanced.py +5 -0
  7. ms_flow/api.py +79 -0
  8. ms_flow/artifacts.py +329 -0
  9. ms_flow/core/__init__.py +0 -0
  10. ms_flow/core/app_settings.py +133 -0
  11. ms_flow/core/apps.py +173 -0
  12. ms_flow/core/callable_refs.py +66 -0
  13. ms_flow/core/configuration.py +365 -0
  14. ms_flow/core/data/__init__.py +53 -0
  15. ms_flow/core/data/backends.py +708 -0
  16. ms_flow/core/data/bridge.py +212 -0
  17. ms_flow/core/data/contracts.py +534 -0
  18. ms_flow/core/data/planner.py +75 -0
  19. ms_flow/core/data/project_output.py +27 -0
  20. ms_flow/core/data/runtime.py +72 -0
  21. ms_flow/core/data/shard.py +556 -0
  22. ms_flow/core/database/__init__.py +33 -0
  23. ms_flow/core/database/base.py +229 -0
  24. ms_flow/core/database/executor.py +218 -0
  25. ms_flow/core/database/executor_models.py +112 -0
  26. ms_flow/core/database/master.py +168 -0
  27. ms_flow/core/database/master_models.py +40 -0
  28. ms_flow/core/database/project.py +678 -0
  29. ms_flow/core/database/project_models.py +59 -0
  30. ms_flow/core/database/project_records.py +100 -0
  31. ms_flow/core/database/project_store_ops.py +958 -0
  32. ms_flow/core/database/sqlite_utils.py +208 -0
  33. ms_flow/core/events.py +5 -0
  34. ms_flow/core/executor/__init__.py +62 -0
  35. ms_flow/core/executor/backend_selector.py +254 -0
  36. ms_flow/core/executor/command_inbox.py +40 -0
  37. ms_flow/core/executor/dispatch_model.py +71 -0
  38. ms_flow/core/executor/dispatch_service.py +397 -0
  39. ms_flow/core/executor/executor_registry.py +236 -0
  40. ms_flow/core/executor/hpc_adapter.py +436 -0
  41. ms_flow/core/executor/hpc_runtime.py +124 -0
  42. ms_flow/core/executor/job_monitoring.py +666 -0
  43. ms_flow/core/executor/job_snapshot.py +124 -0
  44. ms_flow/core/executor/lifecycle_controller.py +369 -0
  45. ms_flow/core/executor/local_adapters.py +497 -0
  46. ms_flow/core/executor/local_compute_scheduler.py +116 -0
  47. ms_flow/core/executor/local_scheduler.py +169 -0
  48. ms_flow/core/executor/manager/__init__.py +15 -0
  49. ms_flow/core/executor/manager/_manager.py +1904 -0
  50. ms_flow/core/executor/provisioning.py +443 -0
  51. ms_flow/core/executor/ray_adapter.py +452 -0
  52. ms_flow/core/executor/resource_manager.py +65 -0
  53. ms_flow/core/executor/result_handlers.py +696 -0
  54. ms_flow/core/executor/runner_refs.py +152 -0
  55. ms_flow/core/executor/runtime_state.py +209 -0
  56. ms_flow/core/executor/runtime_status_service.py +408 -0
  57. ms_flow/core/executor/services/__init__.py +0 -0
  58. ms_flow/core/executor/services/dispatch_pool.py +230 -0
  59. ms_flow/core/executor/services/event_recorder.py +155 -0
  60. ms_flow/core/executor/services/feeding.py +274 -0
  61. ms_flow/core/executor/services/job_commands.py +258 -0
  62. ms_flow/core/executor/services/job_status.py +481 -0
  63. ms_flow/core/executor/services/job_store.py +350 -0
  64. ms_flow/core/executor/services/payload_store.py +166 -0
  65. ms_flow/core/executor/services/persistence_coordinator.py +240 -0
  66. ms_flow/core/executor/services/sink_writer_pool.py +122 -0
  67. ms_flow/core/executor/staging_manager.py +131 -0
  68. ms_flow/core/executor/submission_service.py +604 -0
  69. ms_flow/core/executor/utils.py +40 -0
  70. ms_flow/core/project/__init__.py +21 -0
  71. ms_flow/core/project/catalog.py +343 -0
  72. ms_flow/core/project/context.py +45 -0
  73. ms_flow/core/project/manager.py +193 -0
  74. ms_flow/core/project/repository.py +264 -0
  75. ms_flow/core/project/resources.py +150 -0
  76. ms_flow/core/project/state.py +45 -0
  77. ms_flow/core/settings/__init__.py +0 -0
  78. ms_flow/core/settings/defaults.toml +42 -0
  79. ms_flow/core/settings/manager.py +397 -0
  80. ms_flow/core/settings/models.py +412 -0
  81. ms_flow/job_templates.py +171 -0
  82. ms_flow/logger.py +316 -0
  83. ms_flow/main.py +164 -0
  84. ms_flow/progress.py +70 -0
  85. ms_flow/query.py +255 -0
  86. ms_flow/runtime.py +208 -0
  87. ms_flow/selection.py +70 -0
  88. ms_flow/sinks.py +180 -0
  89. ms_flow/specs/__init__.py +42 -0
  90. ms_flow/specs/input.py +260 -0
  91. ms_flow/specs/output.py +73 -0
  92. ms_flow/specs/processor.py +30 -0
  93. ms_flow/specs/workflow.py +176 -0
  94. ms_flow/tasking.py +771 -0
  95. ms_flow-0.1.0.dist-info/METADATA +77 -0
  96. ms_flow-0.1.0.dist-info/RECORD +99 -0
  97. ms_flow-0.1.0.dist-info/WHEEL +5 -0
  98. ms_flow-0.1.0.dist-info/licenses/LICENSE +21 -0
  99. 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"]