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.
Files changed (196) hide show
  1. palm/__init__.py +23 -0
  2. palm/app/__init__.py +22 -0
  3. palm/app/app.py +356 -0
  4. palm/app/bootstrap.py +124 -0
  5. palm/app/cli_settings.py +63 -0
  6. palm/app/registry.py +72 -0
  7. palm/app/resolvers.py +43 -0
  8. palm/app/session.py +67 -0
  9. palm/app/settings.py +61 -0
  10. palm/backends/__init__.py +5 -0
  11. palm/backends/behavior_tree/__init__.py +7 -0
  12. palm/backends/behavior_tree/runner.py +68 -0
  13. palm/common/__init__.py +91 -0
  14. palm/common/exceptions.py +50 -0
  15. palm/common/executions/__init__.py +12 -0
  16. palm/common/executions/executor.py +413 -0
  17. palm/common/executions/flow_submission.py +114 -0
  18. palm/common/executions/process_submission.py +55 -0
  19. palm/common/hooks/__init__.py +6 -0
  20. palm/common/hooks/instance_persistence.py +68 -0
  21. palm/common/hooks/state_snapshot.py +72 -0
  22. palm/common/managers/__init__.py +15 -0
  23. palm/common/managers/base.py +25 -0
  24. palm/common/managers/instance_manager.py +341 -0
  25. palm/common/patterns/__init__.py +6 -0
  26. palm/common/patterns/build_context.py +23 -0
  27. palm/common/patterns/builder.py +51 -0
  28. palm/common/persistence/__init__.py +23 -0
  29. palm/common/persistence/definition_repository.py +299 -0
  30. palm/common/persistence/instance_repository.py +193 -0
  31. palm/common/persistence/instance_resume.py +16 -0
  32. palm/common/persistence/instance_sync.py +110 -0
  33. palm/common/plans/__init__.py +7 -0
  34. palm/common/plans/execution_plan.py +59 -0
  35. palm/common/plans/process_plan.py +32 -0
  36. palm/common/plans/registry.py +68 -0
  37. palm/common/storage/__init__.py +5 -0
  38. palm/common/storage/factory.py +141 -0
  39. palm/core/__init__.py +112 -0
  40. palm/core/auth/__init__.py +9 -0
  41. palm/core/auth/engine.py +57 -0
  42. palm/core/base.py +54 -0
  43. palm/core/behavior_tree/__init__.py +58 -0
  44. palm/core/behavior_tree/base.py +92 -0
  45. palm/core/behavior_tree/base_pattern.py +39 -0
  46. palm/core/behavior_tree/composite.py +24 -0
  47. palm/core/behavior_tree/decorator.py +37 -0
  48. palm/core/behavior_tree/engine.py +118 -0
  49. palm/core/behavior_tree/exceptions.py +21 -0
  50. palm/core/behavior_tree/leaf.py +20 -0
  51. palm/core/behavior_tree/nodes/__init__.py +27 -0
  52. palm/core/behavior_tree/nodes/composite/__init__.py +10 -0
  53. palm/core/behavior_tree/nodes/composite/parallel_node.py +74 -0
  54. palm/core/behavior_tree/nodes/composite/selector_node.py +35 -0
  55. palm/core/behavior_tree/nodes/composite/sequence_node.py +35 -0
  56. palm/core/behavior_tree/nodes/decorator/__init__.py +7 -0
  57. palm/core/behavior_tree/nodes/decorator/inverter_node.py +21 -0
  58. palm/core/behavior_tree/nodes/decorator/repeat_node.py +38 -0
  59. palm/core/behavior_tree/nodes/decorator/retry_node.py +33 -0
  60. palm/core/behavior_tree/nodes/leaf/__init__.py +11 -0
  61. palm/core/behavior_tree/nodes/leaf/action_node.py +31 -0
  62. palm/core/behavior_tree/nodes/leaf/condition_node.py +26 -0
  63. palm/core/behavior_tree/nodes/leaf/interactive_leaf.py +45 -0
  64. palm/core/behavior_tree/root.py +19 -0
  65. palm/core/context/__init__.py +10 -0
  66. palm/core/context/base_state.py +43 -0
  67. palm/core/context/engine.py +110 -0
  68. palm/core/event/__init__.py +9 -0
  69. palm/core/event/engine.py +52 -0
  70. palm/core/exceptions.py +56 -0
  71. palm/core/orchestration/__init__.py +35 -0
  72. palm/core/orchestration/drive.py +38 -0
  73. palm/core/orchestration/engine.py +316 -0
  74. palm/core/orchestration/events.py +17 -0
  75. palm/core/orchestration/exceptions.py +47 -0
  76. palm/core/orchestration/execution/__init__.py +5 -0
  77. palm/core/orchestration/execution/base_runner.py +18 -0
  78. palm/core/orchestration/execution_context.py +16 -0
  79. palm/core/orchestration/hooks.py +65 -0
  80. palm/core/orchestration/input_capable.py +26 -0
  81. palm/core/orchestration/job.py +142 -0
  82. palm/core/orchestration/job_state.py +40 -0
  83. palm/core/orchestration/mode/__init__.py +8 -0
  84. palm/core/orchestration/mode/base_mode.py +52 -0
  85. palm/core/orchestration/mode/unconfigured_mode.py +41 -0
  86. palm/core/orchestration/run_result.py +24 -0
  87. palm/core/registry.py +73 -0
  88. palm/core/resource/__init__.py +10 -0
  89. palm/core/resource/base_provider.py +29 -0
  90. palm/core/resource/engine.py +39 -0
  91. palm/core/storage/__init__.py +10 -0
  92. palm/core/storage/base_backend.py +53 -0
  93. palm/core/storage/engine.py +93 -0
  94. palm/definitions/__init__.py +10 -0
  95. palm/definitions/flow.py +67 -0
  96. palm/definitions/process.py +70 -0
  97. palm/instances/__init__.py +9 -0
  98. palm/instances/process_instance.py +113 -0
  99. palm/instances/state_snapshot.py +80 -0
  100. palm/instances/status_history.py +42 -0
  101. palm/patterns/__init__.py +15 -0
  102. palm/patterns/_apps.py +18 -0
  103. palm/patterns/_registry.py +134 -0
  104. palm/patterns/dag/__init__.py +10 -0
  105. palm/patterns/dag/builder.py +26 -0
  106. palm/patterns/dag/pattern.py +21 -0
  107. palm/patterns/dag/registry.py +9 -0
  108. palm/patterns/etl/__init__.py +10 -0
  109. palm/patterns/etl/builder.py +26 -0
  110. palm/patterns/etl/pattern.py +26 -0
  111. palm/patterns/etl/registry.py +9 -0
  112. palm/patterns/wizard/__init__.py +63 -0
  113. palm/patterns/wizard/action_leaf.py +94 -0
  114. palm/patterns/wizard/backtrack.py +71 -0
  115. palm/patterns/wizard/base.py +10 -0
  116. palm/patterns/wizard/builder.py +170 -0
  117. palm/patterns/wizard/commit_leaf.py +113 -0
  118. palm/patterns/wizard/config.py +156 -0
  119. palm/patterns/wizard/events.py +19 -0
  120. palm/patterns/wizard/handler.py +94 -0
  121. palm/patterns/wizard/keys.py +23 -0
  122. palm/patterns/wizard/options.py +66 -0
  123. palm/patterns/wizard/pattern.py +143 -0
  124. palm/patterns/wizard/persistence.py +54 -0
  125. palm/patterns/wizard/registry.py +24 -0
  126. palm/patterns/wizard/resume.py +34 -0
  127. palm/patterns/wizard/step_kinds.py +13 -0
  128. palm/patterns/wizard/step_leaf.py +98 -0
  129. palm/patterns/wizard/submission.py +21 -0
  130. palm/patterns/wizard/summary_leaf.py +78 -0
  131. palm/patterns/wizard/tree.py +78 -0
  132. palm/patterns/wizard/validation.py +137 -0
  133. palm/providers/__init__.py +13 -0
  134. palm/providers/_apps.py +14 -0
  135. palm/providers/graphql/__init__.py +6 -0
  136. palm/providers/graphql/provider.py +20 -0
  137. palm/providers/graphql/registry.py +6 -0
  138. palm/providers/postgres/__init__.py +6 -0
  139. palm/providers/postgres/provider.py +20 -0
  140. palm/providers/postgres/registry.py +6 -0
  141. palm/providers/rest/__init__.py +6 -0
  142. palm/providers/rest/provider.py +20 -0
  143. palm/providers/rest/registry.py +6 -0
  144. palm/runtimes/__init__.py +24 -0
  145. palm/runtimes/base.py +290 -0
  146. palm/runtimes/cli.py +118 -0
  147. palm/runtimes/cli_pkg/__init__.py +6 -0
  148. palm/runtimes/cli_pkg/actions.py +68 -0
  149. palm/runtimes/cli_pkg/args.py +211 -0
  150. palm/runtimes/cli_pkg/bootstrap.py +53 -0
  151. palm/runtimes/cli_pkg/commands/__init__.py +5 -0
  152. palm/runtimes/cli_pkg/commands/registry.py +413 -0
  153. palm/runtimes/cli_pkg/completion.py +184 -0
  154. palm/runtimes/cli_pkg/context.py +80 -0
  155. palm/runtimes/cli_pkg/display.py +178 -0
  156. palm/runtimes/cli_pkg/doctor.py +112 -0
  157. palm/runtimes/cli_pkg/instance_ops.py +126 -0
  158. palm/runtimes/cli_pkg/instances.py +52 -0
  159. palm/runtimes/cli_pkg/output.py +33 -0
  160. palm/runtimes/cli_pkg/repl.py +88 -0
  161. palm/runtimes/cli_pkg/settings.py +5 -0
  162. palm/runtimes/cli_pkg/startup.py +67 -0
  163. palm/runtimes/cli_pkg/version_info.py +59 -0
  164. palm/runtimes/daemon.py +46 -0
  165. palm/runtimes/embedded.py +27 -0
  166. palm/runtimes/hooks.py +115 -0
  167. palm/runtimes/host.py +41 -0
  168. palm/runtimes/schedulers/__init__.py +6 -0
  169. palm/runtimes/schedulers/inline.py +61 -0
  170. palm/runtimes/schedulers/queued.py +133 -0
  171. palm/runtimes/server/__init__.py +6 -0
  172. palm/runtimes/server/auth.py +37 -0
  173. palm/runtimes/server/http.py +281 -0
  174. palm/runtimes/server/runtime.py +195 -0
  175. palm/runtimes/wiring.py +59 -0
  176. palm/states/__init__.py +10 -0
  177. palm/states/blackboard_state.py +40 -0
  178. palm/storages/__init__.py +21 -0
  179. palm/storages/_apps.py +20 -0
  180. palm/storages/filesystem/__init__.py +6 -0
  181. palm/storages/filesystem/backend.py +175 -0
  182. palm/storages/filesystem/registry.py +6 -0
  183. palm/storages/memory/__init__.py +6 -0
  184. palm/storages/memory/backend.py +42 -0
  185. palm/storages/memory/registry.py +6 -0
  186. palm/storages/mongodb/__init__.py +6 -0
  187. palm/storages/mongodb/backend.py +81 -0
  188. palm/storages/mongodb/registry.py +6 -0
  189. palm/storages/postgres/__init__.py +6 -0
  190. palm/storages/postgres/backend.py +36 -0
  191. palm/storages/postgres/registry.py +6 -0
  192. palm/utils/__init__.py +5 -0
  193. palmengine-0.7.4.dist-info/METADATA +370 -0
  194. palmengine-0.7.4.dist-info/RECORD +196 -0
  195. palmengine-0.7.4.dist-info/WHEEL +4 -0
  196. 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
@@ -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()