Skip to content

Commit deef92f

Browse files
committed
Simplify: dedupe optional-key resolution, status cells, doctor checks
- config.resolve_api_key_optional centralizes the NotAuthenticated-swallowing that init/keys and onboard's _has_key each re-implemented. - theme.status_text wraps the repeated Text(value, status_style(value)) used by transcripts/sessions list rendering (drops two rich.text imports). - doctor._check collapses the repeated Check dict literals into one builder. https://claude.ai/code/session_01VCThQ9Rcvt28CQhyx6qPts
1 parent fd85338 commit deef92f

8 files changed

Lines changed: 106 additions & 106 deletions

File tree

‎aai_cli/commands/doctor.py‎

Lines changed: 84 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -44,80 +44,84 @@ def query_devices(self) -> Sequence[Mapping[str, object]]: ...
4444
}
4545

4646

47+
def _check(
48+
name: str,
49+
status: str,
50+
detail: str,
51+
*,
52+
fix: str | None = None,
53+
affects: list[str] | None = None,
54+
) -> Check:
55+
"""Assemble a Check. ``affects`` defaults to empty — an 'ok' check blocks nothing."""
56+
return {"name": name, "status": status, "affects": affects or [], "detail": detail, "fix": fix}
57+
58+
4759
def check_python() -> Check:
4860
v = sys.version_info
4961
version = f"{v.major}.{v.minor}.{v.micro}"
5062
if v >= (3, 12):
51-
return {"name": "python", "status": "ok", "affects": [], "detail": version, "fix": None}
52-
return {
53-
"name": "python",
54-
"status": "fail",
55-
"affects": ["everything"],
56-
"detail": f"Python {version} is too old; the CLI needs 3.12+",
57-
"fix": "Install Python 3.12 or newer, then reinstall the CLI.",
58-
}
63+
return _check("python", "ok", version)
64+
return _check(
65+
"python",
66+
"fail",
67+
f"Python {version} is too old; the CLI needs 3.12+",
68+
fix="Install Python 3.12 or newer, then reinstall the CLI.",
69+
affects=["everything"],
70+
)
5971

6072

6173
def _check_api_key(profile: str) -> Check:
62-
affects = ["everything"]
6374
try:
6475
key = config.resolve_api_key(profile=profile)
6576
except NotAuthenticated:
66-
return {
67-
"name": "api-key",
68-
"status": "fail",
69-
"affects": affects,
70-
"detail": "No API key found.",
71-
"fix": "Run 'aai login' (or set ASSEMBLYAI_API_KEY).",
72-
}
77+
return _check(
78+
"api-key",
79+
"fail",
80+
"No API key found.",
81+
fix="Run 'aai login' (or set ASSEMBLYAI_API_KEY).",
82+
affects=["everything"],
83+
)
7384
# validate_key doubles as the connectivity probe: it makes one cheap authed call,
7485
# so a pass means the key is valid AND api.assemblyai.com is reachable.
7586
try:
7687
valid = client.validate_key(key)
7788
except CLIError as exc:
78-
return {
79-
"name": "api-key",
80-
"status": "fail",
81-
"affects": affects,
82-
"detail": f"Could not reach AssemblyAI: {exc.message}",
83-
"fix": "Check your network/proxy and that api.assemblyai.com is reachable.",
84-
}
89+
return _check(
90+
"api-key",
91+
"fail",
92+
f"Could not reach AssemblyAI: {exc.message}",
93+
fix="Check your network/proxy and that api.assemblyai.com is reachable.",
94+
affects=["everything"],
95+
)
8596
if valid:
86-
return {
87-
"name": "api-key",
88-
"status": "ok",
89-
"affects": [],
90-
"detail": "API key is valid and AssemblyAI is reachable.",
91-
"fix": None,
92-
}
93-
return {
94-
"name": "api-key",
95-
"status": "fail",
96-
"affects": affects,
97-
"detail": "API key was rejected (HTTP 401).",
98-
"fix": "Run 'aai login' with a valid key.",
99-
}
97+
return _check("api-key", "ok", "API key is valid and AssemblyAI is reachable.")
98+
return _check(
99+
"api-key",
100+
"fail",
101+
"API key was rejected (HTTP 401).",
102+
fix="Run 'aai login' with a valid key.",
103+
affects=["everything"],
104+
)
100105

101106

102107
def check_ffmpeg() -> Check:
103108
# ffmpeg is ONLY used to stream non-WAV files or URLs (stream/agent), where it
104109
# decodes them to 16 kHz mono PCM on the fly. Plain `transcribe` (including
105110
# YouTube URLs) uploads the file to AssemblyAI and never invokes ffmpeg, so it is
106111
# not required for transcription.
107-
affects = ["stream/agent (non-WAV file or URL input)"]
108112
if shutil.which("ffmpeg"):
109-
return {"name": "ffmpeg", "status": "ok", "affects": [], "detail": "found", "fix": None}
110-
return {
111-
"name": "ffmpeg",
112-
"status": "warn",
113-
"affects": affects,
114-
"detail": (
113+
return _check("ffmpeg", "ok", "found")
114+
return _check(
115+
"ffmpeg",
116+
"warn",
117+
(
115118
"ffmpeg not found. Only needed to stream non-WAV files or URLs; "
116119
"transcription (including YouTube) works without it, as does streaming a "
117120
"16 kHz mono WAV."
118121
),
119-
"fix": "Install ffmpeg (macOS: brew install ffmpeg; Debian/Ubuntu: apt-get install ffmpeg).",
120-
}
122+
fix="Install ffmpeg (macOS: brew install ffmpeg; Debian/Ubuntu: apt-get install ffmpeg).",
123+
affects=["stream/agent (non-WAV file or URL input)"],
124+
)
121125

122126

123127
def _probe_input_devices() -> int:
@@ -144,59 +148,50 @@ def check_audio() -> Check:
144148
try:
145149
inputs = _probe_input_devices()
146150
except ImportError:
147-
return {
148-
"name": "audio",
149-
"status": "warn",
150-
"affects": affects,
151-
"detail": "sounddevice is not importable; the microphone can't be used.",
152-
"fix": "pip install --force-reinstall sounddevice",
153-
}
151+
return _check(
152+
"audio",
153+
"warn",
154+
"sounddevice is not importable; the microphone can't be used.",
155+
fix="pip install --force-reinstall sounddevice",
156+
affects=affects,
157+
)
154158
except Exception as exc: # noqa: BLE001 - any PortAudio/device failure is a soft warning
155-
return {
156-
"name": "audio",
157-
"status": "warn",
158-
"affects": affects,
159-
"detail": f"audio system unavailable: {exc}",
160-
"fix": "On Linux install PortAudio: sudo apt-get install libportaudio2",
161-
}
159+
return _check(
160+
"audio",
161+
"warn",
162+
f"audio system unavailable: {exc}",
163+
fix="On Linux install PortAudio: sudo apt-get install libportaudio2",
164+
affects=affects,
165+
)
162166
if inputs == 0:
163-
return {
164-
"name": "audio",
165-
"status": "warn",
166-
"affects": affects,
167-
"detail": "No microphone (input device) found.",
168-
"fix": "Connect a microphone; live mic input is needed for stream/agent.",
169-
}
170-
return {
171-
"name": "audio",
172-
"status": "ok",
173-
"affects": [],
174-
"detail": f"{inputs} microphone input device(s) available.",
175-
"fix": None,
176-
}
167+
return _check(
168+
"audio",
169+
"warn",
170+
"No microphone (input device) found.",
171+
fix="Connect a microphone; live mic input is needed for stream/agent.",
172+
affects=affects,
173+
)
174+
return _check("audio", "ok", f"{inputs} microphone input device(s) available.")
177175

178176

179177
def _check_coding_agent() -> Check:
180-
affects = ["aai setup install"]
181178
missing = [tool for tool in ("claude", "npx") if shutil.which(tool) is None]
182179
if not missing:
183-
return {
184-
"name": "coding-agent",
185-
"status": "ok",
186-
"affects": [],
187-
"detail": "claude and npx found; run 'aai setup install' to wire up the docs MCP + skills.",
188-
"fix": None,
189-
}
190-
return {
191-
"name": "coding-agent",
192-
"status": "warn",
193-
"affects": affects,
194-
"detail": f"not found: {', '.join(missing)}.",
195-
"fix": (
180+
return _check(
181+
"coding-agent",
182+
"ok",
183+
"claude and npx found; run 'aai setup install' to wire up the docs MCP + skills.",
184+
)
185+
return _check(
186+
"coding-agent",
187+
"warn",
188+
f"not found: {', '.join(missing)}.",
189+
fix=(
196190
"Install Claude Code (https://claude.com/claude-code) and Node.js, "
197191
"then run 'aai setup install'."
198192
),
199-
}
193+
affects=["aai setup install"],
194+
)
200195

201196

202197
def render(data: DoctorResult) -> str:

‎aai_cli/commands/sessions.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import typer
44
from rich.markup import escape
55
from rich.table import Table
6-
from rich.text import Text
76

87
from aai_cli import jsonshape, output, theme
98
from aai_cli.auth import ams
@@ -72,10 +71,9 @@ def render(data: list[dict[str, object]]) -> Table:
7271
"model",
7372
)
7473
for s in data:
75-
status_str = str(s["status"])
7674
table.add_row(
7775
escape(str(s["session_id"])),
78-
Text(status_str, style=theme.status_style(status_str)),
76+
theme.status_text(str(s["status"])),
7977
escape(str(s.get("created_at") or "")),
8078
escape(str(s.get("audio_duration_sec") or "")),
8179
escape(str(s.get("speech_model") or "")),

‎aai_cli/commands/transcripts.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import typer
44
from rich.markup import escape
55
from rich.table import Table
6-
from rich.text import Text
76

87
from aai_cli import choices, client, config, output, theme
98
from aai_cli.context import AppState, run_command
@@ -86,10 +85,9 @@ def body(state: AppState, json_mode: bool) -> None:
8685
def render(data: list[dict[str, object]]) -> Table:
8786
table = output.data_table("id", "status", "created")
8887
for row in data:
89-
status = str(row["status"])
9088
table.add_row(
9189
escape(str(row["id"])),
92-
Text(status, style=theme.status_style(status)),
90+
theme.status_text(str(row["status"])),
9391
escape(str(row.get("created", ""))),
9492
)
9593
return table

‎aai_cli/config.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,13 @@ def resolve_api_key(*, profile: str | None = None, api_key_flag: str | None = No
300300
if stored:
301301
return stored
302302
raise NotAuthenticated()
303+
304+
305+
def resolve_api_key_optional(*, profile: str | None = None) -> str | None:
306+
"""The same key chain as ``resolve_api_key`` (env -> keyring), but ``None`` instead
307+
of raising when no key is configured — for callers that work without one
308+
(``aai init`` scaffolding, the onboarding wizard's signed-in check)."""
309+
try:
310+
return resolve_api_key(profile=profile)
311+
except NotAuthenticated:
312+
return None

‎aai_cli/init/keys.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
from aai_cli import config
4-
from aai_cli.errors import NotAuthenticated
54

65

76
def resolve_optional_api_key(*, profile: str | None) -> str | None:
@@ -10,7 +9,4 @@ def resolve_optional_api_key(*, profile: str | None) -> str | None:
109
`aai init` scaffolds even without a key (writing a placeholder), so it must not
1110
fail the way run commands do.
1211
"""
13-
try:
14-
return config.resolve_api_key(profile=profile)
15-
except NotAuthenticated:
16-
return None
12+
return config.resolve_api_key_optional(profile=profile)

‎aai_cli/onboard/sections.py‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from aai_cli.commands import init as init_cmd
1212
from aai_cli.commands import setup as setup_cmd
1313
from aai_cli.context import AppState, persist_browser_login
14-
from aai_cli.errors import CLIError, NotAuthenticated
14+
from aai_cli.errors import CLIError
1515
from aai_cli.onboard.prompter import Prompter
1616

1717

@@ -29,11 +29,7 @@ class WizardContext:
2929

3030

3131
def _has_key(profile: str) -> bool:
32-
try:
33-
config.resolve_api_key(profile=profile)
34-
except NotAuthenticated:
35-
return False
36-
return True
32+
return config.resolve_api_key_optional(profile=profile) is not None
3733

3834

3935
def welcome(prompter: Prompter, _ctx: WizardContext) -> SectionResult:

‎aai_cli/theme.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import IO, Any
44

55
from rich.console import Console
6+
from rich.text import Text
67
from rich.theme import Theme
78

89
# AssemblyAI brand accent. Defined once so the whole CLI can be re-tinted here.
@@ -87,3 +88,8 @@ def status_style(status: str) -> str:
8788
if normalized in _WARN:
8889
return "aai.warn"
8990
return "aai.muted"
91+
92+
93+
def status_text(status: str) -> Text:
94+
"""A status string rendered in its semantic status color (see ``status_style``)."""
95+
return Text(status, style=status_style(status))

‎tests/test_doctor.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def test_check_python_flags_old_interpreter(monkeypatch):
136136
check = doctor.check_python()
137137
assert check["status"] == "fail"
138138
assert "3.9.0" in check["detail"]
139+
assert check["affects"] == ["everything"]
139140

140141

141142
def test_check_audio_handles_portaudio_failure(monkeypatch):

0 commit comments

Comments
 (0)