Coverage for  / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / cli / commands / diagnostics / docs_command.py: 99%

84 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-01-26 17:59 +0000

1# SPDX-License-Identifier: Apache-2.0 

2# Copyright © 2025 Bijan Mousavi 

3 

4"""Docs command runtime for the Bijux CLI (IO + exit behavior).""" 

5 

6from __future__ import annotations 

7 

8import os 

9from pathlib import Path 

10 

11import typer 

12import typer.core 

13 

14from bijux_cli.cli.color import resolve_click_color 

15from bijux_cli.cli.commands.diagnostics.docs import ( 

16 _build_spec_payload, 

17 _resolve_output_target, 

18 _spec_mapping, 

19) 

20from bijux_cli.cli.core.command import ( 

21 contains_non_ascii_env, 

22 raise_exit_intent, 

23 record_history, 

24 validate_common_flags, 

25) 

26from bijux_cli.cli.core.constants import ( 

27 ENV_DOCS_OUT, 

28 ENV_TEST_IO_FAIL, 

29 OPT_FORMAT, 

30 OPT_LOG_LEVEL, 

31 OPT_PRETTY, 

32 OPT_QUIET, 

33) 

34from bijux_cli.cli.core.help_text import ( 

35 HELP_FORMAT, 

36 HELP_LOG_LEVEL, 

37 HELP_NO_PRETTY, 

38 HELP_QUIET, 

39) 

40from bijux_cli.core.di import DIContainer 

41from bijux_cli.core.enums import ( 

42 ErrorType, 

43 ExitCode, 

44) 

45from bijux_cli.core.exit_policy import ExitIntent, ExitIntentError 

46from bijux_cli.core.precedence import ( 

47 EffectiveConfig, 

48 Flags, 

49 OutputConfig, 

50 default_execution_policy, 

51) 

52from bijux_cli.core.runtime import AsyncTyper 

53from bijux_cli.services.diagnostics.contracts import DocsProtocol 

54 

55typer.core.rich = None # type: ignore[attr-defined] 

56 

57docs_app = AsyncTyper( 

58 name="docs", 

59 help="(-h, --help) Generate API specifications (OpenAPI-like) for Bijux CLI.", 

60 rich_markup_mode=None, 

61 context_settings={"help_option_names": ["-h", "--help"]}, 

62 no_args_is_help=False, 

63) 

64 

65OUT_OPTION = typer.Option( 

66 None, 

67 "--out", 

68 "-o", 

69 help="Output file path or '-' for stdout. If a directory is given, a default file name is used.", 

70) 

71 

72 

73def _resolve_docs_service() -> DocsProtocol: 

74 """Resolve the docs service from the DI container.""" 

75 return DIContainer.current().resolve(DocsProtocol) 

76 

77 

78def _resolve_docs_config() -> tuple[EffectiveConfig, OutputConfig]: 

79 """Resolve effective and output config for docs handling.""" 

80 try: 

81 effective = DIContainer.current().resolve(EffectiveConfig) 

82 except Exception: 

83 policy = default_execution_policy() 

84 effective = EffectiveConfig( 

85 flags=Flags( 

86 quiet=policy.quiet, 

87 log_level=policy.log_level, 

88 color=policy.color, 

89 format=policy.output_format, 

90 ) 

91 ) 

92 try: 

93 output = DIContainer.current().resolve(OutputConfig) 

94 except Exception: 

95 policy = default_execution_policy() 

96 output = OutputConfig( 

97 include_runtime=policy.log_policy.show_internal, 

98 pretty=policy.log_policy.pretty_default, 

99 log_level=effective.flags.log_level, 

100 color=effective.flags.color, 

101 format=effective.flags.format, 

102 log_policy=policy.log_policy, 

103 ) 

104 return effective, output 

105 

106 

107@docs_app.callback(invoke_without_command=True) 

108def docs( 

109 ctx: typer.Context, 

110 out: Path | None = OUT_OPTION, 

111 quiet: bool = typer.Option(False, *OPT_QUIET, help=HELP_QUIET), 

112 fmt: str = typer.Option("json", *OPT_FORMAT, help=HELP_FORMAT), 

113 pretty: bool = typer.Option(True, OPT_PRETTY, help=HELP_NO_PRETTY), 

114 log_level: str = typer.Option("info", *OPT_LOG_LEVEL, help=HELP_LOG_LEVEL), 

115) -> None: 

116 """Entrypoint for the `bijux docs` command.""" 

117 _ = (quiet, pretty, log_level) 

118 command = "docs" 

119 effective, output = _resolve_docs_config() 

120 effective_include_runtime = output.include_runtime 

121 effective_pretty = output.pretty 

122 log_level_value = output.log_level 

123 output_format = validate_common_flags( 

124 fmt, 

125 command, 

126 effective.flags.quiet, 

127 include_runtime=effective_include_runtime, 

128 log_level=log_level_value, 

129 ) 

130 

131 if contains_non_ascii_env(): 

132 raise_exit_intent( 

133 "Non-ASCII characters in environment variables", 

134 code=3, 

135 failure="ascii_env", 

136 error_type=ErrorType.ASCII, 

137 command=command, 

138 fmt=output_format, 

139 quiet=effective.flags.quiet, 

140 include_runtime=effective_include_runtime, 

141 log_level=log_level_value, 

142 ) 

143 

144 if ctx.args: 

145 stray = ctx.args[0] 

146 msg = ( 

147 f"No such option: {stray}" 

148 if stray.startswith("-") 

149 else f"Too many arguments: {' '.join(ctx.args)}" 

150 ) 

151 raise_exit_intent( 

152 msg, 

153 code=2, 

154 failure="args", 

155 error_type=ErrorType.USAGE, 

156 command=command, 

157 fmt=output_format, 

158 quiet=effective.flags.quiet, 

159 include_runtime=effective_include_runtime, 

160 log_level=log_level_value, 

161 ) 

162 

163 out_env = os.environ.get(ENV_DOCS_OUT) 

164 if out is None and out_env: 

165 out = Path(out_env) 

166 

167 target, path = _resolve_output_target(out, output_format) 

168 

169 try: 

170 spec = _build_spec_payload(effective_include_runtime) 

171 spec_mapping = _spec_mapping(spec) 

172 except ValueError as exc: 

173 raise_exit_intent( 

174 str(exc), 

175 code=3, 

176 failure="ascii", 

177 error_type=ErrorType.ASCII, 

178 command=command, 

179 fmt=output_format, 

180 quiet=effective.flags.quiet, 

181 include_runtime=effective_include_runtime, 

182 log_level=log_level_value, 

183 ) 

184 

185 docs_service = _resolve_docs_service() 

186 try: 

187 content = docs_service.render( 

188 spec_mapping, fmt=output_format, pretty=effective_pretty 

189 ) 

190 except Exception as exc: 

191 raise_exit_intent( 

192 f"Serialization failed: {exc}", 

193 code=1, 

194 failure="serialize", 

195 error_type=ErrorType.INTERNAL, 

196 command=command, 

197 fmt=output_format, 

198 quiet=effective.flags.quiet, 

199 include_runtime=effective_include_runtime, 

200 log_level=log_level_value, 

201 ) 

202 

203 if os.environ.get(ENV_TEST_IO_FAIL) == "1": 

204 raise_exit_intent( 

205 "Simulated I/O failure for test", 

206 code=1, 

207 failure="io_fail", 

208 error_type=ErrorType.INTERNAL, 

209 command=command, 

210 fmt=output_format, 

211 quiet=effective.flags.quiet, 

212 include_runtime=effective_include_runtime, 

213 log_level=log_level_value, 

214 ) 

215 

216 emit_output = not effective.flags.quiet 

217 if target == "-": 

218 if emit_output: 

219 typer.echo( 

220 content, 

221 color=resolve_click_color(quiet=False, fmt=output_format), 

222 err=False, 

223 ) 

224 record_history(command, 0) 

225 raise ExitIntentError( 

226 ExitIntent( 

227 code=ExitCode.SUCCESS, 

228 stream=None, 

229 payload=None, 

230 fmt=output_format, 

231 pretty=effective_pretty, 

232 show_traceback=False, 

233 ) 

234 ) 

235 

236 if path is None: 

237 raise_exit_intent( 

238 "Internal error: expected non-null output path", 

239 code=1, 

240 failure="internal", 

241 error_type=ErrorType.INTERNAL, 

242 command=command, 

243 fmt=output_format, 

244 quiet=effective.flags.quiet, 

245 include_runtime=effective_include_runtime, 

246 log_level=log_level_value, 

247 ) 

248 

249 parent = path.parent 

250 if not parent.exists(): 

251 raise_exit_intent( 

252 f"Output directory does not exist: {parent}", 

253 code=2, 

254 failure="output_dir", 

255 error_type=ErrorType.USER_INPUT, 

256 command=command, 

257 fmt=output_format, 

258 quiet=effective.flags.quiet, 

259 include_runtime=effective_include_runtime, 

260 log_level=log_level_value, 

261 ) 

262 

263 try: 

264 docs_service.write( 

265 spec_mapping, 

266 fmt=output_format, 

267 name=str(path), 

268 pretty=effective_pretty, 

269 ) 

270 except Exception as exc: 

271 raise_exit_intent( 

272 f"Failed to write spec: {exc}", 

273 code=2, 

274 failure="write", 

275 error_type=ErrorType.INTERNAL, 

276 command=command, 

277 fmt=output_format, 

278 quiet=effective.flags.quiet, 

279 include_runtime=effective_include_runtime, 

280 log_level=log_level_value, 

281 ) 

282 

283 record_history(command, 0) 

284 intent_payload = {"status": "written", "file": str(path)} if emit_output else None 

285 stream = "stdout" if emit_output else None 

286 raise ExitIntentError( 

287 ExitIntent( 

288 code=ExitCode.SUCCESS, 

289 stream=stream, 

290 payload=intent_payload, 

291 fmt=output_format, 

292 pretty=effective_pretty, 

293 show_traceback=False, 

294 ) 

295 ) 

296 

297 

298__all__ = ["docs_app", "docs"]