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.
Files changed (65) hide show
  1. aptdata/__init__.py +3 -0
  2. aptdata/cli/__init__.py +5 -0
  3. aptdata/cli/app.py +247 -0
  4. aptdata/cli/commands/__init__.py +9 -0
  5. aptdata/cli/commands/config_cmd.py +128 -0
  6. aptdata/cli/commands/mesh_cmd.py +435 -0
  7. aptdata/cli/commands/plugin_cmd.py +107 -0
  8. aptdata/cli/commands/system_cmd.py +90 -0
  9. aptdata/cli/commands/telemetry_cmd.py +57 -0
  10. aptdata/cli/completions.py +56 -0
  11. aptdata/cli/interactive.py +269 -0
  12. aptdata/cli/rendering/__init__.py +31 -0
  13. aptdata/cli/rendering/console.py +119 -0
  14. aptdata/cli/rendering/logger.py +26 -0
  15. aptdata/cli/rendering/panels.py +87 -0
  16. aptdata/cli/rendering/tables.py +81 -0
  17. aptdata/cli/scaffold.py +1089 -0
  18. aptdata/config/__init__.py +13 -0
  19. aptdata/config/parser.py +136 -0
  20. aptdata/config/schema.py +27 -0
  21. aptdata/config/secrets.py +60 -0
  22. aptdata/core/__init__.py +46 -0
  23. aptdata/core/context.py +31 -0
  24. aptdata/core/dataset.py +39 -0
  25. aptdata/core/lineage.py +213 -0
  26. aptdata/core/state.py +27 -0
  27. aptdata/core/system.py +317 -0
  28. aptdata/core/workflow.py +372 -0
  29. aptdata/mcp/__init__.py +5 -0
  30. aptdata/mcp/server.py +198 -0
  31. aptdata/plugins/__init__.py +77 -0
  32. aptdata/plugins/ai/__init__.py +6 -0
  33. aptdata/plugins/ai/chunking.py +66 -0
  34. aptdata/plugins/ai/embeddings.py +56 -0
  35. aptdata/plugins/base.py +57 -0
  36. aptdata/plugins/dataset.py +62 -0
  37. aptdata/plugins/governance/__init__.py +32 -0
  38. aptdata/plugins/governance/catalog.py +115 -0
  39. aptdata/plugins/governance/classification.py +44 -0
  40. aptdata/plugins/governance/lineage_store.py +49 -0
  41. aptdata/plugins/governance/rules.py +180 -0
  42. aptdata/plugins/local_fs.py +241 -0
  43. aptdata/plugins/manager.py +142 -0
  44. aptdata/plugins/postgres.py +113 -0
  45. aptdata/plugins/quality/__init__.py +39 -0
  46. aptdata/plugins/quality/contract.py +128 -0
  47. aptdata/plugins/quality/expectations.py +310 -0
  48. aptdata/plugins/quality/report.py +94 -0
  49. aptdata/plugins/quality/validator.py +139 -0
  50. aptdata/plugins/rest.py +135 -0
  51. aptdata/plugins/transform/__init__.py +14 -0
  52. aptdata/plugins/transform/pandas.py +129 -0
  53. aptdata/plugins/transform/spark.py +134 -0
  54. aptdata/plugins/vector/__init__.py +6 -0
  55. aptdata/plugins/vector/base.py +19 -0
  56. aptdata/plugins/vector/qdrant.py +41 -0
  57. aptdata/telemetry/__init__.py +5 -0
  58. aptdata/telemetry/instrumentation.py +164 -0
  59. aptdata/tui/__init__.py +5 -0
  60. aptdata/tui/monitor.py +279 -0
  61. aptdata-0.0.2.dist-info/METADATA +330 -0
  62. aptdata-0.0.2.dist-info/RECORD +65 -0
  63. aptdata-0.0.2.dist-info/WHEEL +4 -0
  64. aptdata-0.0.2.dist-info/entry_points.txt +3 -0
  65. 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