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,178 @@
1
+ """Environment and daemon diagnostics for the CLI surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import subprocess
7
+ import sys
8
+ from typing import Any, Callable
9
+
10
+ from data_engine.authoring.model import FlowValidationError
11
+ from data_engine.domain import ClassifiedProcessInfo, DoctorCheck, ProcessInfo, WorkspaceLeaseDiagnostic
12
+ from data_engine.platform.workspace_models import authored_workspace_is_available, machine_id_text
13
+
14
+
15
+ def doctor(*, settings: Any, paths: Any) -> int:
16
+ checks: list[DoctorCheck] = []
17
+
18
+ def add(status: str, message: str) -> None:
19
+ checks.append(DoctorCheck(status=status, message=message))
20
+
21
+ add("OK", f"app root: {settings.app_root}")
22
+ add("OK", f"python executable: {Path(sys.executable).expanduser()}")
23
+ add("OK" if settings.settings_path.is_file() else "WARN", f"workspace settings: {settings.settings_path}")
24
+ add("OK" if settings.state_root.exists() else "WARN", f"state root: {settings.state_root}")
25
+ add("OK" if settings.runtime_root.exists() else "WARN", f"runtime root: {settings.runtime_root}")
26
+ if settings.workspace_collection_root is None:
27
+ add("WARN", "workspace collection root: not configured")
28
+ else:
29
+ add("OK" if settings.workspace_collection_root.is_dir() else "WARN", f"workspace collection root: {settings.workspace_collection_root}")
30
+ if paths.workspace_configured:
31
+ add("OK" if paths.workspace_root.is_dir() else "WARN", f"workspace root: {paths.workspace_root}")
32
+ add("OK" if paths.flow_modules_dir.is_dir() else "WARN", f"flow modules dir: {paths.flow_modules_dir}")
33
+ add("OK" if (paths.workspace_root / ".vscode" / "settings.json").is_file() else "WARN", f"VS Code settings: {paths.workspace_root / '.vscode' / 'settings.json'}")
34
+ add("OK" if authored_workspace_is_available(paths) else "WARN", f"authored workspace ready: {paths.workspace_root}")
35
+ else:
36
+ add("WARN", "workspace root: not configured")
37
+ add("WARN", "flow modules dir: not configured")
38
+ add("WARN", "VS Code settings: not configured")
39
+ add("WARN", "authored workspace ready: workspace collection root not configured")
40
+ add("OK" if paths.artifacts_dir.exists() else "WARN", f"artifacts dir: {paths.artifacts_dir}")
41
+
42
+ failures = 0
43
+ for check in checks:
44
+ print(f"[{check.status}] {check.message}")
45
+ if check.status == "FAIL":
46
+ failures += 1
47
+ return 1 if failures else 0
48
+
49
+
50
+ def run_process_listing() -> list[ProcessInfo]:
51
+ result = subprocess.run(
52
+ ["ps", "-ax", "-o", "pid=", "-o", "ppid=", "-o", "stat=", "-o", "command="],
53
+ capture_output=True,
54
+ text=True,
55
+ check=False,
56
+ )
57
+ if result.returncode != 0:
58
+ raise FlowValidationError("Unable to inspect the local process table.")
59
+ rows: list[ProcessInfo] = []
60
+ for line in result.stdout.splitlines():
61
+ parts = line.strip().split(None, 3)
62
+ if len(parts) != 4:
63
+ continue
64
+ pid_text, ppid_text, status, command = parts
65
+ try:
66
+ pid = int(pid_text)
67
+ ppid = int(ppid_text)
68
+ except ValueError:
69
+ continue
70
+ rows.append(ProcessInfo(pid=pid, ppid=ppid, status=status, command=command))
71
+ return rows
72
+
73
+
74
+ def classify_process_kind(command: str) -> str | None:
75
+ if "data_engine.hosts.daemon.app" in command:
76
+ return "daemon"
77
+ if "data_engine.ui.gui.launcher" in command:
78
+ return "gui"
79
+ if "data_engine.ui.tui.app" in command:
80
+ return "tui"
81
+ return None
82
+
83
+
84
+ def doctor_daemons(
85
+ *,
86
+ settings: Any,
87
+ workspace_service: Any,
88
+ process_rows: list[ProcessInfo] | None = None,
89
+ process_listing_func: Callable[[], list[ProcessInfo]] = run_process_listing,
90
+ classify_process_kind_func: Callable[[str], str | None] = classify_process_kind,
91
+ read_lease_metadata_func: Callable[[Any], dict[str, Any] | None],
92
+ lease_is_stale_func: Callable[[Any, float], bool],
93
+ machine_id_text_func: Callable[[], str] = machine_id_text,
94
+ ) -> int:
95
+ rows = process_rows if process_rows is not None else process_listing_func()
96
+ relevant = [
97
+ ClassifiedProcessInfo(
98
+ pid=row.pid,
99
+ ppid=row.ppid,
100
+ status=row.status,
101
+ command=row.command,
102
+ kind=kind,
103
+ )
104
+ for row in rows
105
+ for kind in (classify_process_kind_func(str(row.command)),)
106
+ if kind is not None
107
+ ]
108
+ daemons = [row for row in relevant if row.kind == "daemon"]
109
+ surfaces = [row for row in relevant if row.kind in {"gui", "tui"}]
110
+ defunct = [row for row in daemons if row.is_defunct]
111
+ live_daemons = [row for row in daemons if row not in defunct]
112
+
113
+ print("Data Engine Daemon Diagnostics")
114
+ print("")
115
+ print(f"Live daemons: {len(live_daemons)}")
116
+ for row in live_daemons:
117
+ orphaned = " orphaned" if row.is_orphaned else ""
118
+ print(f" daemon pid={row.pid} ppid={row.ppid} status={row.status}{orphaned}")
119
+
120
+ print("")
121
+ print(f"Defunct daemons: {len(defunct)}")
122
+ for row in defunct:
123
+ print(f" defunct pid={row.pid} ppid={row.ppid} status={row.status}")
124
+
125
+ print("")
126
+ print(f"Related UI processes: {len(surfaces)}")
127
+ for row in surfaces:
128
+ print(f" {row.kind} pid={row.pid} ppid={row.ppid} status={row.status}")
129
+
130
+ print("")
131
+ print("Workspace leases:")
132
+ discovered = ()
133
+ if settings.workspace_collection_root is not None:
134
+ discovered = workspace_service.discover(
135
+ app_root=settings.app_root,
136
+ workspace_collection_root=settings.workspace_collection_root,
137
+ )
138
+ any_workspace = False
139
+ for item in discovered:
140
+ any_workspace = True
141
+ paths = workspace_service.resolve_paths(
142
+ workspace_id=item.workspace_id,
143
+ workspace_root=item.workspace_root,
144
+ workspace_collection_root=settings.workspace_collection_root,
145
+ )
146
+ metadata = read_lease_metadata_func(paths)
147
+ if metadata is None:
148
+ print(f" {item.workspace_id}: no lease metadata")
149
+ continue
150
+ pid_value = metadata.get("pid")
151
+ try:
152
+ pid = int(pid_value)
153
+ except (TypeError, ValueError):
154
+ pid = None
155
+ matching = next((row for row in rows if row.pid == pid), None) if pid is not None else None
156
+ owner = metadata.get("machine_id")
157
+ if matching is None:
158
+ status = "missing"
159
+ elif matching.status.startswith("Z"):
160
+ status = "defunct"
161
+ else:
162
+ status = "live"
163
+ lease_row = WorkspaceLeaseDiagnostic(
164
+ workspace_id=item.workspace_id,
165
+ lease_pid=pid,
166
+ state=status,
167
+ stale=lease_is_stale_func(paths, stale_after_seconds=30.0),
168
+ local_owner=owner == machine_id_text_func(),
169
+ )
170
+ stale_text = " stale" if lease_row.stale else ""
171
+ local_text = " local" if lease_row.local_owner else ""
172
+ print(
173
+ f" {lease_row.workspace_id}: lease_pid={lease_row.lease_pid if lease_row.lease_pid is not None else '-'} "
174
+ f"state={lease_row.state}{stale_text}{local_text}"
175
+ )
176
+ if not any_workspace:
177
+ print(" no discovered workspaces")
178
+ return 0
@@ -0,0 +1,80 @@
1
+ """Curated test-running commands for the CLI surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import subprocess
7
+ import sys
8
+
9
+ from data_engine.authoring.model import FlowValidationError
10
+
11
+ TEST_SLICE_CHOICES = ("all", "unit", "ui", "qt", "tui", "integration", "live")
12
+
13
+
14
+ def checkout_tests_dir(app_root: Path) -> Path:
15
+ """Return the repo-local tests directory for one checkout-style app root."""
16
+ tests_dir = app_root / "tests"
17
+ if not tests_dir.is_dir():
18
+ raise FlowValidationError(
19
+ f"Run tests is only available from a checkout-style app root with a tests directory: {app_root}"
20
+ )
21
+ return tests_dir
22
+
23
+
24
+ def raise_open_file_limit(*, minimum_soft_limit: int = 4096) -> None:
25
+ """Best-effort raise of the soft open-file limit before long pytest runs."""
26
+ try:
27
+ import resource
28
+ except ImportError: # pragma: no cover - non-Unix fallback
29
+ return
30
+ try:
31
+ soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
32
+ except (OSError, ValueError):
33
+ return
34
+ target_limit = hard_limit if hard_limit >= 0 else minimum_soft_limit
35
+ target_limit = max(soft_limit, min(target_limit, minimum_soft_limit) if hard_limit >= 0 else minimum_soft_limit)
36
+ if hard_limit >= 0:
37
+ target_limit = min(hard_limit, max(soft_limit, minimum_soft_limit))
38
+ if target_limit <= soft_limit:
39
+ return
40
+ try:
41
+ resource.setrlimit(resource.RLIMIT_NOFILE, (target_limit, hard_limit))
42
+ except (OSError, ValueError):
43
+ return
44
+
45
+
46
+ def test_slice_args(slice_name: str, *, app_root: Path) -> tuple[str, ...]:
47
+ tests_dir = checkout_tests_dir(app_root)
48
+ match slice_name:
49
+ case "all":
50
+ return (str(tests_dir),)
51
+ case "unit":
52
+ return (
53
+ str(tests_dir),
54
+ f"--ignore={tests_dir / 'test_qt_ui.py'}",
55
+ f"--ignore={tests_dir / 'test_tui.py'}",
56
+ f"--ignore={tests_dir / 'test_integration.py'}",
57
+ f"--ignore={tests_dir / 'test_live_runtime_suite.py'}",
58
+ )
59
+ case "ui":
60
+ return (str(tests_dir / "test_qt_ui.py"), str(tests_dir / "test_tui.py"))
61
+ case "qt":
62
+ return (str(tests_dir / "test_qt_ui.py"),)
63
+ case "tui":
64
+ return (str(tests_dir / "test_tui.py"),)
65
+ case "integration":
66
+ return (str(tests_dir / "test_integration.py"),)
67
+ case "live":
68
+ return (str(tests_dir / "test_live_runtime_suite.py"),)
69
+ raise FlowValidationError(f"Unknown test slice: {slice_name}")
70
+
71
+
72
+ def run_tests(*, slice_name: str, list_slices: bool, app_root: Path) -> int:
73
+ if list_slices:
74
+ for name in TEST_SLICE_CHOICES:
75
+ print(name)
76
+ return 0
77
+ checkout_tests_dir(app_root)
78
+ raise_open_file_limit()
79
+ command = [sys.executable, "-m", "pytest", "-q", *test_slice_args(slice_name, app_root=app_root)]
80
+ return subprocess.run(command, check=False).returncode
@@ -0,0 +1,100 @@
1
+ """Surface-launch command helpers for the CLI surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import time
11
+
12
+ from data_engine.authoring.model import FlowValidationError
13
+
14
+
15
+ def start_surface(surface: str) -> int:
16
+ """Launch one operator surface."""
17
+ if surface == "gui":
18
+ return start_gui_subprocess()
19
+ if surface == "tui":
20
+ return launch_terminal_ui()
21
+ raise FlowValidationError(f"Unknown surface: {surface}")
22
+
23
+
24
+ def preferred_gui_python_executable() -> Path:
25
+ """Return the preferred Python executable for detached GUI launches."""
26
+ # Preserve the active interpreter path instead of resolving symlinks.
27
+ # On macOS, resolving a venv python can collapse back to the base framework
28
+ # interpreter, which loses the installed package context for child launches.
29
+ executable = Path(sys.executable).expanduser()
30
+ if os.name == "nt":
31
+ candidate = executable.with_name("pythonw.exe")
32
+ return candidate if candidate.exists() else executable
33
+ if sys.platform == "darwin":
34
+ candidate = executable.with_name("pythonw")
35
+ return candidate if candidate.exists() else executable
36
+ return executable
37
+
38
+
39
+ def start_gui_subprocess() -> int:
40
+ """Spawn the desktop GUI in a detached process."""
41
+ command = [str(preferred_gui_python_executable()), "-m", "data_engine.ui.gui.launcher"]
42
+ log_fd, log_path_text = tempfile.mkstemp(prefix="data-engine-gui-start-", suffix=".log")
43
+ os.close(log_fd)
44
+ log_path = Path(log_path_text)
45
+ with log_path.open("w", encoding="utf-8") as startup_log:
46
+ kwargs: dict[str, object] = {
47
+ "cwd": str(Path.cwd()),
48
+ "env": dict(os.environ),
49
+ "stdin": subprocess.DEVNULL,
50
+ "stdout": startup_log,
51
+ "stderr": startup_log,
52
+ }
53
+ if os.name == "nt":
54
+ creationflags = 0
55
+ detached_process = getattr(subprocess, "DETACHED_PROCESS", 0)
56
+ create_new_process_group = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
57
+ creationflags = detached_process | create_new_process_group
58
+ if creationflags:
59
+ kwargs["creationflags"] = creationflags
60
+ else:
61
+ kwargs["start_new_session"] = True
62
+ process = subprocess.Popen(command, **kwargs)
63
+ time.sleep(0.3)
64
+ exit_code = process.poll()
65
+ if exit_code is not None:
66
+ startup_output = log_path.read_text(encoding="utf-8").strip()
67
+ print(
68
+ "Data Engine GUI exited during startup."
69
+ f" See startup log: {log_path}"
70
+ + (f"\n{startup_output}" if startup_output else ""),
71
+ file=sys.stderr,
72
+ )
73
+ return exit_code or 1
74
+ print("Started Data Engine GUI.")
75
+ return 0
76
+
77
+
78
+ def launch_desktop_ui(*, theme_name: str | None = None) -> int:
79
+ """Launch the PySide desktop UI in the current process."""
80
+ from data_engine.ui.gui.launcher import launch
81
+
82
+ launch(theme_name=theme_name or "light")
83
+ return 0
84
+
85
+
86
+ def launch_terminal_ui() -> int:
87
+ """Launch the Textual terminal UI in the current process."""
88
+ from data_engine.ui.tui.app import main as tui_main
89
+
90
+ tui_main()
91
+ return 0
92
+
93
+
94
+ __all__ = [
95
+ "launch_desktop_ui",
96
+ "launch_terminal_ui",
97
+ "preferred_gui_python_executable",
98
+ "start_gui_subprocess",
99
+ "start_surface",
100
+ ]
@@ -0,0 +1,97 @@
1
+ """Workspace scaffolding helpers for the CLI surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from data_engine.authoring.model import FlowValidationError
8
+ from data_engine.platform.workspace_models import (
9
+ WORKSPACE_CONFIG_DIR_NAME,
10
+ WORKSPACE_DATABASES_DIR_NAME,
11
+ WORKSPACE_FLOW_HELPERS_DIR_NAME,
12
+ WORKSPACE_FLOW_MODULES_DIR_NAME,
13
+ WorkspaceSettings,
14
+ validate_workspace_id,
15
+ )
16
+ from data_engine.services.workspace_provisioning import (
17
+ collection_vscode_settings as build_collection_vscode_settings,
18
+ checkout_source_dir,
19
+ checkout_tests_dir,
20
+ write_collection_vscode_settings as persist_collection_vscode_settings,
21
+ workspace_vscode_settings as build_workspace_vscode_settings,
22
+ write_workspace_vscode_settings as persist_workspace_vscode_settings,
23
+ )
24
+
25
+
26
+ def create_command(args, *, dependencies) -> int:
27
+ """Dispatch one create subcommand."""
28
+ if args.create_command == "workspace":
29
+ return create_workspace(args.path, dependencies=dependencies)
30
+ raise FlowValidationError(f"Unknown create command: {args.create_command}")
31
+
32
+
33
+ def create_workspace(path: Path, *, dependencies) -> int:
34
+ """Create one authored workspace scaffold and select it as default."""
35
+ target = path.expanduser().resolve()
36
+ workspace_id = validate_workspace_id(target.name)
37
+ if target.exists():
38
+ if not target.is_dir():
39
+ raise FlowValidationError(f"Workspace path is not a directory: {target}")
40
+ if any(target.iterdir()):
41
+ raise FlowValidationError(f"Refusing to create workspace in a non-empty directory: {target}")
42
+ target.mkdir(parents=True, exist_ok=True)
43
+ for child in (
44
+ WORKSPACE_FLOW_MODULES_DIR_NAME,
45
+ f"{WORKSPACE_FLOW_MODULES_DIR_NAME}/{WORKSPACE_FLOW_HELPERS_DIR_NAME}",
46
+ WORKSPACE_CONFIG_DIR_NAME,
47
+ WORKSPACE_DATABASES_DIR_NAME,
48
+ ):
49
+ (target / child).mkdir(parents=True, exist_ok=True)
50
+ write_collection_vscode_settings(target.parent, dependencies=dependencies)
51
+ write_workspace_vscode_settings(target, dependencies=dependencies)
52
+ settings = dependencies.app_state_policy.load_settings()
53
+ dependencies.app_state_policy.write_settings(
54
+ WorkspaceSettings(
55
+ app_root=settings.app_root,
56
+ settings_path=settings.settings_path,
57
+ state_root=settings.state_root,
58
+ runtime_root=settings.runtime_root,
59
+ workspace_collection_root=target.parent,
60
+ default_selected=workspace_id,
61
+ )
62
+ )
63
+ print(f"Created workspace: {target}")
64
+ print(f"Selected default workspace: {workspace_id}")
65
+ return 0
66
+
67
+
68
+ def workspace_vscode_settings(workspace_root: Path, *, app_root: Path) -> dict[str, object]:
69
+ """Return VS Code settings for one workspace, with dev extras only for checkout roots."""
70
+ return build_workspace_vscode_settings(workspace_root, app_root=app_root)
71
+
72
+
73
+ def collection_vscode_settings(collection_root: Path, *, app_root: Path) -> dict[str, object]:
74
+ """Return VS Code settings for one workspace collection root."""
75
+ return build_collection_vscode_settings(collection_root, app_root=app_root)
76
+
77
+
78
+ def write_collection_vscode_settings(collection_root: Path, *, dependencies) -> None:
79
+ """Write the collection-root VS Code settings file."""
80
+ app_root = dependencies.app_state_policy.load_settings().app_root
81
+ persist_collection_vscode_settings(collection_root, app_root=app_root, overwrite=True)
82
+
83
+
84
+ def write_workspace_vscode_settings(workspace_root: Path, *, dependencies) -> None:
85
+ """Write the workspace-local VS Code settings file."""
86
+ app_root = dependencies.app_state_policy.load_settings().app_root
87
+ persist_workspace_vscode_settings(workspace_root, app_root=app_root, overwrite=True)
88
+
89
+
90
+ __all__ = [
91
+ "collection_vscode_settings",
92
+ "create_command",
93
+ "create_workspace",
94
+ "write_collection_vscode_settings",
95
+ "workspace_vscode_settings",
96
+ "write_workspace_vscode_settings",
97
+ ]
@@ -0,0 +1,44 @@
1
+ """Dependency wiring for the CLI surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Callable
7
+
8
+ from data_engine.platform.workspace_policy import AppStatePolicy
9
+ from data_engine.services import SharedStateService, WorkspaceService
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class CliDependencies:
14
+ """Concrete collaborators used by the public CLI surface."""
15
+
16
+ app_state_policy: AppStatePolicy
17
+ shared_state_service: SharedStateService
18
+ workspace_service: WorkspaceService
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class CliDependencyFactories:
23
+ """Factories for the CLI's default concrete collaborators."""
24
+
25
+ app_state_policy_factory: Callable[[], AppStatePolicy]
26
+ shared_state_service_factory: Callable[[], SharedStateService]
27
+ workspace_service_factory: Callable[[], WorkspaceService]
28
+
29
+
30
+ def default_cli_dependency_factories() -> CliDependencyFactories:
31
+ return CliDependencyFactories(
32
+ app_state_policy_factory=AppStatePolicy,
33
+ shared_state_service_factory=SharedStateService,
34
+ workspace_service_factory=WorkspaceService,
35
+ )
36
+
37
+
38
+ def build_default_cli_dependencies(*, factories: CliDependencyFactories | None = None) -> CliDependencies:
39
+ factories = factories or default_cli_dependency_factories()
40
+ return CliDependencies(
41
+ app_state_policy=factories.app_state_policy_factory(),
42
+ shared_state_service=factories.shared_state_service_factory(),
43
+ workspace_service=factories.workspace_service_factory(),
44
+ )
@@ -0,0 +1,56 @@
1
+ """Argument parser construction for the CLI surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from data_engine.platform.identity import APP_DISPLAY_NAME, APP_DISTRIBUTION_NAME
9
+ from data_engine.ui.cli.commands_run import TEST_SLICE_CHOICES
10
+
11
+
12
+ class _HelpFormatter(argparse.RawDescriptionHelpFormatter):
13
+ """Readable CLI help with preserved examples."""
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ parser = argparse.ArgumentParser(
18
+ prog=APP_DISTRIBUTION_NAME,
19
+ description=f"{APP_DISPLAY_NAME} command-line interface.",
20
+ epilog=(
21
+ "Examples:\n"
22
+ " data-engine start gui\n"
23
+ " data-engine start tui\n"
24
+ " data-engine create workspace ~/workspaces/claims\n"
25
+ " data-engine run tests\n"
26
+ " data-engine run tests all\n"
27
+ " data-engine doctor"
28
+ ),
29
+ formatter_class=_HelpFormatter,
30
+ )
31
+ parser.add_argument("--workspace", type=Path, help="Authored workspace root to use.")
32
+ parser.add_argument("--app-root", type=Path, help=f"{APP_DISPLAY_NAME} project/app root used for local artifacts.")
33
+
34
+ subparsers = parser.add_subparsers(dest="command", required=True, metavar="{start,create,run,doctor}")
35
+
36
+ start_parser = subparsers.add_parser("start", help="Launch one Data Engine operator surface.")
37
+ start_subparsers = start_parser.add_subparsers(dest="start_command", required=True, metavar="{gui,tui}")
38
+ start_subparsers.add_parser("gui", help="Launch the desktop GUI.")
39
+ start_subparsers.add_parser("tui", help="Launch the terminal UI.")
40
+
41
+ create_parser = subparsers.add_parser("create", help="Create Data Engine scaffolding.")
42
+ create_subparsers = create_parser.add_subparsers(dest="create_command", required=True, metavar="{workspace}")
43
+ workspace_parser = create_subparsers.add_parser("workspace", help="Create and select one authored workspace.")
44
+ workspace_parser.add_argument("path", type=Path, help="Path to the workspace root to create.")
45
+
46
+ run_parser = subparsers.add_parser("run", help="Run helpful project tasks.")
47
+ run_subparsers = run_parser.add_subparsers(dest="run_command", required=True, metavar="{tests}")
48
+ tests_parser = run_subparsers.add_parser("tests", help="Run one curated test slice.")
49
+ tests_parser.add_argument("slice", nargs="?", default="unit", choices=TEST_SLICE_CHOICES, help="Named test slice to run.")
50
+ tests_parser.add_argument("--list-slices", action="store_true", help="Print the available named test slices.")
51
+
52
+ doctor_parser = subparsers.add_parser("doctor", help="Inspect the local Data Engine environment and workspace setup.")
53
+ doctor_subparsers = doctor_parser.add_subparsers(dest="doctor_command", required=False, metavar="{daemons}")
54
+ doctor_subparsers.add_parser("daemons", help="Inspect Data Engine daemon and related process state.")
55
+ return parser
56
+
@@ -0,0 +1,25 @@
1
+ """Qt GUI surface for Data Engine."""
2
+
3
+ from data_engine.domain import format_log_line
4
+ from data_engine.views import QtFlowCard, flow_category
5
+
6
+ __all__ = [
7
+ "DataEngineWindow",
8
+ "QtFlowCard",
9
+ "flow_category",
10
+ "format_log_line",
11
+ "launch",
12
+ "main",
13
+ ]
14
+
15
+
16
+ def __getattr__(name: str):
17
+ if name == "DataEngineWindow":
18
+ from data_engine.ui.gui.app import DataEngineWindow
19
+
20
+ return DataEngineWindow
21
+ if name in {"launch", "main"}:
22
+ from data_engine.ui.gui.launcher import launch, main
23
+
24
+ return {"launch": launch, "main": main}[name]
25
+ raise AttributeError(name)