aptdata 0.0.2__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.
- aptdata/__init__.py +3 -0
- aptdata/cli/__init__.py +5 -0
- aptdata/cli/app.py +247 -0
- aptdata/cli/commands/__init__.py +9 -0
- aptdata/cli/commands/config_cmd.py +128 -0
- aptdata/cli/commands/mesh_cmd.py +435 -0
- aptdata/cli/commands/plugin_cmd.py +107 -0
- aptdata/cli/commands/system_cmd.py +90 -0
- aptdata/cli/commands/telemetry_cmd.py +57 -0
- aptdata/cli/completions.py +56 -0
- aptdata/cli/interactive.py +269 -0
- aptdata/cli/rendering/__init__.py +31 -0
- aptdata/cli/rendering/console.py +119 -0
- aptdata/cli/rendering/logger.py +26 -0
- aptdata/cli/rendering/panels.py +87 -0
- aptdata/cli/rendering/tables.py +81 -0
- aptdata/cli/scaffold.py +1089 -0
- aptdata/config/__init__.py +13 -0
- aptdata/config/parser.py +136 -0
- aptdata/config/schema.py +27 -0
- aptdata/config/secrets.py +60 -0
- aptdata/core/__init__.py +46 -0
- aptdata/core/context.py +31 -0
- aptdata/core/dataset.py +39 -0
- aptdata/core/lineage.py +213 -0
- aptdata/core/state.py +27 -0
- aptdata/core/system.py +317 -0
- aptdata/core/workflow.py +372 -0
- aptdata/mcp/__init__.py +5 -0
- aptdata/mcp/server.py +198 -0
- aptdata/plugins/__init__.py +77 -0
- aptdata/plugins/ai/__init__.py +6 -0
- aptdata/plugins/ai/chunking.py +66 -0
- aptdata/plugins/ai/embeddings.py +56 -0
- aptdata/plugins/base.py +57 -0
- aptdata/plugins/dataset.py +62 -0
- aptdata/plugins/governance/__init__.py +32 -0
- aptdata/plugins/governance/catalog.py +115 -0
- aptdata/plugins/governance/classification.py +44 -0
- aptdata/plugins/governance/lineage_store.py +49 -0
- aptdata/plugins/governance/rules.py +180 -0
- aptdata/plugins/local_fs.py +241 -0
- aptdata/plugins/manager.py +142 -0
- aptdata/plugins/postgres.py +113 -0
- aptdata/plugins/quality/__init__.py +39 -0
- aptdata/plugins/quality/contract.py +128 -0
- aptdata/plugins/quality/expectations.py +310 -0
- aptdata/plugins/quality/report.py +94 -0
- aptdata/plugins/quality/validator.py +139 -0
- aptdata/plugins/rest.py +135 -0
- aptdata/plugins/transform/__init__.py +14 -0
- aptdata/plugins/transform/pandas.py +129 -0
- aptdata/plugins/transform/spark.py +134 -0
- aptdata/plugins/vector/__init__.py +6 -0
- aptdata/plugins/vector/base.py +19 -0
- aptdata/plugins/vector/qdrant.py +41 -0
- aptdata/telemetry/__init__.py +5 -0
- aptdata/telemetry/instrumentation.py +164 -0
- aptdata/tui/__init__.py +5 -0
- aptdata/tui/monitor.py +279 -0
- aptdata-0.0.2.dist-info/METADATA +330 -0
- aptdata-0.0.2.dist-info/RECORD +65 -0
- aptdata-0.0.2.dist-info/WHEEL +4 -0
- aptdata-0.0.2.dist-info/entry_points.txt +3 -0
- aptdata-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Dynamic autocompletion functions for aptdata CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def complete_system_names(incomplete: str) -> list[str]:
|
|
7
|
+
"""Return registered system names matching *incomplete*."""
|
|
8
|
+
try:
|
|
9
|
+
from aptdata.plugins import registry # noqa: PLC0415
|
|
10
|
+
|
|
11
|
+
return [n for n in registry.list_systems() if n.startswith(incomplete)]
|
|
12
|
+
except Exception: # noqa: BLE001
|
|
13
|
+
return []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def complete_reader_names(incomplete: str) -> list[str]:
|
|
17
|
+
"""Return registered reader plugin names matching *incomplete*."""
|
|
18
|
+
try:
|
|
19
|
+
from aptdata.plugins import plugin_manager # noqa: PLC0415
|
|
20
|
+
|
|
21
|
+
return [n for n in plugin_manager.list_readers() if n.startswith(incomplete)]
|
|
22
|
+
except Exception: # noqa: BLE001
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def complete_writer_names(incomplete: str) -> list[str]:
|
|
27
|
+
"""Return registered writer plugin names matching *incomplete*."""
|
|
28
|
+
try:
|
|
29
|
+
from aptdata.plugins import plugin_manager # noqa: PLC0415
|
|
30
|
+
|
|
31
|
+
return [n for n in plugin_manager.list_writers() if n.startswith(incomplete)]
|
|
32
|
+
except Exception: # noqa: BLE001
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def complete_plugin_names(incomplete: str) -> list[str]:
|
|
37
|
+
"""Return all registered plugin names (readers + writers) matching *incomplete*."""
|
|
38
|
+
return sorted(
|
|
39
|
+
set(complete_reader_names(incomplete)) | set(complete_writer_names(incomplete))
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def complete_env_names(incomplete: str) -> list[str]:
|
|
44
|
+
"""Return common environment names matching *incomplete*."""
|
|
45
|
+
envs = ["dev", "staging", "prod", "test", "local"]
|
|
46
|
+
return [e for e in envs if e.startswith(incomplete)]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def complete_template_names(incomplete: str) -> list[str]:
|
|
50
|
+
"""Return scaffold template names matching *incomplete*."""
|
|
51
|
+
try:
|
|
52
|
+
from aptdata.cli.scaffold import TEMPLATE_NAMES # noqa: PLC0415
|
|
53
|
+
|
|
54
|
+
return [t for t in TEMPLATE_NAMES if t.startswith(incomplete)]
|
|
55
|
+
except Exception: # noqa: BLE001
|
|
56
|
+
return []
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Interactive wizard for aptdata CLI.
|
|
2
|
+
|
|
3
|
+
Uses *questionary* (if available) for prompt-driven UX; falls back to
|
|
4
|
+
plain ``typer.prompt()`` when questionary is not installed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from aptdata.cli.rendering.console import SmartConsole
|
|
12
|
+
|
|
13
|
+
_console = SmartConsole(json_mode=False)
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# questionary availability
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import questionary # type: ignore[import]
|
|
21
|
+
|
|
22
|
+
_HAS_QUESTIONARY = True
|
|
23
|
+
except ModuleNotFoundError:
|
|
24
|
+
_HAS_QUESTIONARY = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _select(message: str, choices: list[str]) -> str:
|
|
28
|
+
"""Prompt user to select from *choices*."""
|
|
29
|
+
if _HAS_QUESTIONARY:
|
|
30
|
+
answer = questionary.select(message, choices=choices).ask()
|
|
31
|
+
return answer or choices[0]
|
|
32
|
+
_console.print(f"\n{message}")
|
|
33
|
+
for i, c in enumerate(choices, 1):
|
|
34
|
+
_console.print(f" {i}. {c}")
|
|
35
|
+
idx_str = typer.prompt("Enter number", default="1")
|
|
36
|
+
try:
|
|
37
|
+
idx = int(idx_str) - 1
|
|
38
|
+
return choices[max(0, min(idx, len(choices) - 1))]
|
|
39
|
+
except ValueError:
|
|
40
|
+
return choices[0]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _text(message: str, default: str = "") -> str:
|
|
44
|
+
"""Prompt user for free text."""
|
|
45
|
+
if _HAS_QUESTIONARY:
|
|
46
|
+
answer = questionary.text(message, default=default).ask()
|
|
47
|
+
return answer if answer is not None else default
|
|
48
|
+
return typer.prompt(message, default=default)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _confirm(message: str, default: bool = True) -> bool:
|
|
52
|
+
"""Prompt user for yes/no confirmation."""
|
|
53
|
+
if _HAS_QUESTIONARY:
|
|
54
|
+
answer = questionary.confirm(message, default=default).ask()
|
|
55
|
+
return answer if answer is not None else default
|
|
56
|
+
return typer.confirm(message, default=default)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Wizard flows
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
_MAIN_MENU = [
|
|
64
|
+
"🚀 Run a registered system",
|
|
65
|
+
"📋 List systems / plugins",
|
|
66
|
+
"🔍 Inspect a plugin",
|
|
67
|
+
"📝 Config (validate / run YAML)",
|
|
68
|
+
"🏗️ Scaffold a new project",
|
|
69
|
+
"⚙️ Telemetry status",
|
|
70
|
+
"❌ Exit",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _wizard_run() -> None:
|
|
75
|
+
"""Guided wizard for running a registered system."""
|
|
76
|
+
from aptdata.plugins import registry # noqa: PLC0415
|
|
77
|
+
|
|
78
|
+
names = registry.list_systems()
|
|
79
|
+
if not names:
|
|
80
|
+
_console.warning("No systems registered.")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
name = _select("Select a system to run:", names)
|
|
84
|
+
env = _select("Select environment:", ["dev", "staging", "prod"])
|
|
85
|
+
dry_run = _confirm("Dry run (no execution)?", default=False)
|
|
86
|
+
|
|
87
|
+
_console.rule(f"Running '{name}' [{env}]")
|
|
88
|
+
system_cls = registry.get(name)
|
|
89
|
+
if system_cls is None:
|
|
90
|
+
_console.error(f"System '{name}' not found.")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
with _console.spinner(f"Executing '{name}'..."):
|
|
95
|
+
instance = system_cls(system_id=name)
|
|
96
|
+
if not dry_run:
|
|
97
|
+
instance.run()
|
|
98
|
+
_console.success(f"System '{name}' completed.")
|
|
99
|
+
except Exception as exc: # noqa: BLE001
|
|
100
|
+
_console.error(f"Execution failed: {exc}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _wizard_list() -> None:
|
|
104
|
+
"""Guided wizard for listing systems / plugins."""
|
|
105
|
+
from aptdata.cli.rendering.tables import ( # noqa: PLC0415
|
|
106
|
+
plugins_table,
|
|
107
|
+
systems_table,
|
|
108
|
+
)
|
|
109
|
+
from aptdata.plugins import plugin_manager, registry # noqa: PLC0415
|
|
110
|
+
|
|
111
|
+
choice = _select(
|
|
112
|
+
"What to list?",
|
|
113
|
+
["Systems", "Readers", "Writers", "All plugins"],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if choice == "Systems":
|
|
117
|
+
names = registry.list_systems()
|
|
118
|
+
if names:
|
|
119
|
+
_console.render(systems_table(names))
|
|
120
|
+
else:
|
|
121
|
+
_console.warning("No systems registered.")
|
|
122
|
+
elif choice in ("Readers", "Writers", "All plugins"):
|
|
123
|
+
plugins = plugin_manager.list_plugins()
|
|
124
|
+
_console.render(plugins_table(plugins))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _wizard_inspect() -> None:
|
|
128
|
+
"""Guided wizard for plugin inspection."""
|
|
129
|
+
from aptdata.cli.rendering.tables import plugin_schema_table # noqa: PLC0415
|
|
130
|
+
from aptdata.plugins import plugin_manager # noqa: PLC0415
|
|
131
|
+
|
|
132
|
+
all_plugins = sorted(plugin_manager.list_readers() + plugin_manager.list_writers())
|
|
133
|
+
if not all_plugins:
|
|
134
|
+
_console.warning("No plugins registered.")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
name = _select("Select a plugin to inspect:", all_plugins)
|
|
138
|
+
try:
|
|
139
|
+
schema = plugin_manager.get_plugin_schema(name)
|
|
140
|
+
_console.render(plugin_schema_table(schema))
|
|
141
|
+
except KeyError as exc:
|
|
142
|
+
_console.error(str(exc))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _wizard_config() -> None:
|
|
146
|
+
"""Guided wizard for YAML config validation / run."""
|
|
147
|
+
from pathlib import Path # noqa: PLC0415
|
|
148
|
+
|
|
149
|
+
from aptdata.cli.rendering.panels import yaml_preview # noqa: PLC0415
|
|
150
|
+
from aptdata.cli.rendering.tables import config_summary_table # noqa: PLC0415
|
|
151
|
+
from aptdata.config.parser import YamlConfigParser # noqa: PLC0415
|
|
152
|
+
|
|
153
|
+
action = _select("Config action:", ["Load and validate", "Generate template"])
|
|
154
|
+
|
|
155
|
+
if action == "Generate template":
|
|
156
|
+
from aptdata.cli.commands.config_cmd import _STARTER_YAML # noqa: PLC0415
|
|
157
|
+
|
|
158
|
+
out_path_str = _text("Output path:", default="pipeline.yaml")
|
|
159
|
+
out_path = Path(out_path_str)
|
|
160
|
+
if out_path.exists():
|
|
161
|
+
_console.warning(f"File '{out_path}' already exists.")
|
|
162
|
+
else:
|
|
163
|
+
out_path.write_text(_STARTER_YAML, encoding="utf-8")
|
|
164
|
+
_console.success(f"Template written to '{out_path}'.")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
path_str = _text("Path to YAML file:")
|
|
168
|
+
if not path_str:
|
|
169
|
+
_console.warning("No path provided.")
|
|
170
|
+
return
|
|
171
|
+
path = Path(path_str)
|
|
172
|
+
if not path.exists():
|
|
173
|
+
_console.error(f"File '{path}' not found.")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
parser = YamlConfigParser()
|
|
178
|
+
parsed = parser.parse_file(path)
|
|
179
|
+
_console.success("Config is valid.")
|
|
180
|
+
_console.render(config_summary_table(parsed))
|
|
181
|
+
|
|
182
|
+
content = path.read_text(encoding="utf-8")
|
|
183
|
+
if _confirm("Preview YAML?"):
|
|
184
|
+
_console.render(yaml_preview(content))
|
|
185
|
+
|
|
186
|
+
if _confirm("Run the system?", default=False):
|
|
187
|
+
with _console.spinner("Running..."):
|
|
188
|
+
parsed.system.run()
|
|
189
|
+
_console.success("Done.")
|
|
190
|
+
except Exception as exc: # noqa: BLE001
|
|
191
|
+
_console.error(f"Config error: {exc}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _wizard_scaffold() -> None:
|
|
195
|
+
"""Guided wizard for project scaffolding."""
|
|
196
|
+
from aptdata.cli.scaffold import TEMPLATE_NAMES # noqa: PLC0415
|
|
197
|
+
|
|
198
|
+
name = _text("Project name:")
|
|
199
|
+
if not name:
|
|
200
|
+
_console.warning("No name provided.")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
template = _select("Select template:", TEMPLATE_NAMES)
|
|
204
|
+
output = _text("Output directory:", default=".")
|
|
205
|
+
|
|
206
|
+
_console.rule(f"Scaffolding '{name}' [{template}]")
|
|
207
|
+
try:
|
|
208
|
+
from typer.testing import CliRunner # noqa: PLC0415
|
|
209
|
+
|
|
210
|
+
from aptdata.cli.app import app # noqa: PLC0415
|
|
211
|
+
|
|
212
|
+
runner = CliRunner()
|
|
213
|
+
result = runner.invoke(
|
|
214
|
+
app,
|
|
215
|
+
["scaffold", name, "--template", template, "--output", output],
|
|
216
|
+
)
|
|
217
|
+
if result.exit_code == 0:
|
|
218
|
+
_console.success(f"Project '{name}' created in '{output}/{name}'.")
|
|
219
|
+
else:
|
|
220
|
+
_console.error(f"Scaffold failed.\n{result.output}")
|
|
221
|
+
except Exception as exc: # noqa: BLE001
|
|
222
|
+
_console.error(f"Scaffold error: {exc}")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _wizard_telemetry() -> None:
|
|
226
|
+
"""Guided wizard for telemetry inspection."""
|
|
227
|
+
from aptdata.cli.commands.telemetry_cmd import (
|
|
228
|
+
_get_telemetry_status, # noqa: PLC0415
|
|
229
|
+
)
|
|
230
|
+
from aptdata.cli.rendering.tables import telemetry_status_table # noqa: PLC0415
|
|
231
|
+
|
|
232
|
+
status = _get_telemetry_status()
|
|
233
|
+
_console.render(telemetry_status_table(status))
|
|
234
|
+
|
|
235
|
+
if _confirm("Export telemetry as JSON?", default=False):
|
|
236
|
+
import json # noqa: PLC0415
|
|
237
|
+
|
|
238
|
+
_console.print(json.dumps({"telemetry": status}, indent=2))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Entry point
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def interactive_command() -> None:
|
|
247
|
+
"""Launch the interactive wizard for aptdata."""
|
|
248
|
+
_console.rule("[bold cyan]aptdata interactive wizard[/bold cyan]")
|
|
249
|
+
|
|
250
|
+
while True:
|
|
251
|
+
choice = _select("What would you like to do?", _MAIN_MENU)
|
|
252
|
+
|
|
253
|
+
if choice.startswith("🚀"):
|
|
254
|
+
_wizard_run()
|
|
255
|
+
elif choice.startswith("📋"):
|
|
256
|
+
_wizard_list()
|
|
257
|
+
elif choice.startswith("🔍"):
|
|
258
|
+
_wizard_inspect()
|
|
259
|
+
elif choice.startswith("📝"):
|
|
260
|
+
_wizard_config()
|
|
261
|
+
elif choice.startswith("🏗"):
|
|
262
|
+
_wizard_scaffold()
|
|
263
|
+
elif choice.startswith("⚙"):
|
|
264
|
+
_wizard_telemetry()
|
|
265
|
+
elif choice.startswith("❌"):
|
|
266
|
+
_console.success("Goodbye!")
|
|
267
|
+
break
|
|
268
|
+
|
|
269
|
+
_console.rule()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Rich rendering layer for aptdata CLI."""
|
|
2
|
+
|
|
3
|
+
from aptdata.cli.rendering.console import SmartConsole
|
|
4
|
+
from aptdata.cli.rendering.logger import setup_rich_logging
|
|
5
|
+
from aptdata.cli.rendering.panels import (
|
|
6
|
+
component_panel,
|
|
7
|
+
flow_tree,
|
|
8
|
+
system_detail_panel,
|
|
9
|
+
yaml_preview,
|
|
10
|
+
)
|
|
11
|
+
from aptdata.cli.rendering.tables import (
|
|
12
|
+
config_summary_table,
|
|
13
|
+
plugin_schema_table,
|
|
14
|
+
plugins_table,
|
|
15
|
+
systems_table,
|
|
16
|
+
telemetry_status_table,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"SmartConsole",
|
|
21
|
+
"systems_table",
|
|
22
|
+
"plugins_table",
|
|
23
|
+
"plugin_schema_table",
|
|
24
|
+
"config_summary_table",
|
|
25
|
+
"telemetry_status_table",
|
|
26
|
+
"system_detail_panel",
|
|
27
|
+
"flow_tree",
|
|
28
|
+
"yaml_preview",
|
|
29
|
+
"component_panel",
|
|
30
|
+
"setup_rich_logging",
|
|
31
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""SmartConsole — dual-mode (Rich / JSON) output for aptdata CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Generator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.status import Status
|
|
13
|
+
from rich.theme import Theme
|
|
14
|
+
|
|
15
|
+
_THEME = Theme(
|
|
16
|
+
{
|
|
17
|
+
"info": "bold cyan",
|
|
18
|
+
"success": "bold green",
|
|
19
|
+
"warning": "bold yellow",
|
|
20
|
+
"error": "bold red",
|
|
21
|
+
"event": "dim white",
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SmartConsole:
|
|
27
|
+
"""Dual-mode console: Rich for humans, JSON lines for machines.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
json_mode:
|
|
32
|
+
When *True*, all output is emitted as JSON lines (backward-compat).
|
|
33
|
+
When *False* (default), Rich markup is used for human-friendly output.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, json_mode: bool = False) -> None:
|
|
37
|
+
self.json_mode = json_mode
|
|
38
|
+
self._console = Console(theme=_THEME, highlight=False)
|
|
39
|
+
self._err_console = Console(theme=_THEME, stderr=True, highlight=False)
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
# Core output helpers
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def print(self, *args: Any, **kwargs: Any) -> None:
|
|
46
|
+
"""Print to stdout using Rich (or plain text in json_mode)."""
|
|
47
|
+
if self.json_mode:
|
|
48
|
+
print(*args, flush=True)
|
|
49
|
+
else:
|
|
50
|
+
self._console.print(*args, **kwargs)
|
|
51
|
+
|
|
52
|
+
def emit_event(self, event: str, **data: Any) -> None:
|
|
53
|
+
"""Emit a structured event. In JSON mode: JSON line. In Rich mode: formatted."""
|
|
54
|
+
payload = {"event": event, **data}
|
|
55
|
+
if self.json_mode:
|
|
56
|
+
print(json.dumps(payload, default=str), flush=True)
|
|
57
|
+
else:
|
|
58
|
+
self._console.print(
|
|
59
|
+
f"[event]▶ {event}[/event]", json.dumps(data, default=str)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def info(self, msg: str) -> None:
|
|
63
|
+
"""Emit an informational message."""
|
|
64
|
+
if self.json_mode:
|
|
65
|
+
print(
|
|
66
|
+
json.dumps({"level": "info", "message": msg}, default=str), flush=True
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
self._console.print(f"[info]ℹ {msg}[/info]")
|
|
70
|
+
|
|
71
|
+
def success(self, msg: str) -> None:
|
|
72
|
+
"""Emit a success message."""
|
|
73
|
+
if self.json_mode:
|
|
74
|
+
print(
|
|
75
|
+
json.dumps({"level": "success", "message": msg}, default=str),
|
|
76
|
+
flush=True,
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
self._console.print(f"[success]✓ {msg}[/success]")
|
|
80
|
+
|
|
81
|
+
def warning(self, msg: str) -> None:
|
|
82
|
+
"""Emit a warning message."""
|
|
83
|
+
if self.json_mode:
|
|
84
|
+
print(
|
|
85
|
+
json.dumps({"level": "warning", "message": msg}, default=str),
|
|
86
|
+
flush=True,
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
self._console.print(f"[warning]⚠ {msg}[/warning]")
|
|
90
|
+
|
|
91
|
+
def error(self, msg: str) -> None:
|
|
92
|
+
"""Emit an error message to stderr."""
|
|
93
|
+
if self.json_mode:
|
|
94
|
+
print(
|
|
95
|
+
json.dumps({"level": "error", "message": msg}, default=str),
|
|
96
|
+
file=sys.stderr,
|
|
97
|
+
flush=True,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
self._err_console.print(f"[error]✗ {msg}[/error]")
|
|
101
|
+
|
|
102
|
+
def rule(self, title: str = "") -> None:
|
|
103
|
+
"""Print a horizontal rule (no-op in json_mode)."""
|
|
104
|
+
if not self.json_mode:
|
|
105
|
+
self._console.rule(title)
|
|
106
|
+
|
|
107
|
+
def render(self, renderable: Any) -> None:
|
|
108
|
+
"""Render a Rich renderable (table, panel, tree, …). No-op in json_mode."""
|
|
109
|
+
if not self.json_mode:
|
|
110
|
+
self._console.print(renderable)
|
|
111
|
+
|
|
112
|
+
@contextmanager
|
|
113
|
+
def spinner(self, msg: str) -> Generator[None, None, None]:
|
|
114
|
+
"""Context manager that shows a spinner in Rich mode (no-op in json_mode)."""
|
|
115
|
+
if self.json_mode:
|
|
116
|
+
yield
|
|
117
|
+
else:
|
|
118
|
+
with Status(msg, console=self._console):
|
|
119
|
+
yield
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Rich logging setup for aptdata CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setup_rich_logging(level: int = logging.INFO) -> None:
|
|
9
|
+
"""Configure the root logger to use RichHandler for human-friendly output.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
level:
|
|
14
|
+
Logging level (default: INFO).
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
from rich.logging import RichHandler # noqa: PLC0415
|
|
18
|
+
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=level,
|
|
21
|
+
format="%(message)s",
|
|
22
|
+
datefmt="[%X]",
|
|
23
|
+
handlers=[RichHandler(rich_tracebacks=True, markup=True)],
|
|
24
|
+
)
|
|
25
|
+
except ImportError:
|
|
26
|
+
logging.basicConfig(level=level)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Rich panel / tree generators for aptdata CLI output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.syntax import Syntax
|
|
9
|
+
from rich.tree import Tree
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def system_detail_panel(name: str, system_cls: Any) -> Panel:
|
|
13
|
+
"""Return a Rich Panel with detailed system information."""
|
|
14
|
+
doc = (system_cls.__doc__ or "No description.").strip()
|
|
15
|
+
module = getattr(system_cls, "__module__", "unknown")
|
|
16
|
+
content = (
|
|
17
|
+
f"[bold]Class:[/bold] {system_cls.__name__}\n"
|
|
18
|
+
f"[bold]Module:[/bold] {module}\n\n{doc}"
|
|
19
|
+
)
|
|
20
|
+
return Panel(content, title=f"[bold cyan]System: {name}[/bold cyan]", expand=False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def flow_tree(flow: Any) -> Tree:
|
|
24
|
+
"""Return a Rich Tree representing the flow's component DAG."""
|
|
25
|
+
tree = Tree(f"[bold]{getattr(flow, 'flow_id', 'flow')}[/bold]")
|
|
26
|
+
components = getattr(flow, "components", [])
|
|
27
|
+
edges = getattr(flow, "edges", [])
|
|
28
|
+
|
|
29
|
+
# Build adjacency for display
|
|
30
|
+
children: dict[str, list[str]] = {}
|
|
31
|
+
for edge in edges:
|
|
32
|
+
src = getattr(edge, "source_id", "?")
|
|
33
|
+
tgt = getattr(edge, "target_id", "?")
|
|
34
|
+
children.setdefault(src, []).append(tgt)
|
|
35
|
+
|
|
36
|
+
added: set[str] = set()
|
|
37
|
+
|
|
38
|
+
def _add(node: str, branch: Any) -> None:
|
|
39
|
+
if node in added:
|
|
40
|
+
return
|
|
41
|
+
added.add(node)
|
|
42
|
+
b = branch.add(f"[green]{node}[/green]")
|
|
43
|
+
for child in children.get(node, []):
|
|
44
|
+
_add(child, b)
|
|
45
|
+
|
|
46
|
+
component_ids = [
|
|
47
|
+
getattr(c, "component_id", str(i)) for i, c in enumerate(components)
|
|
48
|
+
]
|
|
49
|
+
all_targets: set[str] = {t for targets in children.values() for t in targets}
|
|
50
|
+
roots = [cid for cid in component_ids if cid not in all_targets]
|
|
51
|
+
if not roots:
|
|
52
|
+
roots = component_ids[:1]
|
|
53
|
+
|
|
54
|
+
for root in roots:
|
|
55
|
+
_add(root, tree)
|
|
56
|
+
|
|
57
|
+
# Add any unconnected components
|
|
58
|
+
for cid in component_ids:
|
|
59
|
+
if cid not in added:
|
|
60
|
+
tree.add(f"[dim]{cid}[/dim]")
|
|
61
|
+
|
|
62
|
+
return tree
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def yaml_preview(content: str) -> Syntax:
|
|
66
|
+
"""Return a Rich Syntax object for YAML content."""
|
|
67
|
+
return Syntax(content, "yaml", theme="monokai", line_numbers=True)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def component_panel(component: Any) -> Panel:
|
|
71
|
+
"""Return a Rich Panel with component metadata."""
|
|
72
|
+
cid = getattr(component, "component_id", "unknown")
|
|
73
|
+
meta = getattr(component, "metadata", None)
|
|
74
|
+
lines = [f"[bold]ID:[/bold] {cid}"]
|
|
75
|
+
if meta is not None:
|
|
76
|
+
kind = getattr(meta, "kind", None)
|
|
77
|
+
if kind is not None:
|
|
78
|
+
lines.append(f"[bold]Kind:[/bold] {kind}")
|
|
79
|
+
tags = getattr(meta, "tags", [])
|
|
80
|
+
if tags:
|
|
81
|
+
lines.append(f"[bold]Tags:[/bold] {', '.join(tags)}")
|
|
82
|
+
desc = getattr(meta, "description", "")
|
|
83
|
+
if desc:
|
|
84
|
+
lines.append(f"[bold]Description:[/bold] {desc}")
|
|
85
|
+
return Panel(
|
|
86
|
+
"\n".join(lines), title=f"[bold cyan]Component: {cid}[/bold cyan]", expand=False
|
|
87
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Rich table generators for aptdata CLI output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from aptdata.config.parser import ParsedConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def systems_table(names: list[str]) -> Table:
|
|
14
|
+
"""Return a Rich Table of registered system names."""
|
|
15
|
+
table = Table(
|
|
16
|
+
title="Registered Systems", show_header=True, header_style="bold cyan"
|
|
17
|
+
)
|
|
18
|
+
table.add_column("#", style="dim", width=4)
|
|
19
|
+
table.add_column("Name", style="bold")
|
|
20
|
+
for i, name in enumerate(names, 1):
|
|
21
|
+
table.add_row(str(i), name)
|
|
22
|
+
return table
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def plugins_table(plugins: dict[str, list[str]]) -> Table:
|
|
26
|
+
"""Return a Rich Table of readers and writers."""
|
|
27
|
+
table = Table(
|
|
28
|
+
title="Registered Plugins", show_header=True, header_style="bold cyan"
|
|
29
|
+
)
|
|
30
|
+
table.add_column("Kind", style="bold magenta", width=10)
|
|
31
|
+
table.add_column("Name", style="bold")
|
|
32
|
+
for name in plugins.get("readers", []):
|
|
33
|
+
table.add_row("reader", name)
|
|
34
|
+
for name in plugins.get("writers", []):
|
|
35
|
+
table.add_row("writer", name)
|
|
36
|
+
return table
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def plugin_schema_table(schema: dict[str, Any]) -> Table:
|
|
40
|
+
"""Return a Rich Table of constructor arguments for a plugin."""
|
|
41
|
+
table = Table(
|
|
42
|
+
title=f"Plugin Schema: {schema.get('name', '')} ({schema.get('type', '')})",
|
|
43
|
+
show_header=True,
|
|
44
|
+
header_style="bold cyan",
|
|
45
|
+
)
|
|
46
|
+
table.add_column("Argument", style="bold")
|
|
47
|
+
table.add_column("Required", style="bold yellow")
|
|
48
|
+
table.add_column("Default", style="dim")
|
|
49
|
+
for arg in schema.get("arguments", []):
|
|
50
|
+
required = "✓" if arg.get("required") else ""
|
|
51
|
+
default = str(arg.get("default", "")) if not arg.get("required") else "-"
|
|
52
|
+
table.add_row(arg["name"], required, default)
|
|
53
|
+
return table
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def config_summary_table(parsed: ParsedConfig) -> Table:
|
|
57
|
+
"""Return a Rich Table summarising a parsed YAML config."""
|
|
58
|
+
table = Table(title="Config Summary", show_header=True, header_style="bold cyan")
|
|
59
|
+
table.add_column("Field", style="bold")
|
|
60
|
+
table.add_column("Value")
|
|
61
|
+
|
|
62
|
+
meta = parsed.metadata
|
|
63
|
+
system = parsed.system
|
|
64
|
+
|
|
65
|
+
table.add_row("system_id", system.system_id)
|
|
66
|
+
table.add_row("flows", str(len(getattr(system, "flows", []))))
|
|
67
|
+
|
|
68
|
+
for key, value in meta.items():
|
|
69
|
+
table.add_row(f"metadata.{key}", str(value))
|
|
70
|
+
|
|
71
|
+
return table
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def telemetry_status_table(status: dict[str, Any]) -> Table:
|
|
75
|
+
"""Return a Rich Table of OpenTelemetry status."""
|
|
76
|
+
table = Table(title="Telemetry Status", show_header=True, header_style="bold cyan")
|
|
77
|
+
table.add_column("Key", style="bold")
|
|
78
|
+
table.add_column("Value")
|
|
79
|
+
for key, value in status.items():
|
|
80
|
+
table.add_row(str(key), str(value))
|
|
81
|
+
return table
|