Conversation
There was a problem hiding this comment.
Pull request overview
Добавляет TUI/CLI клиент для управления ботами ITS Reg: конфигурация через env/.env, HTTP-клиент на httpx, интерактивное создание/просмотр/удаление ботов и экспорт ответов, а также dev-обвязку (Makefile, pre-commit, OpenAPI спецификация).
Changes:
- Добавлен Python-пакет
itsreg_cli(TUI меню, сервисный слой, API клиент, модели, импорт из JSON). - Добавлены OpenAPI-спека и конфиг для генерации клиента.
- Добавлены файлы для локальной разработки/запуска:
Makefile,requirements.txt,.pre-commit-config.yaml,pre-commit.sh, расширен.gitignore, обновлёнREADME.md.
Reviewed changes
Copilot reviewed 15 out of 23 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| requirements.txt | Базовые runtime-зависимости для CLI/TUI |
| pre-commit.sh | Скрипт локальных проверок (syntax/flake8/isort/black/mypy) |
| openapi-config.json | Конфиг генерации OpenAPI клиента |
| itsreg_cli/tui/menus.py | TUI меню управления ботами + экспорт ответов |
| itsreg_cli/services/bot_service.py | Интерактивное создание сценария/бота |
| itsreg_cli/main.py | Точка входа CLI, загрузка настроек, проверки, запуск TUI |
| itsreg_cli/json_importer.py | Импорт бота из JSON и создание через API |
| itsreg_cli/domain/models.py | Pydantic-модели Bot/Script/Node/Edge/Message |
| itsreg_cli/config/settings.py | Настройки через pydantic-settings (.env/env/CLI overrides) |
| itsreg_cli/api/client.py | HTTP-клиент к ITS Reg API |
| itsreg_cli/main.py | python -m itsreg_cli entrypoint |
| bot_schema.json | Пример/шаблон JSON схемы бота |
| api/openapi/bots.yaml | OpenAPI спецификация API ботов |
| README.md | Инструкция по установке/запуску/функционалу |
| Makefile | Команды установки/генерации клиента/запуска |
| .pre-commit-config.yaml | Конфиг pre-commit хуков (black/isort/flake8/…) |
| .gitignore | Игнор IDE/OS файлов и сгенерированного OpenAPI клиента |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| install: $(VENV) | ||
| $(PIP) install --upgrade pip | ||
| $(PIP) install -r requirements.txt | ||
| $(VENV)/bin/pre-commit install |
There was a problem hiding this comment.
make install вызывает $(VENV)/bin/pre-commit install, но pre-commit не устанавливается (в requirements.txt его нет). Из-за этого установка падает на чистом окружении. Либо добавьте pre-commit в зависимости (отдельный dev-requirements тоже подойдёт), либо уберите/условно выполняйте этот шаг.
| $(VENV)/bin/pre-commit install | |
| if [ -x "$(VENV)/bin/pre-commit" ]; then "$(VENV)/bin/pre-commit" install; fi |
Makefile
Outdated
| generate-client: $(VENV) | ||
| rm -rf its_reg_api_client itsreg_api_client openapi_client itsreg_api | ||
| $(OPENAPI_CLIENT) generate \ | ||
| --url https://raw.githubusercontent.com/bmstu-itstech/itsreg/main/api/openapi/bots.yaml \ | ||
| --config openapi-config.json \ | ||
| --meta none |
There was a problem hiding this comment.
Таргет generate-client использует $(VENV)/bin/openapi-python-client, но этот пакет нигде не устанавливается, поэтому make run (зависит от generate-client) сломается на чистой машине. Решение: добавить openapi-python-client в зависимости (лучше dev), либо не генерировать клиент при каждом запуске/коммитить сгенерированный клиент.
| import json | ||
|
|
||
| print("\n=== DEBUG: Payload being sent to API ===") | ||
| print(json.dumps(payload, indent=2, ensure_ascii=False)) | ||
| print("=== End DEBUG ===\n") |
There was a problem hiding this comment.
В create_bot включён безусловный debug-print отправляемого payload, который содержит Telegram token бота. Это утечка чувствительных данных в stdout/логи и будет мешать обычному использованию CLI. Сделайте вывод опциональным (флаг/ENV), или уберите целиком.
| import json | |
| print("\n=== DEBUG: Payload being sent to API ===") | |
| print(json.dumps(payload, indent=2, ensure_ascii=False)) | |
| print("=== End DEBUG ===\n") |
| def _build_node_recursive( | ||
| self, nodes_by_state: dict[int, Node], state: int | ||
| ) -> Node: | ||
| if state in nodes_by_state: | ||
| return nodes_by_state[state] | ||
| title = ( | ||
| questionary.text(f"Узел {state}: Название:", default=f"state-{state}").ask() | ||
| or f"state-{state}" | ||
| ) | ||
| messages: List[Message] = [] | ||
| while True: | ||
| message_text = questionary.text(f"Узел {state}: Текст сообщения:").ask() | ||
| if message_text: | ||
| messages.append(Message(text=message_text)) | ||
| if not questionary.confirm( | ||
| f"Узел {state}: Добавить ещё сообщение?", default=False | ||
| ).ask(): | ||
| break | ||
| if not messages: | ||
| messages.append(Message(text="")) | ||
| options: List[str] = [] | ||
| while questionary.confirm( | ||
| f"Узел {state}: Добавить кнопку-опцию?", default=False | ||
| ).ask(): | ||
| option_text = questionary.text(f"Узел {state}: Текст опции:").ask() | ||
| if option_text: | ||
| options.append(option_text) | ||
| edges: List[Edge] = [] | ||
| edge_index = 1 | ||
| while questionary.confirm( | ||
| f"Узел {state}: Добавить исходящее ребро?", default=False | ||
| ).ask(): | ||
| predicate_type = ( | ||
| questionary.select( | ||
| f"Узел {state}, Ребро #{edge_index}: Тип условия:", | ||
| choices=[ | ||
| questionary.Choice(title="Всегда", value="always"), | ||
| questionary.Choice(title="Точный текст", value="exact"), | ||
| questionary.Choice(title="Регекс", value="regex"), | ||
| ], | ||
| default="exact", | ||
| ).ask() | ||
| or "exact" | ||
| ) | ||
| if predicate_type == "always": | ||
| predicate = {"type": "always"} | ||
| elif predicate_type == "regex": | ||
| pattern = ( | ||
| questionary.text( | ||
| f"Узел {state}, Ребро #{edge_index}: Regex-паттерн:", | ||
| default=".*", | ||
| ).ask() | ||
| or ".*" | ||
| ) | ||
| predicate = {"type": "regex", "pattern": pattern} | ||
| else: | ||
| text_value = ( | ||
| questionary.text( | ||
| f"Узел {state}, Ребро #{edge_index}: Ожидаемый текст:", | ||
| default="Далее", | ||
| ).ask() | ||
| or "Далее" | ||
| ) | ||
| predicate = {"type": "exact", "text": text_value} | ||
| to_state_str = questionary.text( | ||
| f"Узел {state}, Ребро #{edge_index}: Целевое состояние:", | ||
| default=str(state + 1), | ||
| ).ask() or str(state + 1) | ||
| try: | ||
| to_state = int(to_state_str) | ||
| except ValueError: | ||
| to_state = state + 1 | ||
| if to_state == 0: | ||
| to_state = 1 | ||
| operation = ( | ||
| questionary.select( | ||
| f"Узел {state}, Ребро #{edge_index}: Операция:", | ||
| choices=["noop", "save", "append"], | ||
| default="noop", | ||
| ).ask() | ||
| or "noop" | ||
| ) | ||
| edges.append(Edge(predicate=predicate, to=to_state, operation=operation)) | ||
| if to_state not in nodes_by_state: | ||
| self._build_node_recursive(nodes_by_state, to_state) | ||
| edge_index += 1 | ||
| node = Node( | ||
| state=state, title=title, messages=messages, edges=edges, options=options | ||
| ) | ||
| nodes_by_state[state] = node | ||
| return node |
There was a problem hiding this comment.
Рекурсивная сборка узлов может уйти в бесконечную рекурсию при цикле (например, ребро на текущий state или A→B→A): текущий state добавляется в nodes_by_state только в конце, поэтому проверка if state in nodes_by_state не спасает. Нужно помечать узел как "в процессе" до рекурсивных вызовов (или отказаться от рекурсивного автосоздания / хранить visited set).
| if not questionary.confirm( | ||
| f"Узел {state}: Добавить ещё сообщение?", default=False | ||
| ).ask(): | ||
| break | ||
| if not messages: | ||
| messages.append(Message(text="")) |
There was a problem hiding this comment.
Если пользователь не добавил ни одного сообщения, создаётся Message(text=""). По OpenAPI (пример ошибки message-empty-text) сервер ожидает непустой текст, из-за чего создание бота может стабильно падать с 400. Лучше требовать хотя бы одно непустое сообщение или повторно спрашивать ввод вместо подстановки пустой строки.
| if not questionary.confirm( | |
| f"Узел {state}: Добавить ещё сообщение?", default=False | |
| ).ask(): | |
| break | |
| if not messages: | |
| messages.append(Message(text="")) | |
| # Предлагаем добавить ещё сообщение только после того, | |
| # как было добавлено хотя бы одно непустое сообщение. | |
| if messages and not questionary.confirm( | |
| f"Узел {state}: Добавить ещё сообщение?", default=False | |
| ).ask(): | |
| break |
| ).ask() | ||
| if action == "Показать сценарий": | ||
| console.print("Сценарий:", style="cyan") | ||
| console.print_json(json.dumps(bot.script.model_dump(), ensure_ascii=False)) |
There was a problem hiding this comment.
В ветке "Показать сценарий" вызывается bot.script.model_dump() без проверки на None, хотя в модели Bot поле script допускает None. Это приведёт к AttributeError при неполных данных от API. Добавьте guard (например, показать сообщение "Сценарий отсутствует") или сделайте script обязательным на уровне модели/валидации.
| console.print_json(json.dumps(bot.script.model_dump(), ensure_ascii=False)) | |
| if bot.script is not None: | |
| console.print_json( | |
| json.dumps(bot.script.model_dump(), ensure_ascii=False) | |
| ) | |
| else: | |
| console.print("Сценарий отсутствует.", style="yellow") |
| try: | ||
| print(f"Загрузка бота из {json_path}...") | ||
| print(f"ID: {bot.id}") | ||
| print(f"Token: {bot.token[:20]}...") |
There was a problem hiding this comment.
print(f"Token: {bot.token[:20]}...") упадёт с TypeError, если в JSON нет поля token (в модели Bot token допускает None). Нужен guard/маскирование (например, печатать <missing> или пустую строку), либо сделать token обязательным при загрузке JSON.
| print(f"Token: {bot.token[:20]}...") | |
| token_preview = "<missing>" if not bot.token else f"{bot.token[:20]}..." | |
| print(f"Token: {token_preview}") |
| ensure_api_available(settings.api_url) | ||
| ensure_token(settings) | ||
| except ValidationError as exc: | ||
| raise SystemExit("ITSREG_JWT_TOKEN не задан") from exc |
There was a problem hiding this comment.
В except ValidationError всегда выбрасывается ITSREG_JWT_TOKEN не задан, даже если ошибка валидации относится к другим полям (например, некорректный URL). Это усложняет диагностику. Лучше вывести exc.errors()/str(exc) или сформировать сообщение в зависимости от конкретного поля.
| raise SystemExit("ITSREG_JWT_TOKEN не задан") from exc | |
| raise SystemExit(f"Ошибка в настройках: {exc}") from exc |
| ```bash | ||
| git clone https://github.com/bmstu-itstech/itsreg-cli | ||
| cd itsreg-cli | ||
| make install | ||
| ``` | ||
|
|
||
| Команда `make install` создаст виртуальное окружение и установит все зависимости. | ||
|
|
There was a problem hiding this comment.
В README предлагается make install/make run, но текущий Makefile требует pre-commit и openapi-python-client в venv, которых нет в requirements.txt. В итоге шаги установки/запуска из документации не воспроизводимы. Обновите README в соответствии с фактическими зависимостями/процессом.
| find . -name "*.py" -not -path "./venv/*" -not -path "./.venv/*" -not -path "*/__pycache__/*" | while read -r file; do | ||
| if ! python -m py_compile "$file" >/dev/null 2>&1; then | ||
| error "Syntax error in $file" | ||
| python -m py_compile "$file" 2>&1 | head -10 | ||
| fi | ||
| done |
There was a problem hiding this comment.
ERRORS не будет выставлен при синтаксических ошибках: цикл while read выполняется в сабшелле из-за пайпа find ... | while ..., поэтому изменения переменной теряются и скрипт может завершиться с кодом 0 даже при ошибках. Перепишите на while ...; do ...; done < <(find ...) или используйте другой способ обхода файлов без пайпа в цикл.
| find . -name "*.py" -not -path "./venv/*" -not -path "./.venv/*" -not -path "*/__pycache__/*" | while read -r file; do | |
| if ! python -m py_compile "$file" >/dev/null 2>&1; then | |
| error "Syntax error in $file" | |
| python -m py_compile "$file" 2>&1 | head -10 | |
| fi | |
| done | |
| while IFS= read -r file; do | |
| if ! python -m py_compile "$file" >/dev/null 2>&1; then | |
| error "Syntax error in $file" | |
| python -m py_compile "$file" 2>&1 | head -10 | |
| fi | |
| done < <(find . -name "*.py" -not -path "./venv/*" -not -path "./.venv/*" -not -path "*/__pycache__/*") |
Лучше поздно, чем никогда)