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,84 @@
1
+ """Listener loop and host serving helpers for the daemon process."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from multiprocessing import AuthenticationError
6
+ from multiprocessing.connection import Listener
7
+ from pathlib import Path
8
+ import threading
9
+ import traceback
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from data_engine.domain import DaemonLifecyclePolicy
13
+ from data_engine.hosts.daemon.client import (
14
+ _decode_message,
15
+ _encode_message,
16
+ _remove_stale_unix_endpoint,
17
+ daemon_authkey,
18
+ endpoint_address,
19
+ endpoint_family,
20
+ )
21
+ from data_engine.services import WorkspaceService
22
+
23
+ if TYPE_CHECKING:
24
+ from data_engine.hosts.daemon.app import DataEngineDaemonService
25
+
26
+
27
+ def serve_forever(service: "DataEngineDaemonService") -> None:
28
+ """Run the workspace daemon listener loop until shutdown."""
29
+ try:
30
+ service.initialize()
31
+ service.state.checkpoint_thread = threading.Thread(target=service._checkpoint_loop, daemon=True)
32
+ service.state.checkpoint_thread.start()
33
+ _remove_stale_unix_endpoint(service.paths)
34
+ listener = Listener(
35
+ endpoint_address(service.paths),
36
+ family=endpoint_family(service.paths),
37
+ authkey=daemon_authkey(service.paths),
38
+ )
39
+ service.host.listener = listener
40
+ service._debug_log(f"listener ready endpoint={service.paths.daemon_endpoint_path}")
41
+ while not service.host.shutdown_event.is_set():
42
+ try:
43
+ connection = listener.accept()
44
+ except (AuthenticationError, OSError, EOFError):
45
+ if service.host.shutdown_event.is_set():
46
+ break
47
+ service._debug_log("listener accept failed but daemon remains alive")
48
+ continue
49
+ with connection:
50
+ try:
51
+ payload = _decode_message(connection.recv_bytes())
52
+ response = service._handle_command(payload)
53
+ except Exception as exc: # pragma: no cover - defensive daemon boundary
54
+ service._debug_log(f"command handling error: {exc!r}")
55
+ response = {"ok": False, "error": str(exc)}
56
+ connection.send_bytes(_encode_message(response))
57
+ except Exception as exc:
58
+ service._debug_log(f"serve_forever fatal error: {exc!r}")
59
+ service._debug_log(traceback.format_exc().rstrip())
60
+ raise
61
+ finally:
62
+ service._shutdown()
63
+
64
+
65
+ def serve_workspace_daemon(
66
+ service_type: type["DataEngineDaemonService"],
67
+ *,
68
+ workspace_root: Path | None = None,
69
+ workspace_id: str | None = None,
70
+ lifecycle_policy: DaemonLifecyclePolicy = DaemonLifecyclePolicy.PERSISTENT,
71
+ workspace_service: WorkspaceService | None = None,
72
+ resolve_paths_func=None,
73
+ ) -> int:
74
+ """Start serving one workspace daemon in the current process."""
75
+ if resolve_paths_func is None:
76
+ workspace_service = workspace_service or WorkspaceService()
77
+ resolve_paths_func = workspace_service.resolve_paths
78
+ paths = resolve_paths_func(workspace_root=workspace_root, workspace_id=workspace_id)
79
+ service = service_type(paths, lifecycle_policy=lifecycle_policy)
80
+ service.serve_forever()
81
+ return 0
82
+
83
+
84
+ __all__ = ["serve_forever", "serve_workspace_daemon"]
@@ -0,0 +1,147 @@
1
+ """Host-owned adapter over shared workspace lease and snapshot operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from data_engine.platform.workspace_models import WorkspacePaths
8
+ from data_engine.runtime.runtime_db import RuntimeLedger
9
+ from data_engine.runtime.shared_state import (
10
+ checkpoint_workspace_state as checkpoint_runtime_workspace_state,
11
+ claim_workspace as claim_runtime_workspace,
12
+ hydrate_local_runtime_state,
13
+ initialize_workspace_state,
14
+ lease_is_stale,
15
+ read_control_request,
16
+ read_lease_metadata,
17
+ recover_stale_workspace,
18
+ release_workspace,
19
+ remove_control_request,
20
+ remove_lease_metadata,
21
+ write_control_request,
22
+ write_lease_metadata,
23
+ )
24
+
25
+
26
+ class DaemonSharedStateAdapter:
27
+ """Own host-facing access to shared lease, control-request, and snapshot state."""
28
+
29
+ def initialize_workspace(self, paths: WorkspacePaths) -> None:
30
+ initialize_workspace_state(paths)
31
+
32
+ def claim_workspace(self, paths: WorkspacePaths) -> bool:
33
+ return claim_runtime_workspace(paths)
34
+
35
+ def release_workspace(self, paths: WorkspacePaths) -> None:
36
+ release_workspace(paths)
37
+
38
+ def recover_stale_workspace(
39
+ self,
40
+ paths: WorkspacePaths,
41
+ *,
42
+ machine_id: str,
43
+ stale_after_seconds: float,
44
+ reclaim: bool = True,
45
+ ) -> bool:
46
+ return recover_stale_workspace(
47
+ paths,
48
+ machine_id=machine_id,
49
+ stale_after_seconds=stale_after_seconds,
50
+ reclaim=reclaim,
51
+ )
52
+
53
+ def lease_is_stale(self, paths: WorkspacePaths, *, stale_after_seconds: float) -> bool:
54
+ return lease_is_stale(paths, stale_after_seconds=stale_after_seconds)
55
+
56
+ def hydrate_local_runtime(self, paths: WorkspacePaths, ledger: RuntimeLedger) -> None:
57
+ hydrate_local_runtime_state(paths, ledger)
58
+
59
+ def checkpoint_workspace_state(
60
+ self,
61
+ paths: WorkspacePaths,
62
+ ledger: RuntimeLedger,
63
+ *,
64
+ workspace_id: str,
65
+ machine_id: str,
66
+ daemon_id: str,
67
+ pid: int,
68
+ status: str,
69
+ started_at_utc: str,
70
+ last_checkpoint_at_utc: str,
71
+ app_version: str | None,
72
+ ) -> None:
73
+ checkpoint_runtime_workspace_state(
74
+ paths,
75
+ ledger,
76
+ workspace_id=workspace_id,
77
+ machine_id=machine_id,
78
+ daemon_id=daemon_id,
79
+ pid=pid,
80
+ status=status,
81
+ started_at_utc=started_at_utc,
82
+ last_checkpoint_at_utc=last_checkpoint_at_utc,
83
+ app_version=app_version,
84
+ )
85
+
86
+ def read_lease_metadata(self, paths: WorkspacePaths) -> dict[str, Any] | None:
87
+ metadata = read_lease_metadata(paths)
88
+ return metadata if isinstance(metadata, dict) else None
89
+
90
+ def write_lease_metadata(
91
+ self,
92
+ paths: WorkspacePaths,
93
+ *,
94
+ workspace_id: str,
95
+ machine_id: str,
96
+ daemon_id: str,
97
+ pid: int,
98
+ status: str,
99
+ started_at_utc: str,
100
+ last_checkpoint_at_utc: str,
101
+ app_version: str | None,
102
+ ) -> None:
103
+ write_lease_metadata(
104
+ paths,
105
+ workspace_id=workspace_id,
106
+ machine_id=machine_id,
107
+ daemon_id=daemon_id,
108
+ pid=pid,
109
+ status=status,
110
+ started_at_utc=started_at_utc,
111
+ last_checkpoint_at_utc=last_checkpoint_at_utc,
112
+ app_version=app_version,
113
+ )
114
+
115
+ def remove_lease_metadata(self, paths: WorkspacePaths) -> None:
116
+ remove_lease_metadata(paths)
117
+
118
+ def read_control_request(self, paths: WorkspacePaths) -> dict[str, Any] | None:
119
+ metadata = read_control_request(paths)
120
+ return metadata if isinstance(metadata, dict) else None
121
+
122
+ def write_control_request(
123
+ self,
124
+ paths: WorkspacePaths,
125
+ *,
126
+ workspace_id: str,
127
+ requester_machine_id: str,
128
+ requester_host_name: str,
129
+ requester_pid: int,
130
+ requester_client_kind: str,
131
+ requested_at_utc: str,
132
+ ) -> None:
133
+ write_control_request(
134
+ paths,
135
+ workspace_id=workspace_id,
136
+ requester_machine_id=requester_machine_id,
137
+ requester_host_name=requester_host_name,
138
+ requester_pid=requester_pid,
139
+ requester_client_kind=requester_client_kind,
140
+ requested_at_utc=requested_at_utc,
141
+ )
142
+
143
+ def remove_control_request(self, paths: WorkspacePaths) -> None:
144
+ remove_control_request(paths)
145
+
146
+
147
+ __all__ = ["DaemonSharedStateAdapter"]
@@ -0,0 +1,101 @@
1
+ """Daemon state publication and observer-sync helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from data_engine.domain.time import utcnow_text
8
+ from data_engine.hosts.daemon.constants import APP_VERSION
9
+ from data_engine.views.models import QtFlowCard
10
+
11
+ if TYPE_CHECKING:
12
+ from data_engine.hosts.daemon.app import DataEngineDaemonService
13
+
14
+
15
+ class DaemonStateSyncHandler:
16
+ """Own daemon status payloads, checkpoint publication, and observer sync."""
17
+
18
+ def __init__(self, service: "DataEngineDaemonService") -> None:
19
+ self.service = service
20
+
21
+ def load_flow_cards(self, *, force: bool = False) -> tuple[QtFlowCard, ...]:
22
+ return self.service._load_flow_cards(force=force)
23
+
24
+ def status_payload(self) -> dict[str, Any]:
25
+ service = self.service
26
+ with service._state_lock:
27
+ state = service.state
28
+ status = state.status
29
+ workspace_owned = state.workspace_owned
30
+ leased_by_machine_id = state.leased_by_machine_id
31
+ runtime_active = state.runtime_active
32
+ runtime_stopping = state.runtime_stopping
33
+ manual_runs = sorted(state.manual_run_threads)
34
+ last_checkpoint_at_utc = state.last_checkpoint_at_utc
35
+ return {
36
+ "workspace_id": service.paths.workspace_id,
37
+ "workspace_root": str(service.paths.workspace_root),
38
+ "machine_id": service.machine_id,
39
+ "daemon_id": service.daemon_id,
40
+ "pid": service.pid,
41
+ "status": status,
42
+ "workspace_owned": workspace_owned,
43
+ "leased_by_machine_id": leased_by_machine_id,
44
+ "engine_active": runtime_active,
45
+ "engine_stopping": runtime_stopping,
46
+ "manual_runs": manual_runs,
47
+ "last_checkpoint_at_utc": last_checkpoint_at_utc,
48
+ }
49
+
50
+ def checkpoint_once(self, *, status: str) -> None:
51
+ service = self.service
52
+ checkpoint_time = utcnow_text()
53
+ service.shared_state_adapter.checkpoint_workspace_state(
54
+ service.paths,
55
+ service.runtime_ledger,
56
+ workspace_id=service.paths.workspace_id,
57
+ machine_id=service.machine_id,
58
+ daemon_id=service.daemon_id,
59
+ pid=service.pid,
60
+ status=status,
61
+ started_at_utc=service.started_at_utc,
62
+ last_checkpoint_at_utc=checkpoint_time,
63
+ app_version=APP_VERSION,
64
+ )
65
+ with service._state_lock:
66
+ service.state.set_checkpoint_time(checkpoint_time)
67
+ self.update_daemon_state(status=status)
68
+
69
+ def refresh_observer_snapshot(self) -> None:
70
+ service = self.service
71
+ service.shared_state_adapter.hydrate_local_runtime(service.paths, service.runtime_ledger)
72
+ metadata = service.shared_state_adapter.read_lease_metadata(service.paths)
73
+ with service._state_lock:
74
+ service.state.set_leased_by_machine_id(
75
+ str(metadata.get("machine_id"))
76
+ if metadata is not None and metadata.get("machine_id") is not None
77
+ else None
78
+ )
79
+ if metadata is None:
80
+ self.update_daemon_state(status="available")
81
+ service._shutdown_if_unowned_and_idle(reason="lease released")
82
+ return
83
+ self.update_daemon_state(status="leased")
84
+
85
+ def update_daemon_state(self, *, status: str) -> None:
86
+ service = self.service
87
+ service.runtime_ledger.upsert_daemon_state(
88
+ workspace_id=service.paths.workspace_id,
89
+ pid=service.pid,
90
+ endpoint_kind=service.paths.daemon_endpoint_kind,
91
+ endpoint_path=service.paths.daemon_endpoint_path,
92
+ started_at_utc=service.started_at_utc,
93
+ last_checkpoint_at_utc=service.state.last_checkpoint_at_utc,
94
+ status=status,
95
+ app_root=str(service.paths.app_root),
96
+ workspace_root=str(service.paths.workspace_root),
97
+ version_text=APP_VERSION,
98
+ )
99
+
100
+
101
+ __all__ = ["DaemonStateSyncHandler"]
@@ -0,0 +1 @@
1
+ """Application identity, paths, local settings, and theming."""
@@ -0,0 +1,35 @@
1
+ """Centralized application identity and naming helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ APP_INTERNAL_ID: Final[str] = "data_engine"
8
+ APP_DISTRIBUTION_NAME: Final[str] = "py-data-engine"
9
+ APP_DISPLAY_NAME: Final[str] = "Data Engine"
10
+ APP_ENV_PREFIX: Final[str] = "DATA_ENGINE"
11
+ APP_CACHE_DIR_NAME: Final[str] = "data_engine"
12
+ APP_RUNTIME_NAMESPACE: Final[str] = "data_engine"
13
+ APP_ARTIFACTS_DIR_NAME: Final[str] = "artifacts"
14
+ WORKSPACE_CACHE_DIR_NAME: Final[str] = "workspace_cache"
15
+ RUNTIME_STATE_DIR_NAME: Final[str] = "runtime_state"
16
+
17
+
18
+ def env_var(name: str) -> str:
19
+ """Return one application-scoped environment variable name."""
20
+ normalized = name.strip().upper()
21
+ return f"{APP_ENV_PREFIX}_{normalized}"
22
+
23
+
24
+ __all__ = [
25
+ "APP_CACHE_DIR_NAME",
26
+ "APP_DISPLAY_NAME",
27
+ "APP_DISTRIBUTION_NAME",
28
+ "APP_ENV_PREFIX",
29
+ "APP_INTERNAL_ID",
30
+ "APP_RUNTIME_NAMESPACE",
31
+ "APP_ARTIFACTS_DIR_NAME",
32
+ "RUNTIME_STATE_DIR_NAME",
33
+ "WORKSPACE_CACHE_DIR_NAME",
34
+ "env_var",
35
+ ]
@@ -0,0 +1,146 @@
1
+ """Machine-local app settings persisted in a local SQLite database."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ import sqlite3
8
+
9
+ from data_engine.platform.identity import APP_CACHE_DIR_NAME, env_var
10
+
11
+
12
+ DATA_ENGINE_APP_ROOT_ENV_VAR = env_var("app_root")
13
+ DATA_ENGINE_STATE_ROOT_ENV_VAR = env_var("state_root")
14
+
15
+
16
+ def default_state_root(*, app_root: Path | None = None) -> Path:
17
+ """Return the platform-local mutable state root for the app."""
18
+ env_value = os.environ.get(DATA_ENGINE_STATE_ROOT_ENV_VAR)
19
+ if env_value and env_value.strip():
20
+ return Path(env_value).expanduser().resolve()
21
+
22
+ home = Path.home()
23
+ if os.name == "nt":
24
+ base = Path(os.environ.get("LOCALAPPDATA") or home / "AppData" / "Local")
25
+ return base / APP_CACHE_DIR_NAME
26
+ if sys_platform() == "darwin":
27
+ return home / "Library" / "Application Support" / APP_CACHE_DIR_NAME
28
+ xdg_state = os.environ.get("XDG_STATE_HOME")
29
+ if xdg_state and xdg_state.strip():
30
+ return Path(xdg_state).expanduser().resolve() / APP_CACHE_DIR_NAME
31
+ xdg_data = os.environ.get("XDG_DATA_HOME")
32
+ if xdg_data and xdg_data.strip():
33
+ return Path(xdg_data).expanduser().resolve() / APP_CACHE_DIR_NAME
34
+ return home / ".local" / "share" / APP_CACHE_DIR_NAME
35
+
36
+
37
+ def default_settings_db_path(*, app_root: Path | None = None) -> Path:
38
+ """Return the default machine-local settings database path."""
39
+ return default_state_root(app_root=app_root) / "settings" / "app_settings.sqlite"
40
+
41
+
42
+ def sys_platform() -> str:
43
+ """Return the normalized platform identifier."""
44
+ import sys
45
+
46
+ return sys.platform
47
+
48
+
49
+ class LocalSettingsStore:
50
+ """Persist simple machine-local UI settings in SQLite."""
51
+
52
+ def __init__(self, db_path: Path) -> None:
53
+ self.db_path = Path(db_path).expanduser().resolve()
54
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
55
+ self._initialize()
56
+
57
+ @classmethod
58
+ def open_default(cls, *, app_root: Path | None = None) -> "LocalSettingsStore":
59
+ return cls(default_settings_db_path(app_root=app_root))
60
+
61
+ def _connection(self) -> sqlite3.Connection:
62
+ # Recreate the parent on every open so repeated temp-root churn during
63
+ # long test sessions cannot strand the settings store on a deleted path.
64
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
65
+ connection = sqlite3.connect(self.db_path)
66
+ connection.row_factory = sqlite3.Row
67
+ self._ensure_schema(connection)
68
+ return connection
69
+
70
+ @staticmethod
71
+ def _ensure_schema(connection: sqlite3.Connection) -> None:
72
+ connection.execute(
73
+ """
74
+ CREATE TABLE IF NOT EXISTS settings (
75
+ key TEXT PRIMARY KEY,
76
+ value TEXT
77
+ )
78
+ """
79
+ )
80
+
81
+ def _initialize(self) -> None:
82
+ with self._connection():
83
+ pass
84
+
85
+ def get(self, key: str) -> str | None:
86
+ with self._connection() as connection:
87
+ row = connection.execute("SELECT value FROM settings WHERE key = ?", (key,)).fetchone()
88
+ if row is None:
89
+ return None
90
+ value = row["value"]
91
+ return str(value) if value is not None else None
92
+
93
+ def set(self, key: str, value: str | None) -> None:
94
+ with self._connection() as connection:
95
+ if value is None or not str(value).strip():
96
+ connection.execute("DELETE FROM settings WHERE key = ?", (key,))
97
+ else:
98
+ connection.execute(
99
+ """
100
+ INSERT INTO settings (key, value)
101
+ VALUES (?, ?)
102
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
103
+ """,
104
+ (key, str(value)),
105
+ )
106
+
107
+ def workspace_collection_root(self) -> Path | None:
108
+ value = self.get("workspace_collection_root")
109
+ if value is None or not value.strip():
110
+ return None
111
+ return Path(value).expanduser().resolve()
112
+
113
+ def set_workspace_collection_root(self, value: Path | str | None) -> None:
114
+ if value is None:
115
+ self.set("workspace_collection_root", None)
116
+ return
117
+ self.set("workspace_collection_root", str(Path(value).expanduser().resolve()))
118
+
119
+ def default_workspace_id(self) -> str | None:
120
+ value = self.get("default_workspace_id")
121
+ if value is None or not value.strip():
122
+ return None
123
+ return value.strip()
124
+
125
+ def set_default_workspace_id(self, value: str | None) -> None:
126
+ self.set("default_workspace_id", value.strip() if value is not None else None)
127
+
128
+ def runtime_root(self) -> Path | None:
129
+ value = self.get("runtime_root")
130
+ if value is None or not value.strip():
131
+ return None
132
+ return Path(value).expanduser().resolve()
133
+
134
+ def set_runtime_root(self, value: Path | str | None) -> None:
135
+ if value is None:
136
+ self.set("runtime_root", None)
137
+ return
138
+ self.set("runtime_root", str(Path(value).expanduser().resolve()))
139
+
140
+
141
+ __all__ = [
142
+ "DATA_ENGINE_STATE_ROOT_ENV_VAR",
143
+ "LocalSettingsStore",
144
+ "default_settings_db_path",
145
+ "default_state_root",
146
+ ]