Add src/selectel_ml_tui/cli.py
This commit is contained in:
@@ -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()
|
||||||
Reference in New Issue
Block a user