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
« 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
4"""Docs command runtime for the Bijux CLI (IO + exit behavior)."""
6from __future__ import annotations
8import os
9from pathlib import Path
11import typer
12import typer.core
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
55typer.core.rich = None # type: ignore[attr-defined]
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)
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)
73def _resolve_docs_service() -> DocsProtocol:
74 """Resolve the docs service from the DI container."""
75 return DIContainer.current().resolve(DocsProtocol)
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
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 )
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 )
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 )
163 out_env = os.environ.get(ENV_DOCS_OUT)
164 if out is None and out_env:
165 out = Path(out_env)
167 target, path = _resolve_output_target(out, output_format)
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
298__all__ = ["docs_app", "docs"]