py-data-engine 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 (200) hide show
  1. data_engine/__init__.py +37 -0
  2. data_engine/application/__init__.py +39 -0
  3. data_engine/application/actions.py +42 -0
  4. data_engine/application/catalog.py +151 -0
  5. data_engine/application/control.py +213 -0
  6. data_engine/application/details.py +73 -0
  7. data_engine/application/runtime.py +449 -0
  8. data_engine/application/workspace.py +62 -0
  9. data_engine/authoring/__init__.py +14 -0
  10. data_engine/authoring/builder.py +31 -0
  11. data_engine/authoring/execution/__init__.py +6 -0
  12. data_engine/authoring/execution/app.py +6 -0
  13. data_engine/authoring/execution/context.py +82 -0
  14. data_engine/authoring/execution/continuous.py +176 -0
  15. data_engine/authoring/execution/grouped.py +106 -0
  16. data_engine/authoring/execution/logging.py +83 -0
  17. data_engine/authoring/execution/polling.py +135 -0
  18. data_engine/authoring/execution/runner.py +210 -0
  19. data_engine/authoring/execution/single.py +171 -0
  20. data_engine/authoring/flow.py +361 -0
  21. data_engine/authoring/helpers.py +160 -0
  22. data_engine/authoring/model.py +59 -0
  23. data_engine/authoring/primitives.py +430 -0
  24. data_engine/authoring/services.py +42 -0
  25. data_engine/devtools/__init__.py +3 -0
  26. data_engine/devtools/project_ast_map.py +503 -0
  27. data_engine/docs/__init__.py +1 -0
  28. data_engine/docs/sphinx_source/_static/custom.css +13 -0
  29. data_engine/docs/sphinx_source/api.rst +42 -0
  30. data_engine/docs/sphinx_source/conf.py +37 -0
  31. data_engine/docs/sphinx_source/guides/app-runtime-and-workspaces.md +397 -0
  32. data_engine/docs/sphinx_source/guides/authoring-flow-modules.md +215 -0
  33. data_engine/docs/sphinx_source/guides/configuring-flows.md +185 -0
  34. data_engine/docs/sphinx_source/guides/core-concepts.md +208 -0
  35. data_engine/docs/sphinx_source/guides/database-methods.md +107 -0
  36. data_engine/docs/sphinx_source/guides/duckdb-helpers.md +462 -0
  37. data_engine/docs/sphinx_source/guides/flow-context.md +538 -0
  38. data_engine/docs/sphinx_source/guides/flow-methods.md +206 -0
  39. data_engine/docs/sphinx_source/guides/getting-started.md +271 -0
  40. data_engine/docs/sphinx_source/guides/project-inventory.md +5683 -0
  41. data_engine/docs/sphinx_source/guides/project-map.md +118 -0
  42. data_engine/docs/sphinx_source/guides/recipes.md +268 -0
  43. data_engine/docs/sphinx_source/index.rst +22 -0
  44. data_engine/domain/__init__.py +92 -0
  45. data_engine/domain/actions.py +69 -0
  46. data_engine/domain/catalog.py +128 -0
  47. data_engine/domain/details.py +214 -0
  48. data_engine/domain/diagnostics.py +56 -0
  49. data_engine/domain/errors.py +104 -0
  50. data_engine/domain/inspection.py +99 -0
  51. data_engine/domain/logs.py +118 -0
  52. data_engine/domain/operations.py +172 -0
  53. data_engine/domain/operator.py +72 -0
  54. data_engine/domain/runs.py +155 -0
  55. data_engine/domain/runtime.py +279 -0
  56. data_engine/domain/source_state.py +17 -0
  57. data_engine/domain/support.py +54 -0
  58. data_engine/domain/time.py +23 -0
  59. data_engine/domain/workspace.py +159 -0
  60. data_engine/flow_modules/__init__.py +1 -0
  61. data_engine/flow_modules/flow_module_compiler.py +179 -0
  62. data_engine/flow_modules/flow_module_loader.py +201 -0
  63. data_engine/helpers/__init__.py +25 -0
  64. data_engine/helpers/duckdb.py +705 -0
  65. data_engine/hosts/__init__.py +1 -0
  66. data_engine/hosts/daemon/__init__.py +23 -0
  67. data_engine/hosts/daemon/app.py +221 -0
  68. data_engine/hosts/daemon/bootstrap.py +69 -0
  69. data_engine/hosts/daemon/client.py +465 -0
  70. data_engine/hosts/daemon/commands.py +64 -0
  71. data_engine/hosts/daemon/composition.py +310 -0
  72. data_engine/hosts/daemon/constants.py +15 -0
  73. data_engine/hosts/daemon/entrypoints.py +97 -0
  74. data_engine/hosts/daemon/lifecycle.py +191 -0
  75. data_engine/hosts/daemon/manager.py +272 -0
  76. data_engine/hosts/daemon/ownership.py +126 -0
  77. data_engine/hosts/daemon/runtime_commands.py +188 -0
  78. data_engine/hosts/daemon/runtime_control.py +31 -0
  79. data_engine/hosts/daemon/server.py +84 -0
  80. data_engine/hosts/daemon/shared_state.py +147 -0
  81. data_engine/hosts/daemon/state_sync.py +101 -0
  82. data_engine/platform/__init__.py +1 -0
  83. data_engine/platform/identity.py +35 -0
  84. data_engine/platform/local_settings.py +146 -0
  85. data_engine/platform/theme.py +259 -0
  86. data_engine/platform/workspace_models.py +190 -0
  87. data_engine/platform/workspace_policy.py +333 -0
  88. data_engine/runtime/__init__.py +1 -0
  89. data_engine/runtime/file_watch.py +185 -0
  90. data_engine/runtime/ledger_models.py +116 -0
  91. data_engine/runtime/runtime_db.py +938 -0
  92. data_engine/runtime/shared_state.py +523 -0
  93. data_engine/services/__init__.py +49 -0
  94. data_engine/services/daemon.py +64 -0
  95. data_engine/services/daemon_state.py +40 -0
  96. data_engine/services/flow_catalog.py +102 -0
  97. data_engine/services/flow_execution.py +48 -0
  98. data_engine/services/ledger.py +85 -0
  99. data_engine/services/logs.py +65 -0
  100. data_engine/services/runtime_binding.py +105 -0
  101. data_engine/services/runtime_execution.py +126 -0
  102. data_engine/services/runtime_history.py +62 -0
  103. data_engine/services/settings.py +58 -0
  104. data_engine/services/shared_state.py +28 -0
  105. data_engine/services/theme.py +59 -0
  106. data_engine/services/workspace_provisioning.py +224 -0
  107. data_engine/services/workspaces.py +74 -0
  108. data_engine/ui/__init__.py +3 -0
  109. data_engine/ui/cli/__init__.py +19 -0
  110. data_engine/ui/cli/app.py +161 -0
  111. data_engine/ui/cli/commands_doctor.py +178 -0
  112. data_engine/ui/cli/commands_run.py +80 -0
  113. data_engine/ui/cli/commands_start.py +100 -0
  114. data_engine/ui/cli/commands_workspace.py +97 -0
  115. data_engine/ui/cli/dependencies.py +44 -0
  116. data_engine/ui/cli/parser.py +56 -0
  117. data_engine/ui/gui/__init__.py +25 -0
  118. data_engine/ui/gui/app.py +116 -0
  119. data_engine/ui/gui/bootstrap.py +487 -0
  120. data_engine/ui/gui/bootstrapper.py +140 -0
  121. data_engine/ui/gui/cache_models.py +23 -0
  122. data_engine/ui/gui/control_support.py +185 -0
  123. data_engine/ui/gui/controllers/__init__.py +6 -0
  124. data_engine/ui/gui/controllers/flows.py +439 -0
  125. data_engine/ui/gui/controllers/runtime.py +245 -0
  126. data_engine/ui/gui/dialogs/__init__.py +12 -0
  127. data_engine/ui/gui/dialogs/messages.py +88 -0
  128. data_engine/ui/gui/dialogs/previews.py +222 -0
  129. data_engine/ui/gui/helpers/__init__.py +62 -0
  130. data_engine/ui/gui/helpers/inspection.py +81 -0
  131. data_engine/ui/gui/helpers/lifecycle.py +112 -0
  132. data_engine/ui/gui/helpers/scroll.py +28 -0
  133. data_engine/ui/gui/helpers/theming.py +87 -0
  134. data_engine/ui/gui/icons/dark_light.svg +12 -0
  135. data_engine/ui/gui/icons/documentation.svg +1 -0
  136. data_engine/ui/gui/icons/failed.svg +3 -0
  137. data_engine/ui/gui/icons/group.svg +4 -0
  138. data_engine/ui/gui/icons/home.svg +2 -0
  139. data_engine/ui/gui/icons/manual.svg +2 -0
  140. data_engine/ui/gui/icons/poll.svg +2 -0
  141. data_engine/ui/gui/icons/schedule.svg +4 -0
  142. data_engine/ui/gui/icons/settings.svg +2 -0
  143. data_engine/ui/gui/icons/started.svg +3 -0
  144. data_engine/ui/gui/icons/success.svg +3 -0
  145. data_engine/ui/gui/icons/view-log.svg +3 -0
  146. data_engine/ui/gui/icons.py +50 -0
  147. data_engine/ui/gui/launcher.py +48 -0
  148. data_engine/ui/gui/presenters/__init__.py +72 -0
  149. data_engine/ui/gui/presenters/docs.py +140 -0
  150. data_engine/ui/gui/presenters/logs.py +58 -0
  151. data_engine/ui/gui/presenters/runtime_projection.py +29 -0
  152. data_engine/ui/gui/presenters/sidebar.py +88 -0
  153. data_engine/ui/gui/presenters/steps.py +148 -0
  154. data_engine/ui/gui/presenters/workspace.py +39 -0
  155. data_engine/ui/gui/presenters/workspace_binding.py +75 -0
  156. data_engine/ui/gui/presenters/workspace_settings.py +182 -0
  157. data_engine/ui/gui/preview_models.py +37 -0
  158. data_engine/ui/gui/render_support.py +241 -0
  159. data_engine/ui/gui/rendering/__init__.py +12 -0
  160. data_engine/ui/gui/rendering/artifacts.py +95 -0
  161. data_engine/ui/gui/rendering/icons.py +50 -0
  162. data_engine/ui/gui/runtime.py +47 -0
  163. data_engine/ui/gui/state_support.py +193 -0
  164. data_engine/ui/gui/support.py +214 -0
  165. data_engine/ui/gui/surface.py +209 -0
  166. data_engine/ui/gui/theme.py +720 -0
  167. data_engine/ui/gui/widgets/__init__.py +34 -0
  168. data_engine/ui/gui/widgets/config.py +41 -0
  169. data_engine/ui/gui/widgets/logs.py +62 -0
  170. data_engine/ui/gui/widgets/panels.py +507 -0
  171. data_engine/ui/gui/widgets/sidebar.py +130 -0
  172. data_engine/ui/gui/widgets/steps.py +84 -0
  173. data_engine/ui/tui/__init__.py +5 -0
  174. data_engine/ui/tui/app.py +222 -0
  175. data_engine/ui/tui/bootstrap.py +475 -0
  176. data_engine/ui/tui/bootstrapper.py +117 -0
  177. data_engine/ui/tui/controllers/__init__.py +6 -0
  178. data_engine/ui/tui/controllers/flows.py +349 -0
  179. data_engine/ui/tui/controllers/runtime.py +167 -0
  180. data_engine/ui/tui/runtime.py +34 -0
  181. data_engine/ui/tui/state_support.py +141 -0
  182. data_engine/ui/tui/support.py +63 -0
  183. data_engine/ui/tui/theme.py +204 -0
  184. data_engine/ui/tui/widgets.py +123 -0
  185. data_engine/views/__init__.py +109 -0
  186. data_engine/views/actions.py +80 -0
  187. data_engine/views/artifacts.py +58 -0
  188. data_engine/views/flow_display.py +69 -0
  189. data_engine/views/logs.py +54 -0
  190. data_engine/views/models.py +96 -0
  191. data_engine/views/presentation.py +133 -0
  192. data_engine/views/runs.py +62 -0
  193. data_engine/views/state.py +39 -0
  194. data_engine/views/status.py +13 -0
  195. data_engine/views/text.py +109 -0
  196. py_data_engine-0.1.0.dist-info/METADATA +330 -0
  197. py_data_engine-0.1.0.dist-info/RECORD +200 -0
  198. py_data_engine-0.1.0.dist-info/WHEEL +5 -0
  199. py_data_engine-0.1.0.dist-info/entry_points.txt +2 -0
  200. py_data_engine-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,310 @@
1
+ """Composition helpers for the daemon host."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ import threading
8
+ from uuid import uuid4
9
+ from typing import Callable
10
+
11
+ from data_engine.hosts.daemon.shared_state import DaemonSharedStateAdapter
12
+ from data_engine.platform.workspace_models import WorkspacePaths, machine_id_text
13
+ from data_engine.runtime.runtime_db import RuntimeLedger
14
+ from data_engine.services.flow_catalog import FlowCatalogService
15
+ from data_engine.services.flow_execution import FlowExecutionService
16
+ from data_engine.services.ledger import LedgerService
17
+ from data_engine.services.runtime_execution import RuntimeExecutionService
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class DaemonHostDependencyFactories:
22
+ """Constructor seam for daemon host collaborators."""
23
+
24
+ flow_catalog_service_factory: Callable[[], FlowCatalogService]
25
+ flow_execution_service_factory: Callable[[], FlowExecutionService]
26
+ runtime_execution_service_factory: Callable[[], RuntimeExecutionService]
27
+ shared_state_adapter_factory: Callable[[], DaemonSharedStateAdapter] = field(default=DaemonSharedStateAdapter)
28
+
29
+
30
+ def default_daemon_host_dependency_factories() -> DaemonHostDependencyFactories:
31
+ """Build the default daemon-host constructor bundle."""
32
+ return DaemonHostDependencyFactories(
33
+ flow_catalog_service_factory=FlowCatalogService,
34
+ flow_execution_service_factory=FlowExecutionService,
35
+ runtime_execution_service_factory=RuntimeExecutionService,
36
+ shared_state_adapter_factory=DaemonSharedStateAdapter,
37
+ )
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class DaemonHostDependencies:
42
+ """Concrete collaborators used by one daemon host instance."""
43
+
44
+ runtime_ledger: RuntimeLedger
45
+ flow_catalog_service: FlowCatalogService
46
+ flow_execution_service: FlowExecutionService
47
+ runtime_execution_service: RuntimeExecutionService
48
+ shared_state_adapter: DaemonSharedStateAdapter
49
+
50
+ @classmethod
51
+ def build_default(
52
+ cls,
53
+ paths: WorkspacePaths,
54
+ *,
55
+ ledger_service: LedgerService | None = None,
56
+ factories: DaemonHostDependencyFactories | None = None,
57
+ ) -> "DaemonHostDependencies":
58
+ """Build the default dependency bundle for one workspace host."""
59
+ ledger_service = ledger_service or LedgerService()
60
+ factories = factories or default_daemon_host_dependency_factories()
61
+ return cls(
62
+ runtime_ledger=ledger_service.open_for_workspace(paths.workspace_root),
63
+ flow_catalog_service=factories.flow_catalog_service_factory(),
64
+ flow_execution_service=factories.flow_execution_service_factory(),
65
+ runtime_execution_service=factories.runtime_execution_service_factory(),
66
+ shared_state_adapter=factories.shared_state_adapter_factory(),
67
+ )
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class DaemonHostIdentity:
72
+ """Process and machine identity for one daemon host instance."""
73
+
74
+ machine_id: str
75
+ daemon_id: str
76
+ pid: int
77
+
78
+ @classmethod
79
+ def current_process(cls) -> "DaemonHostIdentity":
80
+ """Build the current-process identity for one daemon host."""
81
+ return cls(
82
+ machine_id=machine_id_text(),
83
+ daemon_id=uuid4().hex,
84
+ pid=os.getpid(),
85
+ )
86
+
87
+
88
+ @dataclass
89
+ class DaemonHostState:
90
+ """Mutable state for a fresh daemon host instance."""
91
+
92
+ status: str
93
+ last_checkpoint_at_utc: str
94
+ workspace_owned: bool
95
+ leased_by_machine_id: str | None
96
+ runtime_active: bool
97
+ runtime_stopping: bool
98
+ engine_starting: bool
99
+ engine_thread: threading.Thread | None
100
+ engine_runtime_stop_event: threading.Event
101
+ engine_flow_stop_event: threading.Event
102
+ pending_manual_run_names: set[str]
103
+ manual_run_threads: dict[str, threading.Thread]
104
+ manual_stop_events: dict[str, threading.Event]
105
+ shutdown_event: threading.Event
106
+ checkpoint_thread: threading.Thread | None
107
+ consecutive_checkpoint_failures: int
108
+ listener: object | None
109
+
110
+ @classmethod
111
+ def build(cls, *, started_at_utc: str) -> "DaemonHostState":
112
+ """Build the default mutable state for a fresh daemon host."""
113
+ return cls(
114
+ status="starting",
115
+ last_checkpoint_at_utc=started_at_utc,
116
+ workspace_owned=False,
117
+ leased_by_machine_id=None,
118
+ runtime_active=False,
119
+ runtime_stopping=False,
120
+ engine_starting=False,
121
+ engine_thread=None,
122
+ engine_runtime_stop_event=threading.Event(),
123
+ engine_flow_stop_event=threading.Event(),
124
+ pending_manual_run_names=set(),
125
+ manual_run_threads={},
126
+ manual_stop_events={},
127
+ shutdown_event=threading.Event(),
128
+ checkpoint_thread=None,
129
+ consecutive_checkpoint_failures=0,
130
+ listener=None,
131
+ )
132
+
133
+ def claim_workspace(self) -> None:
134
+ """Mark the current daemon as owning the workspace."""
135
+ self.workspace_owned = True
136
+ self.leased_by_machine_id = None
137
+ self.status = "idle"
138
+
139
+ def release_workspace(self, *, leased_by_machine_id: str | None = None, status: str | None = None) -> None:
140
+ """Mark the current daemon as no longer owning the workspace."""
141
+ self.workspace_owned = False
142
+ self.leased_by_machine_id = leased_by_machine_id
143
+ if status is not None:
144
+ self.status = status
145
+
146
+ def begin_runtime(self, *, status: str = "running") -> None:
147
+ """Mark the engine runtime as active and running."""
148
+ self.engine_starting = False
149
+ self.runtime_active = True
150
+ self.runtime_stopping = False
151
+ self.status = status
152
+
153
+ def stop_runtime(self, *, status: str = "stopping") -> None:
154
+ """Mark the engine runtime as stopping."""
155
+ self.engine_starting = False
156
+ self.runtime_stopping = True
157
+ self.status = status
158
+
159
+ def end_runtime(self, *, status: str = "idle") -> None:
160
+ """Mark the engine runtime as inactive."""
161
+ self.engine_starting = False
162
+ self.runtime_active = False
163
+ self.runtime_stopping = False
164
+ if self.status != "failed":
165
+ self.status = status
166
+
167
+ def set_checkpoint_time(self, checkpoint_at_utc: str, *, status: str | None = None) -> None:
168
+ """Update the last successful checkpoint timestamp."""
169
+ self.last_checkpoint_at_utc = checkpoint_at_utc
170
+ if status is not None:
171
+ self.status = status
172
+
173
+ def set_leased_by_machine_id(self, machine_id: str | None) -> None:
174
+ """Update the current lease owner identifier."""
175
+ self.leased_by_machine_id = machine_id
176
+
177
+ def increment_checkpoint_failures(self) -> int:
178
+ """Increment the repeated-checkpoint failure counter."""
179
+ self.consecutive_checkpoint_failures += 1
180
+ return self.consecutive_checkpoint_failures
181
+
182
+ def reset_checkpoint_failures(self) -> None:
183
+ """Reset the repeated-checkpoint failure counter."""
184
+ self.consecutive_checkpoint_failures = 0
185
+
186
+ def set_engine_threads(
187
+ self,
188
+ *,
189
+ runtime_stop_event: threading.Event,
190
+ flow_stop_event: threading.Event,
191
+ engine_thread: threading.Thread | None = None,
192
+ ) -> None:
193
+ """Replace the active engine coordination objects."""
194
+ self.engine_runtime_stop_event = runtime_stop_event
195
+ self.engine_flow_stop_event = flow_stop_event
196
+ self.engine_thread = engine_thread
197
+
198
+ def reserve_engine_start(self) -> bool:
199
+ """Reserve engine startup so concurrent start requests collapse to one attempt."""
200
+ if self.runtime_active or self.engine_starting:
201
+ return False
202
+ self.engine_starting = True
203
+ return True
204
+
205
+ def clear_engine_start_reservation(self) -> None:
206
+ """Clear any in-progress engine startup reservation."""
207
+ self.engine_starting = False
208
+
209
+ def reserve_manual_run(self, name: str) -> bool:
210
+ """Reserve one manual run name before flow loading starts."""
211
+ if name in self.pending_manual_run_names:
212
+ return False
213
+ self.pending_manual_run_names.add(name)
214
+ return True
215
+
216
+ def clear_manual_run_reservation(self, name: str) -> None:
217
+ """Clear one in-progress manual run reservation."""
218
+ self.pending_manual_run_names.discard(name)
219
+
220
+ def register_manual_run(self, name: str, *, thread: threading.Thread, stop_event: threading.Event) -> None:
221
+ """Register one manual run and its stop signal."""
222
+ self.pending_manual_run_names.discard(name)
223
+ self.manual_run_threads[name] = thread
224
+ self.manual_stop_events[name] = stop_event
225
+
226
+ def unregister_manual_run(self, name: str) -> None:
227
+ """Remove one completed manual run."""
228
+ self.pending_manual_run_names.discard(name)
229
+ self.manual_run_threads.pop(name, None)
230
+ self.manual_stop_events.pop(name, None)
231
+
232
+ def set_listener(self, listener: object | None) -> None:
233
+ """Update the active listener object."""
234
+ self.listener = listener
235
+
236
+
237
+ class DaemonHostFacade:
238
+ """Explicit high-level host-state facade over the mutable daemon state object."""
239
+
240
+ def __init__(self, state: DaemonHostState) -> None:
241
+ self.state = state
242
+
243
+ @property
244
+ def status(self) -> str:
245
+ return self.state.status
246
+
247
+ @status.setter
248
+ def status(self, value: str) -> None:
249
+ self.state.status = value
250
+
251
+ @property
252
+ def workspace_owned(self) -> bool:
253
+ return self.state.workspace_owned
254
+
255
+ @workspace_owned.setter
256
+ def workspace_owned(self, value: bool) -> None:
257
+ self.state.workspace_owned = value
258
+
259
+ @property
260
+ def leased_by_machine_id(self) -> str | None:
261
+ return self.state.leased_by_machine_id
262
+
263
+ @leased_by_machine_id.setter
264
+ def leased_by_machine_id(self, value: str | None) -> None:
265
+ self.state.leased_by_machine_id = value
266
+
267
+ @property
268
+ def runtime_active(self) -> bool:
269
+ return self.state.runtime_active
270
+
271
+ @runtime_active.setter
272
+ def runtime_active(self, value: bool) -> None:
273
+ self.state.runtime_active = value
274
+
275
+ @property
276
+ def runtime_stopping(self) -> bool:
277
+ return self.state.runtime_stopping
278
+
279
+ @runtime_stopping.setter
280
+ def runtime_stopping(self, value: bool) -> None:
281
+ self.state.runtime_stopping = value
282
+
283
+ @property
284
+ def shutdown_event(self) -> threading.Event:
285
+ return self.state.shutdown_event
286
+
287
+ @shutdown_event.setter
288
+ def shutdown_event(self, value: threading.Event) -> None:
289
+ self.state.shutdown_event = value
290
+
291
+ @property
292
+ def listener(self) -> object | None:
293
+ return self.state.listener
294
+
295
+ @listener.setter
296
+ def listener(self, value: object | None) -> None:
297
+ self.state.listener = value
298
+
299
+
300
+ __all__ = [
301
+ "DaemonHostFacade",
302
+ "DaemonHostDependencyFactories",
303
+ "DaemonHostDependencies",
304
+ "DaemonHostIdentity",
305
+ "DaemonHostState",
306
+ "default_daemon_host_dependency_factories",
307
+ ]
308
+
309
+ # Backward-compatible alias while the daemon host moves to a single grouped state object.
310
+ DaemonHostInitialState = DaemonHostState
@@ -0,0 +1,15 @@
1
+ """Shared daemon host constants."""
2
+
3
+ APP_VERSION = "0.1.0"
4
+ CHECKPOINT_INTERVAL_SECONDS = 30.0
5
+ CONTROL_REQUEST_POLL_INTERVAL_SECONDS = 1.0
6
+ STALE_AFTER_SECONDS = 90.0
7
+ DAEMON_STARTUP_LOCK_STALE_SECONDS = 15.0
8
+
9
+ __all__ = [
10
+ "APP_VERSION",
11
+ "CHECKPOINT_INTERVAL_SECONDS",
12
+ "CONTROL_REQUEST_POLL_INTERVAL_SECONDS",
13
+ "DAEMON_STARTUP_LOCK_STALE_SECONDS",
14
+ "STALE_AFTER_SECONDS",
15
+ ]
@@ -0,0 +1,97 @@
1
+ """Module entrypoints for launching one workspace daemon process."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Callable
9
+
10
+ from data_engine.domain import DaemonLifecyclePolicy
11
+ from data_engine.hosts.daemon.server import serve_workspace_daemon as serve_daemon_process
12
+ from data_engine.platform.workspace_models import (
13
+ DATA_ENGINE_APP_ROOT_ENV_VAR,
14
+ DATA_ENGINE_WORKSPACE_ID_ENV_VAR,
15
+ DATA_ENGINE_WORKSPACE_ROOT_ENV_VAR,
16
+ )
17
+ from data_engine.services import WorkspaceService
18
+
19
+
20
+ def default_workspace_service_factory() -> WorkspaceService:
21
+ """Build the default workspace-service collaborator for daemon entrypoints."""
22
+ return WorkspaceService()
23
+
24
+
25
+ def serve_workspace_daemon(
26
+ service_type,
27
+ *,
28
+ workspace_root: Path | None = None,
29
+ workspace_id: str | None = None,
30
+ lifecycle_policy: DaemonLifecyclePolicy = DaemonLifecyclePolicy.PERSISTENT,
31
+ workspace_service: WorkspaceService | None = None,
32
+ resolve_paths_func=None,
33
+ ) -> int:
34
+ """Start serving one workspace daemon in the current process."""
35
+ return serve_daemon_process(
36
+ service_type,
37
+ workspace_root=workspace_root,
38
+ workspace_id=workspace_id,
39
+ lifecycle_policy=lifecycle_policy,
40
+ workspace_service=workspace_service,
41
+ resolve_paths_func=resolve_paths_func,
42
+ )
43
+
44
+
45
+ def build_parser() -> argparse.ArgumentParser:
46
+ """Build the daemon module parser."""
47
+ parser = argparse.ArgumentParser(description="Run one Data Engine workspace daemon.")
48
+ parser.add_argument("--workspace", type=Path, required=True, help="Authored workspace root to host.")
49
+ parser.add_argument("--app-root", type=Path, default=None, help="Data Engine app root used for local artifacts.")
50
+ parser.add_argument("--workspace-id", default=None, help="Explicit workspace id override.")
51
+ parser.add_argument(
52
+ "--lifecycle-policy",
53
+ choices=tuple(policy.value for policy in DaemonLifecyclePolicy),
54
+ default=DaemonLifecyclePolicy.PERSISTENT.value,
55
+ help="Daemon lifetime policy.",
56
+ )
57
+ return parser
58
+
59
+
60
+ def main(
61
+ service_type,
62
+ argv: list[str] | None = None,
63
+ *,
64
+ workspace_service: WorkspaceService | None = None,
65
+ workspace_service_factory: Callable[[], WorkspaceService] | None = None,
66
+ resolve_paths_func=None,
67
+ serve_workspace_daemon_func=None,
68
+ ) -> int:
69
+ """Run the daemon module entrypoint for one concrete daemon service type."""
70
+ parser = build_parser()
71
+ args = parser.parse_args(argv)
72
+ if args.app_root is not None:
73
+ os.environ[DATA_ENGINE_APP_ROOT_ENV_VAR] = str(args.app_root.expanduser().resolve())
74
+ os.environ[DATA_ENGINE_WORKSPACE_ROOT_ENV_VAR] = str(args.workspace.expanduser().resolve())
75
+ if args.workspace_id:
76
+ os.environ[DATA_ENGINE_WORKSPACE_ID_ENV_VAR] = args.workspace_id
77
+ if resolve_paths_func is None:
78
+ workspace_service = workspace_service or (workspace_service_factory or default_workspace_service_factory)()
79
+ resolve_paths_func = workspace_service.resolve_paths
80
+ paths = resolve_paths_func(workspace_root=args.workspace, workspace_id=args.workspace_id)
81
+ serve_workspace_daemon_func = serve_workspace_daemon_func or serve_workspace_daemon
82
+ return serve_workspace_daemon_func(
83
+ service_type,
84
+ workspace_root=paths.workspace_root,
85
+ workspace_id=paths.workspace_id,
86
+ lifecycle_policy=args.lifecycle_policy,
87
+ workspace_service=workspace_service,
88
+ resolve_paths_func=resolve_paths_func,
89
+ )
90
+
91
+
92
+ __all__ = [
93
+ "build_parser",
94
+ "default_workspace_service_factory",
95
+ "main",
96
+ "serve_workspace_daemon",
97
+ ]
@@ -0,0 +1,191 @@
1
+ """Lifecycle and checkpoint policy for the daemon host."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import time
7
+ import traceback
8
+ from typing import TYPE_CHECKING
9
+
10
+ from data_engine.domain import DaemonLifecyclePolicy
11
+ from data_engine.hosts.daemon.ownership import (
12
+ honor_control_request_if_needed,
13
+ release_workspace_claim,
14
+ try_claim_requested_control,
15
+ )
16
+ from data_engine.hosts.daemon.constants import (
17
+ CHECKPOINT_INTERVAL_SECONDS,
18
+ CONTROL_REQUEST_POLL_INTERVAL_SECONDS,
19
+ )
20
+ from data_engine.hosts.daemon.runtime_control import stop_active_work
21
+
22
+ if TYPE_CHECKING:
23
+ from data_engine.hosts.daemon.app import DataEngineDaemonService
24
+
25
+
26
+ def checkpoint_loop(service: "DataEngineDaemonService") -> None:
27
+ next_checkpoint_at = time.monotonic() + CHECKPOINT_INTERVAL_SECONDS
28
+ while not service.host.shutdown_event.wait(CONTROL_REQUEST_POLL_INTERVAL_SECONDS):
29
+ if _should_shutdown_for_missing_clients(service):
30
+ service._debug_log("no live local clients remain; shutting down ephemeral daemon")
31
+ relinquish_workspace_for_missing_clients(service)
32
+ break
33
+ if not service._workspace_root_is_available():
34
+ service._debug_log("workspace root no longer available; shutting down daemon")
35
+ relinquish_workspace_for_missing_root(service)
36
+ break
37
+ with service._state_lock:
38
+ workspace_owned = service.host.workspace_owned
39
+ if not workspace_owned:
40
+ try:
41
+ if try_claim_requested_control(service):
42
+ next_checkpoint_at = time.monotonic() + CHECKPOINT_INTERVAL_SECONDS
43
+ continue
44
+ service._refresh_observer_snapshot()
45
+ except Exception:
46
+ pass
47
+ continue
48
+ try:
49
+ if honor_control_request_if_needed(service):
50
+ next_checkpoint_at = time.monotonic() + CHECKPOINT_INTERVAL_SECONDS
51
+ continue
52
+ if time.monotonic() < next_checkpoint_at:
53
+ continue
54
+ with service._state_lock:
55
+ status = "degraded" if service.state.consecutive_checkpoint_failures >= 1 else service.host.status
56
+ service._checkpoint_once(status=status)
57
+ with service._state_lock:
58
+ service.state.consecutive_checkpoint_failures = 0
59
+ next_checkpoint_at = time.monotonic() + CHECKPOINT_INTERVAL_SECONDS
60
+ except Exception:
61
+ service._debug_log("checkpoint failed")
62
+ service._debug_log(traceback.format_exc().rstrip())
63
+ with service._state_lock:
64
+ failure_count = service.state.increment_checkpoint_failures()
65
+ if failure_count == 2:
66
+ with service._state_lock:
67
+ service.state.status = "degraded"
68
+ service._update_daemon_state(status="degraded")
69
+ service._debug_log("daemon marked degraded after repeated checkpoint failures")
70
+ if failure_count >= 3:
71
+ service._debug_log("relinquishing workspace after repeated checkpoint failures")
72
+ relinquish_workspace_after_checkpoint_failures(service)
73
+ next_checkpoint_at = time.monotonic() + CHECKPOINT_INTERVAL_SECONDS
74
+
75
+
76
+ def relinquish_workspace_after_checkpoint_failures(service: "DataEngineDaemonService") -> None:
77
+ """Stop active work, release shared ownership, and stop the daemon."""
78
+ with service._state_lock:
79
+ service.state.stop_runtime(status="failed")
80
+ service._debug_log("relinquish workspace starting")
81
+ stop_active_work(service)
82
+ release_workspace_claim(service, status="failed", update_state=True)
83
+ service._debug_log("relinquish workspace complete")
84
+ shutdown_if_unowned_and_idle(service, reason="checkpoint failures")
85
+
86
+
87
+ def relinquish_workspace_for_control_request(service: "DataEngineDaemonService", requester_machine_id: str) -> None:
88
+ """Stop active work, hand ownership off, and stop this daemon."""
89
+ with service._state_lock:
90
+ service.state.stop_runtime(status="stopping flow")
91
+ service._debug_log(f"relinquish for control request requester={requester_machine_id}")
92
+ stop_active_work(service)
93
+ release_workspace_claim(
94
+ service,
95
+ leased_by_machine_id=requester_machine_id,
96
+ status="leased",
97
+ update_state=True,
98
+ )
99
+ service._debug_log("relinquish for control request complete")
100
+ shutdown_if_unowned_and_idle(service, reason="control request handoff")
101
+
102
+
103
+ def relinquish_workspace_for_missing_root(service: "DataEngineDaemonService") -> None:
104
+ """Stop active work and exit when the authored workspace root disappears."""
105
+ with service._state_lock:
106
+ service.state.stop_runtime(status="workspace missing")
107
+ stop_active_work(service)
108
+ release_workspace_claim(service, status="workspace missing")
109
+ service.host.shutdown_event.set()
110
+ service._wake_listener()
111
+
112
+
113
+ def relinquish_workspace_for_missing_clients(service: "DataEngineDaemonService") -> None:
114
+ """Stop active work and exit when an ephemeral daemon has no live local clients."""
115
+ with service._state_lock:
116
+ service.state.stop_runtime(status="client disconnected")
117
+ stop_active_work(service)
118
+ release_workspace_claim(service, status="client disconnected")
119
+ service.host.shutdown_event.set()
120
+ service._wake_listener()
121
+
122
+
123
+ def _should_shutdown_for_missing_clients(service: "DataEngineDaemonService") -> bool:
124
+ """Return whether an ephemeral daemon should stop because no live clients remain."""
125
+ if service.lifecycle_policy is not DaemonLifecyclePolicy.EPHEMERAL:
126
+ return False
127
+ with service._state_lock:
128
+ if service.host.runtime_active or service.host.runtime_stopping:
129
+ return False
130
+ if service.state.manual_run_threads:
131
+ return False
132
+ try:
133
+ return service.runtime_ledger.count_live_client_sessions(service.paths.workspace_id) == 0
134
+ except Exception:
135
+ return False
136
+
137
+
138
+ def shutdown_if_unowned_and_idle(service: "DataEngineDaemonService", *, reason: str) -> None:
139
+ """Exit when this daemon no longer owns the workspace and has no active work."""
140
+ with service._state_lock:
141
+ if service.host.workspace_owned:
142
+ return
143
+ if service.host.runtime_active or service.host.runtime_stopping:
144
+ return
145
+ if service.state.manual_run_threads:
146
+ return
147
+ service._debug_log(f"shutdown requested reason={reason}")
148
+ service.host.shutdown_event.set()
149
+ service._wake_listener()
150
+
151
+
152
+ def shutdown(service: "DataEngineDaemonService") -> None:
153
+ service._debug_log("shutdown starting")
154
+ stop_active_work(service)
155
+ if service.state.checkpoint_thread is not None and service.state.checkpoint_thread.is_alive():
156
+ service.state.checkpoint_thread.join(timeout=5.0)
157
+ with service._state_lock:
158
+ workspace_owned = service.host.workspace_owned
159
+ status = service.host.status
160
+ if workspace_owned and status not in {"failed", "workspace missing"}:
161
+ try:
162
+ service._checkpoint_once(status="stopping")
163
+ except Exception:
164
+ pass
165
+ release_workspace_claim(service)
166
+ try:
167
+ service.runtime_ledger.clear_daemon_state(service.paths.workspace_id)
168
+ except Exception:
169
+ pass
170
+ service.runtime_ledger.close()
171
+ if service.host.listener is not None:
172
+ try:
173
+ service.host.listener.close()
174
+ except Exception:
175
+ pass
176
+ if service.paths.daemon_endpoint_kind == "unix":
177
+ try:
178
+ Path(service.paths.daemon_endpoint_path).unlink()
179
+ except FileNotFoundError:
180
+ pass
181
+ service._debug_log("shutdown complete")
182
+
183
+
184
+ __all__ = [
185
+ "checkpoint_loop",
186
+ "relinquish_workspace_after_checkpoint_failures",
187
+ "relinquish_workspace_for_control_request",
188
+ "relinquish_workspace_for_missing_root",
189
+ "shutdown",
190
+ "shutdown_if_unowned_and_idle",
191
+ ]