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

1"""TraceKit Core CLI Framework implementing CLI-001. 

2 

3Provides the main entry point for the tracekit command-line interface with 

4support for multiple output formats and verbose logging. 

5 

6 

7Example: 

8 $ tracekit --help 

9 $ tracekit characterize signal.wfm --output json 

10 $ tracekit decode uart.wfm -vv 

11 $ tracekit shell # Interactive REPL 

12""" 

13 

14from __future__ import annotations 

15 

16import json 

17import logging 

18import sys 

19from typing import Any 

20 

21import click 

22 

23# Configure logging 

24logging.basicConfig( 

25 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 

26 level=logging.WARNING, 

27) 

28logger = logging.getLogger("tracekit") 

29 

30 

31class OutputFormat: 

32 """Output format handler for CLI results. 

33 

34 Supports JSON, CSV, HTML, and table (default) output formats. 

35 """ 

36 

37 @staticmethod 

38 def json(data: dict[str, Any]) -> str: 

39 """Format as JSON.""" 

40 return json.dumps(data, indent=2, default=str) 

41 

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) 

56 

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 ] 

79 

80 for key, value in data.items(): 

81 html_parts.append(f"<tr><td>{key}</td><td>{value}</td></tr>") 

82 

83 html_parts.extend( 

84 [ 

85 "</table>", 

86 "</body>", 

87 "</html>", 

88 ] 

89 ) 

90 

91 return "\n".join(html_parts) 

92 

93 @staticmethod 

94 def table(data: dict[str, Any]) -> str: 

95 """Format as ASCII table.""" 

96 if not data: 

97 return "No data" 

98 

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

102 

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

108 

109 for key, value in data.items(): 

110 lines.append(f"{key!s:{max_key}} | {value}") 

111 

112 lines.append("=" * (max_key + max_val + 7)) 

113 

114 return "\n".join(lines) 

115 

116 

117def format_output(data: dict[str, Any], format_type: str) -> str: 

118 """Format output data according to specified format. 

119 

120 Args: 

121 data: Dictionary of results to format. 

122 format_type: Output format ('json', 'csv', 'html', 'table'). 

123 

124 Returns: 

125 Formatted string. 

126 """ 

127 formatter = getattr(OutputFormat, format_type, OutputFormat.table) 

128 return formatter(data) 

129 

130 

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. 

142 

143 Command-line tools for characterizing buffers, decoding protocols, 

144 analyzing spectra, and comparing signals. 

145 

146 Args: 

147 ctx: Click context object. 

148 verbose: Verbosity level (0=WARNING, 1=INFO, 2+=DEBUG). 

149 

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) 

159 

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

169 

170 ctx.obj["verbose"] = verbose 

171 

172 

173@click.command() # type: ignore[misc] 

174def shell() -> None: 

175 """Start an interactive TraceKit shell. 

176 

177 Opens a Python REPL with TraceKit pre-imported and ready to use. 

178 Features tab completion, persistent history, and helpful shortcuts. 

179 

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 

187 

188 start_shell() 

189 

190 

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. 

196 

197 Provides step-by-step guidance for learning TraceKit. 

198 

199 Args: 

200 tutorial_id: ID of the tutorial to run (or None to list). 

201 list_tutorials: If True, list available tutorials. 

202 

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 

209 

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 

218 

219 run_tutorial(tutorial_id, interactive=True) 

220 

221 

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 

227 

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) 

235 

236 

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) 

244 

245 

246if __name__ == "__main__": 

247 main()