diff --git a/src/selectel_ml_tui/cli.py b/src/selectel_ml_tui/cli.py new file mode 100644 index 0000000..186ea90 --- /dev/null +++ b/src/selectel_ml_tui/cli.py @@ -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()