palmengine 0.7.4__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.
- palm/__init__.py +23 -0
- palm/app/__init__.py +22 -0
- palm/app/app.py +356 -0
- palm/app/bootstrap.py +124 -0
- palm/app/cli_settings.py +63 -0
- palm/app/registry.py +72 -0
- palm/app/resolvers.py +43 -0
- palm/app/session.py +67 -0
- palm/app/settings.py +61 -0
- palm/backends/__init__.py +5 -0
- palm/backends/behavior_tree/__init__.py +7 -0
- palm/backends/behavior_tree/runner.py +68 -0
- palm/common/__init__.py +91 -0
- palm/common/exceptions.py +50 -0
- palm/common/executions/__init__.py +12 -0
- palm/common/executions/executor.py +413 -0
- palm/common/executions/flow_submission.py +114 -0
- palm/common/executions/process_submission.py +55 -0
- palm/common/hooks/__init__.py +6 -0
- palm/common/hooks/instance_persistence.py +68 -0
- palm/common/hooks/state_snapshot.py +72 -0
- palm/common/managers/__init__.py +15 -0
- palm/common/managers/base.py +25 -0
- palm/common/managers/instance_manager.py +341 -0
- palm/common/patterns/__init__.py +6 -0
- palm/common/patterns/build_context.py +23 -0
- palm/common/patterns/builder.py +51 -0
- palm/common/persistence/__init__.py +23 -0
- palm/common/persistence/definition_repository.py +299 -0
- palm/common/persistence/instance_repository.py +193 -0
- palm/common/persistence/instance_resume.py +16 -0
- palm/common/persistence/instance_sync.py +110 -0
- palm/common/plans/__init__.py +7 -0
- palm/common/plans/execution_plan.py +59 -0
- palm/common/plans/process_plan.py +32 -0
- palm/common/plans/registry.py +68 -0
- palm/common/storage/__init__.py +5 -0
- palm/common/storage/factory.py +141 -0
- palm/core/__init__.py +112 -0
- palm/core/auth/__init__.py +9 -0
- palm/core/auth/engine.py +57 -0
- palm/core/base.py +54 -0
- palm/core/behavior_tree/__init__.py +58 -0
- palm/core/behavior_tree/base.py +92 -0
- palm/core/behavior_tree/base_pattern.py +39 -0
- palm/core/behavior_tree/composite.py +24 -0
- palm/core/behavior_tree/decorator.py +37 -0
- palm/core/behavior_tree/engine.py +118 -0
- palm/core/behavior_tree/exceptions.py +21 -0
- palm/core/behavior_tree/leaf.py +20 -0
- palm/core/behavior_tree/nodes/__init__.py +27 -0
- palm/core/behavior_tree/nodes/composite/__init__.py +10 -0
- palm/core/behavior_tree/nodes/composite/parallel_node.py +74 -0
- palm/core/behavior_tree/nodes/composite/selector_node.py +35 -0
- palm/core/behavior_tree/nodes/composite/sequence_node.py +35 -0
- palm/core/behavior_tree/nodes/decorator/__init__.py +7 -0
- palm/core/behavior_tree/nodes/decorator/inverter_node.py +21 -0
- palm/core/behavior_tree/nodes/decorator/repeat_node.py +38 -0
- palm/core/behavior_tree/nodes/decorator/retry_node.py +33 -0
- palm/core/behavior_tree/nodes/leaf/__init__.py +11 -0
- palm/core/behavior_tree/nodes/leaf/action_node.py +31 -0
- palm/core/behavior_tree/nodes/leaf/condition_node.py +26 -0
- palm/core/behavior_tree/nodes/leaf/interactive_leaf.py +45 -0
- palm/core/behavior_tree/root.py +19 -0
- palm/core/context/__init__.py +10 -0
- palm/core/context/base_state.py +43 -0
- palm/core/context/engine.py +110 -0
- palm/core/event/__init__.py +9 -0
- palm/core/event/engine.py +52 -0
- palm/core/exceptions.py +56 -0
- palm/core/orchestration/__init__.py +35 -0
- palm/core/orchestration/drive.py +38 -0
- palm/core/orchestration/engine.py +316 -0
- palm/core/orchestration/events.py +17 -0
- palm/core/orchestration/exceptions.py +47 -0
- palm/core/orchestration/execution/__init__.py +5 -0
- palm/core/orchestration/execution/base_runner.py +18 -0
- palm/core/orchestration/execution_context.py +16 -0
- palm/core/orchestration/hooks.py +65 -0
- palm/core/orchestration/input_capable.py +26 -0
- palm/core/orchestration/job.py +142 -0
- palm/core/orchestration/job_state.py +40 -0
- palm/core/orchestration/mode/__init__.py +8 -0
- palm/core/orchestration/mode/base_mode.py +52 -0
- palm/core/orchestration/mode/unconfigured_mode.py +41 -0
- palm/core/orchestration/run_result.py +24 -0
- palm/core/registry.py +73 -0
- palm/core/resource/__init__.py +10 -0
- palm/core/resource/base_provider.py +29 -0
- palm/core/resource/engine.py +39 -0
- palm/core/storage/__init__.py +10 -0
- palm/core/storage/base_backend.py +53 -0
- palm/core/storage/engine.py +93 -0
- palm/definitions/__init__.py +10 -0
- palm/definitions/flow.py +67 -0
- palm/definitions/process.py +70 -0
- palm/instances/__init__.py +9 -0
- palm/instances/process_instance.py +113 -0
- palm/instances/state_snapshot.py +80 -0
- palm/instances/status_history.py +42 -0
- palm/patterns/__init__.py +15 -0
- palm/patterns/_apps.py +18 -0
- palm/patterns/_registry.py +134 -0
- palm/patterns/dag/__init__.py +10 -0
- palm/patterns/dag/builder.py +26 -0
- palm/patterns/dag/pattern.py +21 -0
- palm/patterns/dag/registry.py +9 -0
- palm/patterns/etl/__init__.py +10 -0
- palm/patterns/etl/builder.py +26 -0
- palm/patterns/etl/pattern.py +26 -0
- palm/patterns/etl/registry.py +9 -0
- palm/patterns/wizard/__init__.py +63 -0
- palm/patterns/wizard/action_leaf.py +94 -0
- palm/patterns/wizard/backtrack.py +71 -0
- palm/patterns/wizard/base.py +10 -0
- palm/patterns/wizard/builder.py +170 -0
- palm/patterns/wizard/commit_leaf.py +113 -0
- palm/patterns/wizard/config.py +156 -0
- palm/patterns/wizard/events.py +19 -0
- palm/patterns/wizard/handler.py +94 -0
- palm/patterns/wizard/keys.py +23 -0
- palm/patterns/wizard/options.py +66 -0
- palm/patterns/wizard/pattern.py +143 -0
- palm/patterns/wizard/persistence.py +54 -0
- palm/patterns/wizard/registry.py +24 -0
- palm/patterns/wizard/resume.py +34 -0
- palm/patterns/wizard/step_kinds.py +13 -0
- palm/patterns/wizard/step_leaf.py +98 -0
- palm/patterns/wizard/submission.py +21 -0
- palm/patterns/wizard/summary_leaf.py +78 -0
- palm/patterns/wizard/tree.py +78 -0
- palm/patterns/wizard/validation.py +137 -0
- palm/providers/__init__.py +13 -0
- palm/providers/_apps.py +14 -0
- palm/providers/graphql/__init__.py +6 -0
- palm/providers/graphql/provider.py +20 -0
- palm/providers/graphql/registry.py +6 -0
- palm/providers/postgres/__init__.py +6 -0
- palm/providers/postgres/provider.py +20 -0
- palm/providers/postgres/registry.py +6 -0
- palm/providers/rest/__init__.py +6 -0
- palm/providers/rest/provider.py +20 -0
- palm/providers/rest/registry.py +6 -0
- palm/runtimes/__init__.py +24 -0
- palm/runtimes/base.py +290 -0
- palm/runtimes/cli.py +118 -0
- palm/runtimes/cli_pkg/__init__.py +6 -0
- palm/runtimes/cli_pkg/actions.py +68 -0
- palm/runtimes/cli_pkg/args.py +211 -0
- palm/runtimes/cli_pkg/bootstrap.py +53 -0
- palm/runtimes/cli_pkg/commands/__init__.py +5 -0
- palm/runtimes/cli_pkg/commands/registry.py +413 -0
- palm/runtimes/cli_pkg/completion.py +184 -0
- palm/runtimes/cli_pkg/context.py +80 -0
- palm/runtimes/cli_pkg/display.py +178 -0
- palm/runtimes/cli_pkg/doctor.py +112 -0
- palm/runtimes/cli_pkg/instance_ops.py +126 -0
- palm/runtimes/cli_pkg/instances.py +52 -0
- palm/runtimes/cli_pkg/output.py +33 -0
- palm/runtimes/cli_pkg/repl.py +88 -0
- palm/runtimes/cli_pkg/settings.py +5 -0
- palm/runtimes/cli_pkg/startup.py +67 -0
- palm/runtimes/cli_pkg/version_info.py +59 -0
- palm/runtimes/daemon.py +46 -0
- palm/runtimes/embedded.py +27 -0
- palm/runtimes/hooks.py +115 -0
- palm/runtimes/host.py +41 -0
- palm/runtimes/schedulers/__init__.py +6 -0
- palm/runtimes/schedulers/inline.py +61 -0
- palm/runtimes/schedulers/queued.py +133 -0
- palm/runtimes/server/__init__.py +6 -0
- palm/runtimes/server/auth.py +37 -0
- palm/runtimes/server/http.py +281 -0
- palm/runtimes/server/runtime.py +195 -0
- palm/runtimes/wiring.py +59 -0
- palm/states/__init__.py +10 -0
- palm/states/blackboard_state.py +40 -0
- palm/storages/__init__.py +21 -0
- palm/storages/_apps.py +20 -0
- palm/storages/filesystem/__init__.py +6 -0
- palm/storages/filesystem/backend.py +175 -0
- palm/storages/filesystem/registry.py +6 -0
- palm/storages/memory/__init__.py +6 -0
- palm/storages/memory/backend.py +42 -0
- palm/storages/memory/registry.py +6 -0
- palm/storages/mongodb/__init__.py +6 -0
- palm/storages/mongodb/backend.py +81 -0
- palm/storages/mongodb/registry.py +6 -0
- palm/storages/postgres/__init__.py +6 -0
- palm/storages/postgres/backend.py +36 -0
- palm/storages/postgres/registry.py +6 -0
- palm/utils/__init__.py +5 -0
- palmengine-0.7.4.dist-info/METADATA +370 -0
- palmengine-0.7.4.dist-info/RECORD +196 -0
- palmengine-0.7.4.dist-info/WHEEL +4 -0
- palmengine-0.7.4.dist-info/entry_points.txt +2 -0
palm/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Palm Engine — lightweight orchestration for multi-step transactional workflows.
|
|
3
|
+
|
|
4
|
+
The ``palm`` package is organized in layers:
|
|
5
|
+
|
|
6
|
+
- ``palm.app`` — application orchestrator (:class:`~palm.app.PalmApp`, settings, multi-runtime)
|
|
7
|
+
- ``palm.core`` — pure foundational engines (no imports from outside core)
|
|
8
|
+
- ``palm.common`` — shared coordination (plans, submission, hooks, persistence)
|
|
9
|
+
- ``palm.instances`` — durable process instance snapshots
|
|
10
|
+
- ``palm.patterns`` / ``palm.providers`` / ``palm.storages`` — extensible plugin apps
|
|
11
|
+
- ``palm.definitions`` — flow and process definition models
|
|
12
|
+
- ``palm.runtimes`` — CLI, embedded, server, and daemon surfaces
|
|
13
|
+
|
|
14
|
+
Public API version: ``palm.__version__`` (currently 0.7.4).
|
|
15
|
+
|
|
16
|
+
PyPI distribution name: ``palmengine`` (``pip install palmengine``).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
__version__ = "0.7.4"
|
|
22
|
+
|
|
23
|
+
__all__ = ["__version__"]
|
palm/app/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Palm application layer — configuration, bootstrap, and multi-runtime orchestration.
|
|
3
|
+
|
|
4
|
+
Use :class:`~palm.app.app.PalmApp` as the top-level entrypoint when embedding
|
|
5
|
+
Palm in services, tests, or multi-process deployments.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from palm.app.app import CLI_RUNTIME_NAME, PalmApp
|
|
9
|
+
from palm.app.registry import RuntimeHandle, RuntimeKind, RuntimeRegistry
|
|
10
|
+
from palm.app.session import create_cli_app, create_console
|
|
11
|
+
from palm.app.settings import PalmSettings
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CLI_RUNTIME_NAME",
|
|
15
|
+
"PalmApp",
|
|
16
|
+
"PalmSettings",
|
|
17
|
+
"RuntimeHandle",
|
|
18
|
+
"RuntimeKind",
|
|
19
|
+
"RuntimeRegistry",
|
|
20
|
+
"create_cli_app",
|
|
21
|
+
"create_console",
|
|
22
|
+
]
|
palm/app/app.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PalmApp — central application orchestrator for Palm Engine.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
8
|
+
|
|
9
|
+
from palm.app.bootstrap import (
|
|
10
|
+
ensure_plugins,
|
|
11
|
+
load_definitions_for_repository,
|
|
12
|
+
runtime_start_options,
|
|
13
|
+
)
|
|
14
|
+
from palm.app.registry import RuntimeHandle, RuntimeKind, RuntimeRegistry
|
|
15
|
+
from palm.app.settings import PalmSettings
|
|
16
|
+
from palm.common.managers import InstanceManager
|
|
17
|
+
from palm.common.persistence.instance_repository import InstanceRepository
|
|
18
|
+
from palm.core.storage import StorageEngine
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from palm.common.persistence.definition_repository import DefinitionRepository
|
|
22
|
+
from palm.core.orchestration import Job
|
|
23
|
+
from palm.definitions.flow import FlowDefinition
|
|
24
|
+
from palm.definitions.process import ProcessDefinition
|
|
25
|
+
from palm.instances import ProcessInstance, StateSnapshot
|
|
26
|
+
from palm.runtimes.base import BaseRuntime
|
|
27
|
+
|
|
28
|
+
CLI_RUNTIME_NAME = "cli"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PalmApp:
|
|
32
|
+
"""
|
|
33
|
+
Top-level Palm application — configuration, shared storage, and runtimes.
|
|
34
|
+
|
|
35
|
+
A single ``PalmApp`` can host multiple runtimes (embedded, daemon, server)
|
|
36
|
+
that share one :class:`~palm.core.storage.StorageEngine` for durable
|
|
37
|
+
definitions and instances.
|
|
38
|
+
|
|
39
|
+
Typical usage::
|
|
40
|
+
|
|
41
|
+
app = PalmApp().bootstrap()
|
|
42
|
+
embedded = app.create_runtime("embedded", autostart=True)
|
|
43
|
+
daemon = app.create_runtime("daemon", name="worker", autostart=True)
|
|
44
|
+
app.load_definitions()
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
settings: PalmSettings | None = None,
|
|
50
|
+
*,
|
|
51
|
+
storage: StorageEngine | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self.settings = settings or PalmSettings()
|
|
54
|
+
self._owns_storage = storage is None
|
|
55
|
+
self.storage = storage if storage is not None else StorageEngine()
|
|
56
|
+
self._instance_repository = InstanceRepository(self.storage)
|
|
57
|
+
self._instance_manager = InstanceManager(
|
|
58
|
+
self._instance_repository,
|
|
59
|
+
settings=self.settings,
|
|
60
|
+
)
|
|
61
|
+
self._runtimes = RuntimeRegistry()
|
|
62
|
+
self._primary: str | None = None
|
|
63
|
+
self._bootstrapped = False
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def is_bootstrapped(self) -> bool:
|
|
67
|
+
return self._bootstrapped
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def primary_name(self) -> str | None:
|
|
71
|
+
return self._primary
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def instance_manager(self) -> InstanceManager:
|
|
75
|
+
"""Shared instance lifecycle coordinator across runtimes."""
|
|
76
|
+
return self._instance_manager
|
|
77
|
+
|
|
78
|
+
def bootstrap(self) -> Self:
|
|
79
|
+
"""Load plugin apps and mark the application ready for runtime creation."""
|
|
80
|
+
ensure_plugins()
|
|
81
|
+
self._bootstrapped = True
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def bootstrap_cli(self, **start_options: Any) -> BaseRuntime:
|
|
85
|
+
"""Register the CLI embedded runtime, start it, and load definitions."""
|
|
86
|
+
self._require_bootstrapped()
|
|
87
|
+
runtime = self.create_runtime(
|
|
88
|
+
"embedded",
|
|
89
|
+
name=CLI_RUNTIME_NAME,
|
|
90
|
+
autostart=True,
|
|
91
|
+
set_primary=True,
|
|
92
|
+
**start_options,
|
|
93
|
+
)
|
|
94
|
+
self.load_definitions(name=CLI_RUNTIME_NAME)
|
|
95
|
+
return runtime
|
|
96
|
+
|
|
97
|
+
def create_runtime(
|
|
98
|
+
self,
|
|
99
|
+
kind: RuntimeKind,
|
|
100
|
+
*,
|
|
101
|
+
name: str | None = None,
|
|
102
|
+
autostart: bool = False,
|
|
103
|
+
set_primary: bool | None = None,
|
|
104
|
+
**start_options: Any,
|
|
105
|
+
) -> BaseRuntime:
|
|
106
|
+
"""
|
|
107
|
+
Construct and optionally start a named runtime sharing app storage.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
kind:
|
|
112
|
+
``embedded``, ``daemon``, or ``server``.
|
|
113
|
+
name:
|
|
114
|
+
Registry key. Defaults to ``kind`` or ``{kind}-{n}`` when taken.
|
|
115
|
+
autostart:
|
|
116
|
+
When ``True``, call :meth:`start` before returning.
|
|
117
|
+
set_primary:
|
|
118
|
+
When ``True``, make this runtime the default for :attr:`runtime`.
|
|
119
|
+
Defaults to ``True`` when this is the first registered runtime.
|
|
120
|
+
"""
|
|
121
|
+
self._require_bootstrapped()
|
|
122
|
+
runtime_name = name or self._default_runtime_name(kind)
|
|
123
|
+
runtime = self._build_runtime(kind, **start_options)
|
|
124
|
+
handle = RuntimeHandle(name=runtime_name, kind=kind, runtime=runtime)
|
|
125
|
+
self._runtimes.register(handle)
|
|
126
|
+
|
|
127
|
+
if set_primary is True or (set_primary is None and self._primary is None):
|
|
128
|
+
self._primary = runtime_name
|
|
129
|
+
|
|
130
|
+
if autostart:
|
|
131
|
+
self.start(runtime_name, **start_options)
|
|
132
|
+
return runtime
|
|
133
|
+
|
|
134
|
+
def runtime(self, name: str | None = None) -> BaseRuntime:
|
|
135
|
+
"""Return a registered runtime (primary by default)."""
|
|
136
|
+
handle = self._runtimes.get(name or self._require_primary_name())
|
|
137
|
+
return handle.runtime
|
|
138
|
+
|
|
139
|
+
def repository(self, *, runtime_name: str | None = None) -> DefinitionRepository:
|
|
140
|
+
"""Return the definition repository for a registered runtime."""
|
|
141
|
+
return self.runtime(runtime_name).repository
|
|
142
|
+
|
|
143
|
+
def resolve_flow(self, ref: str, *, runtime_name: str | None = None) -> FlowDefinition:
|
|
144
|
+
"""Resolve a flow by display name, falling back to definition id."""
|
|
145
|
+
from palm.app.resolvers import resolve_flow_for_app
|
|
146
|
+
|
|
147
|
+
return resolve_flow_for_app(self, ref, runtime_name=runtime_name)
|
|
148
|
+
|
|
149
|
+
def resolve_process(
|
|
150
|
+
self, ref: str, *, runtime_name: str | None = None
|
|
151
|
+
) -> ProcessDefinition:
|
|
152
|
+
"""Resolve a process by display name, falling back to definition id."""
|
|
153
|
+
from palm.app.resolvers import resolve_process_for_app
|
|
154
|
+
|
|
155
|
+
return resolve_process_for_app(self, ref, runtime_name=runtime_name)
|
|
156
|
+
|
|
157
|
+
def submit_flow(
|
|
158
|
+
self,
|
|
159
|
+
ref: FlowDefinition | str,
|
|
160
|
+
*,
|
|
161
|
+
runtime_name: str | None = None,
|
|
162
|
+
by_id: bool = False,
|
|
163
|
+
job_id: str | None = None,
|
|
164
|
+
state: Any = None,
|
|
165
|
+
metadata: dict[str, Any] | None = None,
|
|
166
|
+
) -> Job:
|
|
167
|
+
"""Submit a flow on a registered runtime (primary by default)."""
|
|
168
|
+
return self.runtime(runtime_name).submit_flow(
|
|
169
|
+
ref,
|
|
170
|
+
by_id=by_id,
|
|
171
|
+
job_id=job_id,
|
|
172
|
+
state=state,
|
|
173
|
+
metadata=metadata,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def submit_process(
|
|
177
|
+
self,
|
|
178
|
+
ref: ProcessDefinition | str,
|
|
179
|
+
*,
|
|
180
|
+
runtime_name: str | None = None,
|
|
181
|
+
by_id: bool = False,
|
|
182
|
+
job_id: str | None = None,
|
|
183
|
+
state: Any = None,
|
|
184
|
+
metadata: dict[str, Any] | None = None,
|
|
185
|
+
) -> Job | list[Job]:
|
|
186
|
+
"""Submit a process on a registered runtime (primary by default)."""
|
|
187
|
+
return self.runtime(runtime_name).submit_process(
|
|
188
|
+
ref,
|
|
189
|
+
by_id=by_id,
|
|
190
|
+
job_id=job_id,
|
|
191
|
+
state=state,
|
|
192
|
+
metadata=metadata,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def resume_process(self, instance_id: str, *, runtime_name: str | None = None) -> Job:
|
|
196
|
+
"""Resume a persisted process instance on a registered runtime."""
|
|
197
|
+
return self.runtime(runtime_name).resume_process(instance_id)
|
|
198
|
+
|
|
199
|
+
def provide_input(
|
|
200
|
+
self, job_id: str, value: Any, *, runtime_name: str | None = None
|
|
201
|
+
) -> str | None:
|
|
202
|
+
"""Deliver interactive input and resume the job on a registered runtime."""
|
|
203
|
+
return self.runtime(runtime_name).provide_input(job_id, value)
|
|
204
|
+
|
|
205
|
+
def get_job(self, job_id: str, *, runtime_name: str | None = None) -> Job:
|
|
206
|
+
"""Return an orchestration job from a registered runtime."""
|
|
207
|
+
return self.runtime(runtime_name).get_job(job_id)
|
|
208
|
+
|
|
209
|
+
def get_instance(self, instance_id: str, *, runtime_name: str | None = None) -> ProcessInstance:
|
|
210
|
+
"""Load a persisted process instance from the shared manager."""
|
|
211
|
+
_ = runtime_name
|
|
212
|
+
return self._instance_manager.get(instance_id)
|
|
213
|
+
|
|
214
|
+
def list_instances(self, *, runtime_name: str | None = None) -> list[ProcessInstance]:
|
|
215
|
+
"""List durable process instances (full load via manager)."""
|
|
216
|
+
_ = runtime_name
|
|
217
|
+
return self._instance_manager.list_instances()
|
|
218
|
+
|
|
219
|
+
def list_instance_summaries(self, *, runtime_name: str | None = None) -> list:
|
|
220
|
+
"""List lightweight instance summaries without loading full payloads."""
|
|
221
|
+
_ = runtime_name
|
|
222
|
+
return self._instance_manager.list_summaries()
|
|
223
|
+
|
|
224
|
+
def list_instance_snapshots(
|
|
225
|
+
self, instance_id: str, *, runtime_name: str | None = None
|
|
226
|
+
) -> list[StateSnapshot]:
|
|
227
|
+
"""Return point-in-time state snapshots for a persisted instance."""
|
|
228
|
+
_ = runtime_name
|
|
229
|
+
return self._instance_manager.list_state_snapshots(instance_id)
|
|
230
|
+
|
|
231
|
+
def list_flows(self, *, runtime_name: str | None = None) -> list[FlowDefinition]:
|
|
232
|
+
"""List flow definitions from a registered runtime repository."""
|
|
233
|
+
return self.repository(runtime_name=runtime_name).list_flows()
|
|
234
|
+
|
|
235
|
+
def list_processes(self, *, runtime_name: str | None = None) -> list[ProcessDefinition]:
|
|
236
|
+
"""List process definitions from a registered runtime repository."""
|
|
237
|
+
return self.repository(runtime_name=runtime_name).list_processes()
|
|
238
|
+
|
|
239
|
+
def current_wizard_step(self, job_id: str, *, runtime_name: str | None = None) -> str | None:
|
|
240
|
+
"""Return the active wizard step slug when applicable."""
|
|
241
|
+
return self.runtime(runtime_name).current_wizard_step(job_id)
|
|
242
|
+
|
|
243
|
+
def resume_job(self, job_id: str, *, runtime_name: str | None = None) -> None:
|
|
244
|
+
"""Resume orchestration for a registered job."""
|
|
245
|
+
self.runtime(runtime_name).orchestration.resume_job(job_id)
|
|
246
|
+
|
|
247
|
+
def persist_job(self, job: Job, *, runtime_name: str | None = None) -> None:
|
|
248
|
+
"""Persist job state through a registered runtime executor."""
|
|
249
|
+
self.runtime(runtime_name).executor.persist_job(job)
|
|
250
|
+
|
|
251
|
+
def is_runtime_started(self, name: str | None = None) -> bool:
|
|
252
|
+
"""Return whether a registered runtime has been started."""
|
|
253
|
+
return self.runtime(name).is_started
|
|
254
|
+
|
|
255
|
+
def get_handle(self, name: str) -> RuntimeHandle:
|
|
256
|
+
"""Return the registry record for a named runtime."""
|
|
257
|
+
return self._runtimes.get(name)
|
|
258
|
+
|
|
259
|
+
def set_primary(self, name: str) -> None:
|
|
260
|
+
"""Choose the default runtime returned by :attr:`runtime`."""
|
|
261
|
+
self._runtimes.get(name) # validate
|
|
262
|
+
self._primary = name
|
|
263
|
+
|
|
264
|
+
def start(self, name: str, **options: Any) -> BaseRuntime:
|
|
265
|
+
"""Start a registered runtime using app settings merged with ``options``."""
|
|
266
|
+
handle = self._runtimes.get(name)
|
|
267
|
+
if handle.runtime.is_started:
|
|
268
|
+
return handle.runtime
|
|
269
|
+
merged = runtime_start_options(self.settings, **options)
|
|
270
|
+
handle.runtime.start(**merged)
|
|
271
|
+
return handle.runtime
|
|
272
|
+
|
|
273
|
+
def stop(self, name: str) -> None:
|
|
274
|
+
"""Stop a single runtime without shutting down shared storage."""
|
|
275
|
+
self._runtimes.get(name).runtime.stop()
|
|
276
|
+
|
|
277
|
+
def load_definitions(self, *, name: str | None = None) -> int:
|
|
278
|
+
"""
|
|
279
|
+
Hydrate definition catalogs for one or all registered runtimes.
|
|
280
|
+
|
|
281
|
+
Returns the total number of definition records touched.
|
|
282
|
+
"""
|
|
283
|
+
if name is not None:
|
|
284
|
+
handle = self._runtimes.get(name)
|
|
285
|
+
return load_definitions_for_repository(handle.runtime.repository, self.settings)
|
|
286
|
+
|
|
287
|
+
total = 0
|
|
288
|
+
for handle in self._runtimes.items():
|
|
289
|
+
total += load_definitions_for_repository(handle.runtime.repository, self.settings)
|
|
290
|
+
return total
|
|
291
|
+
|
|
292
|
+
def running(self) -> list[str]:
|
|
293
|
+
"""Return names of runtimes that are currently started."""
|
|
294
|
+
return [handle.name for handle in self._runtimes.items() if handle.is_started]
|
|
295
|
+
|
|
296
|
+
def shutdown(self) -> None:
|
|
297
|
+
"""Stop all runtimes and release shared storage when owned by the app."""
|
|
298
|
+
for handle in self._runtimes.items():
|
|
299
|
+
if handle.is_started:
|
|
300
|
+
handle.runtime.stop()
|
|
301
|
+
self._instance_manager.shutdown()
|
|
302
|
+
if self._owns_storage and self.storage.is_initialized:
|
|
303
|
+
self.storage.shutdown()
|
|
304
|
+
self._runtimes.clear()
|
|
305
|
+
self._primary = None
|
|
306
|
+
|
|
307
|
+
def __enter__(self) -> Self:
|
|
308
|
+
if not self._bootstrapped:
|
|
309
|
+
self.bootstrap()
|
|
310
|
+
return self
|
|
311
|
+
|
|
312
|
+
def __exit__(self, *exc: object) -> None:
|
|
313
|
+
self.shutdown()
|
|
314
|
+
|
|
315
|
+
def _build_runtime(self, kind: RuntimeKind, **options: Any) -> BaseRuntime:
|
|
316
|
+
if kind == "embedded":
|
|
317
|
+
from palm.runtimes.embedded import EmbeddedRuntime
|
|
318
|
+
|
|
319
|
+
return EmbeddedRuntime(
|
|
320
|
+
storage=self.storage,
|
|
321
|
+
instance_manager=self._instance_manager,
|
|
322
|
+
)
|
|
323
|
+
if kind == "daemon":
|
|
324
|
+
from palm.runtimes.daemon import DaemonRuntime
|
|
325
|
+
|
|
326
|
+
return DaemonRuntime(
|
|
327
|
+
storage=self.storage,
|
|
328
|
+
instance_manager=self._instance_manager,
|
|
329
|
+
)
|
|
330
|
+
if kind == "server":
|
|
331
|
+
from palm.runtimes.server import ServerRuntime
|
|
332
|
+
|
|
333
|
+
return ServerRuntime(
|
|
334
|
+
storage=self.storage,
|
|
335
|
+
instance_manager=self._instance_manager,
|
|
336
|
+
host=str(options.pop("host", "127.0.0.1")),
|
|
337
|
+
port=int(options.pop("port", 8080)),
|
|
338
|
+
)
|
|
339
|
+
raise ValueError(f"Unknown runtime kind {kind!r}")
|
|
340
|
+
|
|
341
|
+
def _default_runtime_name(self, kind: RuntimeKind) -> str:
|
|
342
|
+
if kind not in self._runtimes.names():
|
|
343
|
+
return kind
|
|
344
|
+
index = 1
|
|
345
|
+
while f"{kind}-{index}" in self._runtimes:
|
|
346
|
+
index += 1
|
|
347
|
+
return f"{kind}-{index}"
|
|
348
|
+
|
|
349
|
+
def _require_primary_name(self) -> str:
|
|
350
|
+
if self._primary is None:
|
|
351
|
+
raise RuntimeError("No primary runtime; call create_runtime() first")
|
|
352
|
+
return self._primary
|
|
353
|
+
|
|
354
|
+
def _require_bootstrapped(self) -> None:
|
|
355
|
+
if not self._bootstrapped:
|
|
356
|
+
raise RuntimeError("PalmApp is not bootstrapped; call bootstrap() first")
|
palm/app/bootstrap.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application bootstrap — plugin loading and definition catalog hydration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import importlib.util
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import palm.patterns # — autoload pattern apps
|
|
12
|
+
import palm.providers # — autoload provider apps
|
|
13
|
+
import palm.storages # noqa: F401 — autoload core storage apps
|
|
14
|
+
from palm.app.settings import PalmSettings
|
|
15
|
+
from palm.common.persistence.definition_repository import DefinitionRepository
|
|
16
|
+
from palm.common.storage import StorageFactory
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ensure_plugins() -> None:
|
|
20
|
+
"""Import extensible plugin packages so registries are populated."""
|
|
21
|
+
# Side-effect imports above register patterns, providers, and storages.
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def hydrate_definitions_from_storage(repository: DefinitionRepository) -> int:
|
|
26
|
+
"""Load flow/process definitions from storage into the in-memory cache."""
|
|
27
|
+
count = 0
|
|
28
|
+
for flow in repository.list_flows():
|
|
29
|
+
repository.register_flow(flow)
|
|
30
|
+
count += 1
|
|
31
|
+
for process in repository.list_processes():
|
|
32
|
+
repository.register_process(process)
|
|
33
|
+
count += 1
|
|
34
|
+
return count
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_definition_modules(
|
|
38
|
+
repository: DefinitionRepository,
|
|
39
|
+
*,
|
|
40
|
+
roots: list[Path],
|
|
41
|
+
) -> int:
|
|
42
|
+
"""Import ``register_definitions`` modules from the given directories."""
|
|
43
|
+
loaded = 0
|
|
44
|
+
seen: set[Path] = set()
|
|
45
|
+
for root in roots:
|
|
46
|
+
if not root.is_dir():
|
|
47
|
+
continue
|
|
48
|
+
for path in sorted(root.glob("*.py")):
|
|
49
|
+
if path.name.startswith("_") or path in seen:
|
|
50
|
+
continue
|
|
51
|
+
seen.add(path)
|
|
52
|
+
if _import_register(path, repository):
|
|
53
|
+
loaded += 1
|
|
54
|
+
return loaded
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def package_definition_roots(settings: PalmSettings) -> list[Path]:
|
|
58
|
+
"""Built-in example definition paths bundled with Palm."""
|
|
59
|
+
if not settings.load_example_definitions:
|
|
60
|
+
return []
|
|
61
|
+
package_root = Path(__file__).resolve().parents[3]
|
|
62
|
+
return [package_root / "examples" / "definitions"]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def all_definition_roots(settings: PalmSettings) -> list[Path]:
|
|
66
|
+
"""Merge configured, cwd, and packaged definition directories."""
|
|
67
|
+
roots: list[Path] = []
|
|
68
|
+
if settings.data_dir is not None:
|
|
69
|
+
roots.append(settings.data_dir / "definitions")
|
|
70
|
+
roots.append(Path.cwd() / "examples" / "definitions")
|
|
71
|
+
roots.extend(package_definition_roots(settings))
|
|
72
|
+
# Preserve order while deduplicating
|
|
73
|
+
unique: list[Path] = []
|
|
74
|
+
seen: set[Path] = set()
|
|
75
|
+
for root in roots:
|
|
76
|
+
resolved = root.resolve()
|
|
77
|
+
if resolved not in seen:
|
|
78
|
+
seen.add(resolved)
|
|
79
|
+
unique.append(root)
|
|
80
|
+
return unique
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def load_definitions_for_repository(
|
|
84
|
+
repository: DefinitionRepository,
|
|
85
|
+
settings: PalmSettings,
|
|
86
|
+
) -> int:
|
|
87
|
+
"""Hydrate storage-backed definitions and import code-defined catalogs."""
|
|
88
|
+
count = hydrate_definitions_from_storage(repository)
|
|
89
|
+
count += load_definition_modules(repository, roots=all_definition_roots(settings))
|
|
90
|
+
return count
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def runtime_start_options(settings: PalmSettings, **overrides: Any) -> dict[str, Any]:
|
|
94
|
+
"""Build keyword arguments for :meth:`~palm.runtimes.base.BaseRuntime.start`."""
|
|
95
|
+
options: dict[str, Any] = {
|
|
96
|
+
"storage_backend": settings.storage_backend,
|
|
97
|
+
"backend_options": StorageFactory.backend_options(settings=settings),
|
|
98
|
+
"observability": settings.observability,
|
|
99
|
+
"auth_enforce": settings.auth_enforce,
|
|
100
|
+
"auth_roles": list(settings.auth_roles),
|
|
101
|
+
}
|
|
102
|
+
if settings.max_concurrent_jobs is not None:
|
|
103
|
+
options["max_concurrent_jobs"] = settings.max_concurrent_jobs
|
|
104
|
+
options["enable_state_snapshot"] = settings.enable_state_snapshot
|
|
105
|
+
options["snapshot_on_status"] = list(settings.snapshot_on_status)
|
|
106
|
+
options["max_snapshots_per_instance"] = settings.max_snapshots_per_instance
|
|
107
|
+
options["max_loaded_instances"] = settings.max_loaded_instances
|
|
108
|
+
options["max_concurrent_active"] = settings.max_concurrent_active
|
|
109
|
+
options["reconcile_on_startup"] = settings.reconcile_instances_on_startup
|
|
110
|
+
options.update(overrides)
|
|
111
|
+
return options
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _import_register(path: Path, repository: DefinitionRepository) -> bool:
|
|
115
|
+
spec = importlib.util.spec_from_file_location(f"palm_app_definitions_{path.stem}", path)
|
|
116
|
+
if spec is None or spec.loader is None:
|
|
117
|
+
return False
|
|
118
|
+
module = importlib.util.module_from_spec(spec)
|
|
119
|
+
spec.loader.exec_module(module)
|
|
120
|
+
register = getattr(module, "register_definitions", None)
|
|
121
|
+
if not callable(register):
|
|
122
|
+
return False
|
|
123
|
+
register(repository)
|
|
124
|
+
return True
|
palm/app/cli_settings.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI settings resolution — env-first, flag overrides only when explicit.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from palm.app.settings import PalmSettings, SchedulerPolicy
|
|
10
|
+
from palm.common.storage import StorageFactory
|
|
11
|
+
|
|
12
|
+
DURABLE_STORAGE_BACKENDS: frozenset[str] = frozenset({"filesystem", "postgres", "mongodb"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_durable_storage(backend: str | None) -> bool:
|
|
16
|
+
"""Return whether ``backend`` persists data across process restarts."""
|
|
17
|
+
if not backend:
|
|
18
|
+
return False
|
|
19
|
+
return backend.strip().lower() in DURABLE_STORAGE_BACKENDS
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_cli_settings(
|
|
23
|
+
*,
|
|
24
|
+
storage_backend: str | None = None,
|
|
25
|
+
data_dir: Path | None = None,
|
|
26
|
+
settings: PalmSettings | None = None,
|
|
27
|
+
align_shared_storage: str | None = None,
|
|
28
|
+
enable_state_snapshot: bool | None = None,
|
|
29
|
+
max_loaded_instances: int | None = None,
|
|
30
|
+
max_concurrent_active: int | None = None,
|
|
31
|
+
default_scheduler: SchedulerPolicy | None = None,
|
|
32
|
+
) -> PalmSettings:
|
|
33
|
+
"""
|
|
34
|
+
Build CLI settings with environment variables as the base.
|
|
35
|
+
|
|
36
|
+
Precedence (highest last):
|
|
37
|
+
|
|
38
|
+
1. ``PALM_*`` environment variables via :class:`~palm.app.settings.PalmSettings`
|
|
39
|
+
2. ``--config`` file (loaded into ``PalmSettings`` before this merge)
|
|
40
|
+
3. Explicit ``settings`` argument (when passed to bootstrap)
|
|
41
|
+
4. CLI flags (only when not ``None``)
|
|
42
|
+
5. ``align_shared_storage`` — backend name from a pre-opened shared engine
|
|
43
|
+
"""
|
|
44
|
+
cfg = settings.model_copy(deep=True) if settings is not None else PalmSettings()
|
|
45
|
+
|
|
46
|
+
if storage_backend is not None:
|
|
47
|
+
cfg.storage_backend = storage_backend
|
|
48
|
+
if data_dir is not None:
|
|
49
|
+
cfg.data_dir = data_dir
|
|
50
|
+
if align_shared_storage is not None:
|
|
51
|
+
cfg.storage_backend = align_shared_storage
|
|
52
|
+
if enable_state_snapshot is not None:
|
|
53
|
+
cfg.enable_state_snapshot = enable_state_snapshot
|
|
54
|
+
if max_loaded_instances is not None:
|
|
55
|
+
cfg.max_loaded_instances = max_loaded_instances
|
|
56
|
+
if max_concurrent_active is not None:
|
|
57
|
+
cfg.max_concurrent_active = max_concurrent_active
|
|
58
|
+
if default_scheduler is not None:
|
|
59
|
+
cfg.default_scheduler = default_scheduler
|
|
60
|
+
|
|
61
|
+
if is_durable_storage(cfg.storage_backend) and cfg.data_dir is None:
|
|
62
|
+
cfg.data_dir = StorageFactory.resolve_data_dir(None)
|
|
63
|
+
return cfg
|
palm/app/registry.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime registry — named runtime handles managed by :class:`~palm.app.app.PalmApp`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING, Literal
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from palm.runtimes.base import BaseRuntime
|
|
13
|
+
|
|
14
|
+
RuntimeKind = Literal["embedded", "daemon", "server"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class RuntimeHandle:
|
|
19
|
+
"""A named runtime instance registered on the application."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
kind: RuntimeKind
|
|
23
|
+
runtime: BaseRuntime
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_started(self) -> bool:
|
|
27
|
+
return self.runtime.is_started
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RuntimeRegistry:
|
|
31
|
+
"""Thread-safe in-memory map of named runtimes."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._entries: dict[str, RuntimeHandle] = {}
|
|
35
|
+
self._lock = threading.RLock()
|
|
36
|
+
|
|
37
|
+
def __len__(self) -> int:
|
|
38
|
+
with self._lock:
|
|
39
|
+
return len(self._entries)
|
|
40
|
+
|
|
41
|
+
def __contains__(self, name: str) -> bool:
|
|
42
|
+
with self._lock:
|
|
43
|
+
return name in self._entries
|
|
44
|
+
|
|
45
|
+
def names(self) -> list[str]:
|
|
46
|
+
with self._lock:
|
|
47
|
+
return sorted(self._entries)
|
|
48
|
+
|
|
49
|
+
def register(self, handle: RuntimeHandle) -> RuntimeHandle:
|
|
50
|
+
with self._lock:
|
|
51
|
+
if handle.name in self._entries:
|
|
52
|
+
raise ValueError(f"Runtime {handle.name!r} is already registered")
|
|
53
|
+
self._entries[handle.name] = handle
|
|
54
|
+
return handle
|
|
55
|
+
|
|
56
|
+
def get(self, name: str) -> RuntimeHandle:
|
|
57
|
+
with self._lock:
|
|
58
|
+
try:
|
|
59
|
+
return self._entries[name]
|
|
60
|
+
except KeyError as exc:
|
|
61
|
+
available = ", ".join(sorted(self._entries)) or "(none)"
|
|
62
|
+
raise KeyError(
|
|
63
|
+
f"Unknown runtime {name!r}. Registered: {available}"
|
|
64
|
+
) from exc
|
|
65
|
+
|
|
66
|
+
def items(self) -> list[RuntimeHandle]:
|
|
67
|
+
with self._lock:
|
|
68
|
+
return [self._entries[name] for name in sorted(self._entries)]
|
|
69
|
+
|
|
70
|
+
def clear(self) -> None:
|
|
71
|
+
with self._lock:
|
|
72
|
+
self._entries.clear()
|