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.
- data_engine/__init__.py +37 -0
- data_engine/application/__init__.py +39 -0
- data_engine/application/actions.py +42 -0
- data_engine/application/catalog.py +151 -0
- data_engine/application/control.py +213 -0
- data_engine/application/details.py +73 -0
- data_engine/application/runtime.py +449 -0
- data_engine/application/workspace.py +62 -0
- data_engine/authoring/__init__.py +14 -0
- data_engine/authoring/builder.py +31 -0
- data_engine/authoring/execution/__init__.py +6 -0
- data_engine/authoring/execution/app.py +6 -0
- data_engine/authoring/execution/context.py +82 -0
- data_engine/authoring/execution/continuous.py +176 -0
- data_engine/authoring/execution/grouped.py +106 -0
- data_engine/authoring/execution/logging.py +83 -0
- data_engine/authoring/execution/polling.py +135 -0
- data_engine/authoring/execution/runner.py +210 -0
- data_engine/authoring/execution/single.py +171 -0
- data_engine/authoring/flow.py +361 -0
- data_engine/authoring/helpers.py +160 -0
- data_engine/authoring/model.py +59 -0
- data_engine/authoring/primitives.py +430 -0
- data_engine/authoring/services.py +42 -0
- data_engine/devtools/__init__.py +3 -0
- data_engine/devtools/project_ast_map.py +503 -0
- data_engine/docs/__init__.py +1 -0
- data_engine/docs/sphinx_source/_static/custom.css +13 -0
- data_engine/docs/sphinx_source/api.rst +42 -0
- data_engine/docs/sphinx_source/conf.py +37 -0
- data_engine/docs/sphinx_source/guides/app-runtime-and-workspaces.md +397 -0
- data_engine/docs/sphinx_source/guides/authoring-flow-modules.md +215 -0
- data_engine/docs/sphinx_source/guides/configuring-flows.md +185 -0
- data_engine/docs/sphinx_source/guides/core-concepts.md +208 -0
- data_engine/docs/sphinx_source/guides/database-methods.md +107 -0
- data_engine/docs/sphinx_source/guides/duckdb-helpers.md +462 -0
- data_engine/docs/sphinx_source/guides/flow-context.md +538 -0
- data_engine/docs/sphinx_source/guides/flow-methods.md +206 -0
- data_engine/docs/sphinx_source/guides/getting-started.md +271 -0
- data_engine/docs/sphinx_source/guides/project-inventory.md +5683 -0
- data_engine/docs/sphinx_source/guides/project-map.md +118 -0
- data_engine/docs/sphinx_source/guides/recipes.md +268 -0
- data_engine/docs/sphinx_source/index.rst +22 -0
- data_engine/domain/__init__.py +92 -0
- data_engine/domain/actions.py +69 -0
- data_engine/domain/catalog.py +128 -0
- data_engine/domain/details.py +214 -0
- data_engine/domain/diagnostics.py +56 -0
- data_engine/domain/errors.py +104 -0
- data_engine/domain/inspection.py +99 -0
- data_engine/domain/logs.py +118 -0
- data_engine/domain/operations.py +172 -0
- data_engine/domain/operator.py +72 -0
- data_engine/domain/runs.py +155 -0
- data_engine/domain/runtime.py +279 -0
- data_engine/domain/source_state.py +17 -0
- data_engine/domain/support.py +54 -0
- data_engine/domain/time.py +23 -0
- data_engine/domain/workspace.py +159 -0
- data_engine/flow_modules/__init__.py +1 -0
- data_engine/flow_modules/flow_module_compiler.py +179 -0
- data_engine/flow_modules/flow_module_loader.py +201 -0
- data_engine/helpers/__init__.py +25 -0
- data_engine/helpers/duckdb.py +705 -0
- data_engine/hosts/__init__.py +1 -0
- data_engine/hosts/daemon/__init__.py +23 -0
- data_engine/hosts/daemon/app.py +221 -0
- data_engine/hosts/daemon/bootstrap.py +69 -0
- data_engine/hosts/daemon/client.py +465 -0
- data_engine/hosts/daemon/commands.py +64 -0
- data_engine/hosts/daemon/composition.py +310 -0
- data_engine/hosts/daemon/constants.py +15 -0
- data_engine/hosts/daemon/entrypoints.py +97 -0
- data_engine/hosts/daemon/lifecycle.py +191 -0
- data_engine/hosts/daemon/manager.py +272 -0
- data_engine/hosts/daemon/ownership.py +126 -0
- data_engine/hosts/daemon/runtime_commands.py +188 -0
- data_engine/hosts/daemon/runtime_control.py +31 -0
- data_engine/hosts/daemon/server.py +84 -0
- data_engine/hosts/daemon/shared_state.py +147 -0
- data_engine/hosts/daemon/state_sync.py +101 -0
- data_engine/platform/__init__.py +1 -0
- data_engine/platform/identity.py +35 -0
- data_engine/platform/local_settings.py +146 -0
- data_engine/platform/theme.py +259 -0
- data_engine/platform/workspace_models.py +190 -0
- data_engine/platform/workspace_policy.py +333 -0
- data_engine/runtime/__init__.py +1 -0
- data_engine/runtime/file_watch.py +185 -0
- data_engine/runtime/ledger_models.py +116 -0
- data_engine/runtime/runtime_db.py +938 -0
- data_engine/runtime/shared_state.py +523 -0
- data_engine/services/__init__.py +49 -0
- data_engine/services/daemon.py +64 -0
- data_engine/services/daemon_state.py +40 -0
- data_engine/services/flow_catalog.py +102 -0
- data_engine/services/flow_execution.py +48 -0
- data_engine/services/ledger.py +85 -0
- data_engine/services/logs.py +65 -0
- data_engine/services/runtime_binding.py +105 -0
- data_engine/services/runtime_execution.py +126 -0
- data_engine/services/runtime_history.py +62 -0
- data_engine/services/settings.py +58 -0
- data_engine/services/shared_state.py +28 -0
- data_engine/services/theme.py +59 -0
- data_engine/services/workspace_provisioning.py +224 -0
- data_engine/services/workspaces.py +74 -0
- data_engine/ui/__init__.py +3 -0
- data_engine/ui/cli/__init__.py +19 -0
- data_engine/ui/cli/app.py +161 -0
- data_engine/ui/cli/commands_doctor.py +178 -0
- data_engine/ui/cli/commands_run.py +80 -0
- data_engine/ui/cli/commands_start.py +100 -0
- data_engine/ui/cli/commands_workspace.py +97 -0
- data_engine/ui/cli/dependencies.py +44 -0
- data_engine/ui/cli/parser.py +56 -0
- data_engine/ui/gui/__init__.py +25 -0
- data_engine/ui/gui/app.py +116 -0
- data_engine/ui/gui/bootstrap.py +487 -0
- data_engine/ui/gui/bootstrapper.py +140 -0
- data_engine/ui/gui/cache_models.py +23 -0
- data_engine/ui/gui/control_support.py +185 -0
- data_engine/ui/gui/controllers/__init__.py +6 -0
- data_engine/ui/gui/controllers/flows.py +439 -0
- data_engine/ui/gui/controllers/runtime.py +245 -0
- data_engine/ui/gui/dialogs/__init__.py +12 -0
- data_engine/ui/gui/dialogs/messages.py +88 -0
- data_engine/ui/gui/dialogs/previews.py +222 -0
- data_engine/ui/gui/helpers/__init__.py +62 -0
- data_engine/ui/gui/helpers/inspection.py +81 -0
- data_engine/ui/gui/helpers/lifecycle.py +112 -0
- data_engine/ui/gui/helpers/scroll.py +28 -0
- data_engine/ui/gui/helpers/theming.py +87 -0
- data_engine/ui/gui/icons/dark_light.svg +12 -0
- data_engine/ui/gui/icons/documentation.svg +1 -0
- data_engine/ui/gui/icons/failed.svg +3 -0
- data_engine/ui/gui/icons/group.svg +4 -0
- data_engine/ui/gui/icons/home.svg +2 -0
- data_engine/ui/gui/icons/manual.svg +2 -0
- data_engine/ui/gui/icons/poll.svg +2 -0
- data_engine/ui/gui/icons/schedule.svg +4 -0
- data_engine/ui/gui/icons/settings.svg +2 -0
- data_engine/ui/gui/icons/started.svg +3 -0
- data_engine/ui/gui/icons/success.svg +3 -0
- data_engine/ui/gui/icons/view-log.svg +3 -0
- data_engine/ui/gui/icons.py +50 -0
- data_engine/ui/gui/launcher.py +48 -0
- data_engine/ui/gui/presenters/__init__.py +72 -0
- data_engine/ui/gui/presenters/docs.py +140 -0
- data_engine/ui/gui/presenters/logs.py +58 -0
- data_engine/ui/gui/presenters/runtime_projection.py +29 -0
- data_engine/ui/gui/presenters/sidebar.py +88 -0
- data_engine/ui/gui/presenters/steps.py +148 -0
- data_engine/ui/gui/presenters/workspace.py +39 -0
- data_engine/ui/gui/presenters/workspace_binding.py +75 -0
- data_engine/ui/gui/presenters/workspace_settings.py +182 -0
- data_engine/ui/gui/preview_models.py +37 -0
- data_engine/ui/gui/render_support.py +241 -0
- data_engine/ui/gui/rendering/__init__.py +12 -0
- data_engine/ui/gui/rendering/artifacts.py +95 -0
- data_engine/ui/gui/rendering/icons.py +50 -0
- data_engine/ui/gui/runtime.py +47 -0
- data_engine/ui/gui/state_support.py +193 -0
- data_engine/ui/gui/support.py +214 -0
- data_engine/ui/gui/surface.py +209 -0
- data_engine/ui/gui/theme.py +720 -0
- data_engine/ui/gui/widgets/__init__.py +34 -0
- data_engine/ui/gui/widgets/config.py +41 -0
- data_engine/ui/gui/widgets/logs.py +62 -0
- data_engine/ui/gui/widgets/panels.py +507 -0
- data_engine/ui/gui/widgets/sidebar.py +130 -0
- data_engine/ui/gui/widgets/steps.py +84 -0
- data_engine/ui/tui/__init__.py +5 -0
- data_engine/ui/tui/app.py +222 -0
- data_engine/ui/tui/bootstrap.py +475 -0
- data_engine/ui/tui/bootstrapper.py +117 -0
- data_engine/ui/tui/controllers/__init__.py +6 -0
- data_engine/ui/tui/controllers/flows.py +349 -0
- data_engine/ui/tui/controllers/runtime.py +167 -0
- data_engine/ui/tui/runtime.py +34 -0
- data_engine/ui/tui/state_support.py +141 -0
- data_engine/ui/tui/support.py +63 -0
- data_engine/ui/tui/theme.py +204 -0
- data_engine/ui/tui/widgets.py +123 -0
- data_engine/views/__init__.py +109 -0
- data_engine/views/actions.py +80 -0
- data_engine/views/artifacts.py +58 -0
- data_engine/views/flow_display.py +69 -0
- data_engine/views/logs.py +54 -0
- data_engine/views/models.py +96 -0
- data_engine/views/presentation.py +133 -0
- data_engine/views/runs.py +62 -0
- data_engine/views/state.py +39 -0
- data_engine/views/status.py +13 -0
- data_engine/views/text.py +109 -0
- py_data_engine-0.1.0.dist-info/METADATA +330 -0
- py_data_engine-0.1.0.dist-info/RECORD +200 -0
- py_data_engine-0.1.0.dist-info/WHEEL +5 -0
- py_data_engine-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|