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,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)
|