Add src/selectel_ml_tui/cli.py

This commit is contained in:
2026-09-02 16:41:59 +03:00
parent a1bc9ea099
commit db895ab565
+201
View File
@@ -0,0 +1,201 @@
"""CLI-интерфейс selectel-ml-tui."""
from __future__ import annotations
from pathlib import Path
from typing import cast
import click
from selectel_ml_tui import __version__
from selectel_ml_tui.config import (
Credentials,
Settings,
config_dir,
data_dir,
load_credentials,
load_settings,
save_credentials,
save_settings,
)
def _mask(value: str) -> str:
"""Замаскировать секрет: показать первые 4 символа и длину."""
if not value:
return "(не задан)"
return f"{value[:4]}…({len(value)})"
def _print_config(settings: Settings, credentials: Credentials) -> None:
click.echo(f"API base URL: {settings.api_base_url}")
click.echo(f"Identity URL: {settings.identity_url}")
click.echo(f"Refresh interval: {settings.refresh_interval} c")
router_id = settings.router_id or "(не задан, будет выбран первый)"
click.echo(f"Router id: {router_id}")
if settings.region:
click.echo(f"Region: {settings.region}")
click.echo(f"Service user: {credentials.service_user or '(не задан)'}")
click.echo(f"Project id: {credentials.project_id or '(не задан)'}")
click.echo(f"Domain id: {credentials.domain_id or '(не задан)'}")
click.echo(f"Static token: {_mask(credentials.static_token)}")
click.echo(f"Password: {_mask(credentials.password)}")
def _prompt_str(
label: str,
current: str,
hide_input: bool = False,
default_note: str = "(Enter — оставить)",
) -> str:
prompt = f"{label} [{current or 'пусто'}] {default_note}: "
value = click.prompt(prompt, default="", hide_input=hide_input, show_default=False)
if value == "":
return current
return cast(str, value)
@click.group()
@click.version_option(__version__, prog_name="selectel-ml-tui")
def main() -> None:
"""TUI-приложение для отображения потребления ИИ-роутера Selectel."""
@main.command()
@click.option("--path", is_flag=True, help="Показать пути к конфигурации и выйти")
@click.option("--show", is_flag=True, help="Показать текущие настройки и выйти")
@click.option(
"--non-interactive",
is_flag=True,
help="Не запрашивать ввод (эквивалентно --show)",
)
def config(path: bool, show: bool, non_interactive: bool) -> None:
"""Настроить приложение (интерактивный мастер) или показать настройки."""
if path:
click.echo(f"config dir: {config_dir()}")
click.echo(f"data dir: {data_dir()}")
return
settings = load_settings()
credentials = load_credentials()
if show or non_interactive:
_print_config(settings, credentials)
return
click.echo("Настройка selectel-ml-tui. Пустые значения сохраняют текущие.")
settings.api_base_url = _prompt_str("API base URL", settings.api_base_url)
while True:
raw = _prompt_str(
"Интервал обновления (сек, min 10)",
str(settings.refresh_interval),
)
try:
value = int(raw)
except ValueError:
click.echo("Ошибка: введите целое число секунд")
continue
if value < 10:
click.echo("Ошибка: интервал не может быть меньше 10 секунд")
continue
settings.refresh_interval = value
break
settings.router_id = _prompt_str(
"ID ИИ-роутера (необязательно)", settings.router_id
)
settings.region = _prompt_str("Регион (необязательно)", settings.region)
credentials.service_user = _prompt_str(
"Сервисный пользователь", credentials.service_user
)
credentials.domain_id = _prompt_str(
"Номер аккаунта (domain)", credentials.domain_id
)
credentials.project_id = _prompt_str("ID проекта (ML)", credentials.project_id)
credentials.password = _prompt_str(
"Пароль сервисного пользователя",
credentials.password,
hide_input=True,
)
credentials.static_token = _prompt_str(
"Статический токен (X-Token)",
credentials.static_token,
hide_input=True,
)
save_settings(settings)
save_credentials(credentials)
click.echo(f"Сохранено: {config_dir()}")
_print_config(settings, credentials)
@main.command()
@click.option(
"--format",
"fmt",
type=click.Choice(["csv", "json"]),
default="json",
help="Формат экспорта",
)
@click.option(
"--output",
"-o",
type=click.Path(path_type=Path),
default=None,
help="Путь к файлу экспорта (по умолчанию history.csv/json в каталоге данных)",
)
def export(fmt: str, output: Path | None) -> None:
"""Экспортировать историю потребления в CSV/JSON."""
from selectel_ml_tui.storage import Storage
path = output if output is not None else data_dir() / f"history.{fmt}"
storage = Storage()
try:
if fmt == "csv":
storage.export_csv(path)
else:
storage.export_json(path)
finally:
storage.close()
click.echo(f"Экспортировано: {path}")
@main.command()
def doctor() -> None:
"""Проверить окружение (ОС, терминал, кодировку) и пути."""
from selectel_ml_tui.platform import check_environment, get_platform_info
info = get_platform_info()
click.echo(f"ОС: {info.system}")
click.echo(f"Python: {info.python_version}")
failed = False
for check in check_environment():
mark = "OK " if check.ok else "WARN"
failed = failed or not check.ok
click.echo(f"[{mark}] {check.name}: {check.detail}")
credentials = load_credentials()
has_token = bool(credentials.static_token or credentials.password)
hint = "не настроен — выполните: selectel-ml-tui config"
token_status = "настроен" if has_token else hint
click.echo(f"[{'OK ' if has_token else 'WARN'}] token: {token_status}")
click.echo(f"Config dir: {config_dir()}")
click.echo(f"Data dir: {data_dir()}")
if failed:
raise click.exceptions.Exit(1)
@main.command()
def run() -> None:
"""Запустить TUI-интерфейс (дашборд)."""
from selectel_ml_tui.ui.app import run_app
credentials = load_credentials()
if not credentials.static_token and not credentials.password:
msg = "Учётные данные не настроены. Выполните: selectel-ml-tui config"
click.echo(msg, err=True)
raise click.exceptions.Exit(1)
run_app()
if __name__ == "__main__":
main()