Coverage for src / tracekit / cli / main.py: 97%
96 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""TraceKit Core CLI Framework implementing CLI-001.
3Provides the main entry point for the tracekit command-line interface with
4support for multiple output formats and verbose logging.
7Example:
8 $ tracekit --help
9 $ tracekit characterize signal.wfm --output json
10 $ tracekit decode uart.wfm -vv
11 $ tracekit shell # Interactive REPL
12"""
14from __future__ import annotations
16import json
17import logging
18import sys
19from typing import Any
21import click
23# Configure logging
24logging.basicConfig(
25 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
26 level=logging.WARNING,
27)
28logger = logging.getLogger("tracekit")
31class OutputFormat:
32 """Output format handler for CLI results.
34 Supports JSON, CSV, HTML, and table (default) output formats.
35 """
37 @staticmethod
38 def json(data: dict[str, Any]) -> str:
39 """Format as JSON."""
40 return json.dumps(data, indent=2, default=str)
42 @staticmethod
43 def csv(data: dict[str, Any]) -> str:
44 """Format as CSV (simplified)."""
45 lines = ["key,value"]
46 for key, value in data.items():
47 if isinstance(value, dict):
48 # Nested dict - flatten
49 for subkey, subvalue in value.items():
50 lines.append(f"{key}.{subkey},{subvalue}")
51 elif isinstance(value, list):
52 lines.append(f'{key},"{",".join(map(str, value))}"')
53 else:
54 lines.append(f"{key},{value}")
55 return "\n".join(lines)
57 @staticmethod
58 def html(data: dict[str, Any]) -> str:
59 """Format as HTML."""
60 html_parts = [
61 "<!DOCTYPE html>",
62 "<html>",
63 "<head>",
64 "<meta charset='utf-8'>",
65 "<title>TraceKit Analysis Results</title>",
66 "<style>",
67 "body { font-family: Arial, sans-serif; margin: 20px; }",
68 "table { border-collapse: collapse; width: 100%; }",
69 "th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }",
70 "th { background-color: #4CAF50; color: white; }",
71 "tr:nth-child(even) { background-color: #f2f2f2; }",
72 "</style>",
73 "</head>",
74 "<body>",
75 "<h1>TraceKit Analysis Results</h1>",
76 "<table>",
77 "<tr><th>Parameter</th><th>Value</th></tr>",
78 ]
80 for key, value in data.items():
81 html_parts.append(f"<tr><td>{key}</td><td>{value}</td></tr>")
83 html_parts.extend(
84 [
85 "</table>",
86 "</body>",
87 "</html>",
88 ]
89 )
91 return "\n".join(html_parts)
93 @staticmethod
94 def table(data: dict[str, Any]) -> str:
95 """Format as ASCII table."""
96 if not data:
97 return "No data"
99 # Calculate column widths
100 max_key = max(len(str(k)) for k in data)
101 max_val = max(len(str(v)) for v in data.values())
103 # Build table
104 lines = []
105 lines.append("=" * (max_key + max_val + 7))
106 lines.append(f"{'Parameter':{max_key}} | Value")
107 lines.append("-" * (max_key + max_val + 7))
109 for key, value in data.items():
110 lines.append(f"{key!s:{max_key}} | {value}")
112 lines.append("=" * (max_key + max_val + 7))
114 return "\n".join(lines)
117def format_output(data: dict[str, Any], format_type: str) -> str:
118 """Format output data according to specified format.
120 Args:
121 data: Dictionary of results to format.
122 format_type: Output format ('json', 'csv', 'html', 'table').
124 Returns:
125 Formatted string.
126 """
127 formatter = getattr(OutputFormat, format_type, OutputFormat.table)
128 return formatter(data)
131@click.group() # type: ignore[misc]
132@click.option( # type: ignore[misc]
133 "-v",
134 "--verbose",
135 count=True,
136 help="Increase verbosity (-v for INFO, -vv for DEBUG).",
137)
138@click.version_option(version="0.1.0", prog_name="tracekit") # type: ignore[misc]
139@click.pass_context # type: ignore[misc]
140def cli(ctx: click.Context, verbose: int) -> None:
141 """TraceKit - Signal Analysis Framework for Oscilloscope Data.
143 Command-line tools for characterizing buffers, decoding protocols,
144 analyzing spectra, and comparing signals.
146 Args:
147 ctx: Click context object.
148 verbose: Verbosity level (0=WARNING, 1=INFO, 2+=DEBUG).
150 Examples:
151 tracekit characterize signal.wfm
152 tracekit decode uart.wfm --protocol auto
153 tracekit batch '*.wfm' --analysis characterize
154 tracekit compare before.wfm after.wfm
155 tracekit shell # Interactive REPL
156 """
157 # Ensure ctx.obj exists
158 ctx.ensure_object(dict)
160 # Set logging level based on verbosity
161 if verbose == 0:
162 logger.setLevel(logging.WARNING)
163 elif verbose == 1: 163 ↛ 167line 163 didn't jump to line 167 because the condition on line 163 was always true
164 logger.setLevel(logging.INFO)
165 logger.info("Verbose mode enabled")
166 else: # verbose >= 2
167 logger.setLevel(logging.DEBUG)
168 logger.debug("Debug mode enabled")
170 ctx.obj["verbose"] = verbose
173@click.command() # type: ignore[misc]
174def shell() -> None:
175 """Start an interactive TraceKit shell.
177 Opens a Python REPL with TraceKit pre-imported and ready to use.
178 Features tab completion, persistent history, and helpful shortcuts.
180 Example:
181 $ tracekit shell
182 TraceKit Shell v0.1.0
183 >>> trace = load("signal.wfm")
184 >>> rise_time(trace)
185 """
186 from tracekit.cli.shell import start_shell
188 start_shell()
191@click.command() # type: ignore[misc]
192@click.argument("tutorial_id", required=False, default=None) # type: ignore[misc]
193@click.option("--list", "list_tutorials", is_flag=True, help="List available tutorials") # type: ignore[misc]
194def tutorial(tutorial_id: str | None, list_tutorials: bool) -> None:
195 """Run an interactive tutorial.
197 Provides step-by-step guidance for learning TraceKit.
199 Args:
200 tutorial_id: ID of the tutorial to run (or None to list).
201 list_tutorials: If True, list available tutorials.
203 Examples:
204 tracekit tutorial --list # List available tutorials
205 tracekit tutorial getting_started # Run the getting started tutorial
206 """
207 from tracekit.onboarding import list_tutorials as list_tut
208 from tracekit.onboarding import run_tutorial
210 if list_tutorials or tutorial_id is None:
211 tutorials = list_tut()
212 click.echo("Available tutorials:")
213 for t in tutorials:
214 click.echo(f" {t['id']}: {t['title']} ({t['difficulty']}, {t['steps']} steps)")
215 if tutorial_id is None: 215 ↛ 217line 215 didn't jump to line 217 because the condition on line 215 was always true
216 click.echo("\nRun with: tracekit tutorial <tutorial_id>")
217 return
219 run_tutorial(tutorial_id, interactive=True)
222# Import subcommands
223from tracekit.cli.batch import batch # noqa: E402
224from tracekit.cli.characterize import characterize # noqa: E402
225from tracekit.cli.compare import compare # noqa: E402
226from tracekit.cli.decode import decode # noqa: E402
228# Register subcommands
229cli.add_command(characterize) # type: ignore[has-type]
230cli.add_command(decode) # type: ignore[has-type]
231cli.add_command(batch) # type: ignore[has-type]
232cli.add_command(compare) # type: ignore[has-type]
233cli.add_command(shell)
234cli.add_command(tutorial)
237def main() -> None:
238 """Entry point for the tracekit CLI."""
239 try:
240 cli(obj={})
241 except Exception as e:
242 logger.error(f"Fatal error: {e}")
243 sys.exit(1)
246if __name__ == "__main__":
247 main()