DARK LIGHT
RU EN

Argenta

Python библиотека для создания модульных CLI приложений

Python TUI CLI Shell v1.0.0

1. Введение

Argenta — это современная Python-библиотека для создания модульных CLI приложений. Библиотека предоставляет простой и элегантный способ создания интерактивных консольных приложений с богатым функционалом, включая автодополнение команд, обработку флагов и аргументов, пользовательские обработчики ошибок и многое другое.

Основная цель Argenta — упростить процесс создания сложных консольных интерфейсов, предоставляя модульную архитектуру и интуитивно понятный API. Библиотека отлично подходит для создания CLI-утилит, административных интерфейсов и других приложений, требующих взаимодействия через терминал.

Ключевые особенности:

2. Установка

pip install argenta

Или с помощью Poetry:

poetry add argenta

3. Быстрый старт

Ниже приведены примеры создания простых CLI-приложений с использованием Argenta.

Пример 1: Простейшее приложение с одной командой

Создадим минимальное приложение с одной командой hello, которая выводит сообщение "Hello, world!".

routers.py:

from argenta.router import Router
from argenta.command import Command
from argenta.response import Response

router = Router()

@router.command(Command("hello"))
def handler(response: Response):
    print("Hello, world!")

main.py:

from argenta.app import App
from argenta.orchestrator import Orchestrator
from routers import router

app: App = App()
orchestrator: Orchestrator = Orchestrator()

def main() -> None:
    app.include_router(router)
    orchestrator.start_polling(app)

if __name__ == '__main__':
    main()

Пример 2: CLI с обработкой флагов

Создадим приложение с командой ssh, которая обрабатывает флаги --host и --port.

import re
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
from argenta.orchestrator import Orchestrator
from argenta.command.flag.defaults import PredefinedFlags
from argenta.command.flag import Flags, Flag, InputFlags

router = Router()

registered_flags = Flags(PredefinedFlags.HOST,
                        Flag('port', '--', re.compile(r'^[0-9]{1,4}$')))

@router.command(Command("hello"))
def handler(response: Response):
    print("Hello, world!")

@router.command(Command(trigger="ssh",
                       description='connect via ssh',
                       flags=registered_flags))
def handler_with_flags(response: Response):
    # Работа с правильными флагами
    for flag in response.valid_flags:
        print(f'Flag name: {flag.get_name()}\n'
              f'Flag value: {flag.get_value()}')

    # Проверка статуса и работа с неопределенными флагами
    if response.status == Status.UNDEFINED_FLAGS or response.status == Status.UNDEFINED_AND_INVALID_FLAGS:
        print("\nНеопределенные флаги:")
        for flag in response.undefined_flags:
            print(f'Undefined flag: {flag.get_name()}')

    # Работа с флагами с неверными значениями
    if response.status == Status.INVALID_VALUE_FLAGS or response.status == Status.UNDEFINED_AND_INVALID_FLAGS:
        print("\nФлаги с неверными значениями:")
        for flag in response.invalid_value_flags:
          print(f'Invalid value flag: {flag.get_name()} = {flag.get_value()}')

После запуска этого приложения вы сможете вводить команды вида:

ssh --host 192.168.1.10 --port 22

И ваш обработчик получит эти флаги и их значения для дальнейшей обработки. Даже при наличии неверных флагов или некорректных значений, управление все равно будет передано обработчику.

4. Архитектура

Библиотека Argenta построена на модульной архитектуре, которая обеспечивает гибкость и расширяемость. Ниже приведена схема взаимодействия основных компонентов:

Основные компоненты:

Порядок создания приложения

  1. Создание экземпляра Orchestrator и настройка аргументов запуска
  2. Создание экземпляра App и настройка его параметров
  3. Создание экземпляров Router и регистрация хэндлеров
  4. Регистрация маршрутизаторов (Router) в приложении
  5. Чтение переданных аргументов при запуске
  6. Запуск цикла обработки ввода через orchestrator.start_polling(app)

5. Документация API

В этом разделе подробно описаны основные компоненты библиотеки, их методы и использование.

5.1 App (Приложение)

App — это основной класс приложения, который управляет жизненным циклом и поведением CLI. Он отвечает за регистрацию маршрутизаторов, обработку системных событий и настройку пользовательского интерфейса.

Конструктор

def __init__(prompt: str = 'What do you want to do?',
             initial_message: str = 'Argenta',
             farewell_message: str = 'See you',
             exit_command: Command = Command('Q', 'Exit command'),
             system_router_title: str | None = 'System points:',
             ignore_command_register: bool = True,
             dividing_line: StaticDividingLine | DynamicDividingLine = StaticDividingLine(),
             repeat_command_groups: bool = True,
             override_system_messages: bool = False,
             autocompleter: AutoCompleter = AutoCompleter(),
             print_func: Callable[[str], None] = Console().print) -> None
Параметры:

Основные методы

set_description_message_pattern
def set_description_message_pattern(_: Callable[[str, str], str]) -> None

Установка шаблона вывода доступных команд.

Параметры:

  • _ - шаблон вывода доступных команд
set_incorrect_input_syntax_handler
def set_incorrect_input_syntax_handler(_: Callable[[str], None]) -> None

Установка обработчика некорректного синтаксиса при вводе команды.

Параметры:

  • _ - обработчик неправильных флагов
set_repeated_input_flags_handler
def set_repeated_input_flags_handler(_: Callable[[str], None]) -> None

Установка обработчика повторяющихся флагов при вводе команды.

Параметры:

  • _ - обработчик повторяющихся флагов
set_unknown_command_handler
def set_unknown_command_handler(_: Callable[[str], None]) -> None

Установка обработчика неизвестных команд.

Параметры:

  • _ - обработчик неизвестных команд
set_empty_command_handler
def set_empty_command_handler(_: Callable[[], None]) -> None

Установка обработчика пустых команд при вводе.

Параметры:

  • _ - обработчик пустых команд
set_exit_command_handler
def set_exit_command_handler(_: Callable[[], None]) -> None

Установка обработчика команды выхода.

Параметры:

  • _ - обработчик команды выхода
include_router
def include_router(router: Router) -> None

Регистрация маршрутизатора в приложении.

Параметры:

  • router - регистрируемый маршрутизатор
include_routers
def include_routers(*routers: Router) -> None

Регистрация нескольких маршрутизаторов в приложении.

Параметры:

  • routers - список регистрируемых маршрутизаторов
add_message_on_startup
def add_message_on_startup(message: str) -> None

Добавление сообщения, которое будет отображаться при запуске приложения.

Параметры:

  • message - добавляемое сообщение

Пример использования:

from argenta.app import App
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
from argenta.app.dividing_line import DynamicDividingLine

# Создаем маршрутизатор с командой
router = Router("Основные команды")
@router.command(Command("hello", "Приветствие"))
def hello(response: Response):
    print("Привет, мир!")

# Создаем приложение с настройками
app = App(
    prompt="Введите команду:",
    initial_message="Добро пожаловать в мое приложение!",
    farewell_message="До свидания!",
    dividing_line=DynamicDividingLine("="),
    ignore_command_register=True
)

# Регистрируем маршрутизатор
app.include_router(router)

# Настраиваем обработчик неизвестных команд
app.set_unknown_command_handler(lambda cmd: print(f"Неизвестная команда: {cmd}"))

5.2 Router (Маршрутизатор)

Router — это класс, который непосредственно настраивает и управляет обработчиками команд. Он регистрирует команды и связывает их с функциями-обработчиками.

Конструктор

def __init__(title: str = None)

Параметры:

Основные методы

@command
def command(command: Command) -> Callable

Декоратор для регистрации обработчика команды.

Параметры:

  • command - регистрируемая команда

Возвращает:

  • Декорированный обработчик
get_triggers
def get_triggers() -> list[str]

Получение зарегистрированных триггеров.

Возвращает:

  • Список зарегистрированных триггеров
get_aliases
def get_aliases() -> list[str]

Получение зарегистрированных псевдонимов.

Возвращает:

  • Список зарегистрированных псевдонимов

Пример использования:

from argenta.router import Router
from argenta.command import Command
from argenta.response import Response

# Создаем маршрутизатор с заголовком
router = Router("Управление файлами")

# Регистрируем команду без флагов
@router.command(Command("list", "Вывести список файлов"))
def list_files(response: Response):
    import os
    files = os.listdir()
    for file in files:
        print(file)

# Регистрируем команду с псевдонимами
@router.command(Command("info", "Информация о файле", aliases=["stat", "details"]))
def file_info(response: Response):
    print("Эта команда показывает информацию о файле")

5.3 Command (Команда)

Command — это класс, представляющий команду, которая может и должна быть зарегистрирована в Router. Определяет триггер, описание, флаги и псевдонимы команды.

Конструктор

def __init__(trigger: str,
             description: str = None,
             flags: Flag | Flags = None,
             aliases: list[str] = None)

Параметры:

Пример использования:

from argenta.command import Command
from argenta.command.flag import Flag, Flags
import re

# Простая команда без флагов
simple_command = Command("hello", "Приветственная команда")

# Команда с одним флагом
flag_command = Command(
    trigger="search",
    description="Поиск по ключевому слову",
    flags=Flag("keyword", "--")
)

# Команда с несколькими флагами и псевдонимами
complex_command = Command(
    trigger="connect",
    description="Подключение к серверу",
    flags=Flags(
        Flag("host", "--"),
        Flag("port", "--", re.compile(r"^\d{1,5}$")),
        Flag("user", "--")
    ),
    aliases=["conn", "c"]
)

5.4 Flags (Флаги)

В Argenta имеется развитая система для определения и обработки флагов командной строки. Эта система включает следующие основные классы:

Flag

Класс Flag представляет собой сущность флага, регистрируемого для последующей обработки.

def __init__(name: str,
             prefix: Literal['-', '--', '---'] = '--',
             possible_values: list[str] | Pattern[str] | False = True) -> None

Параметры:

Flags

Класс Flags объединяет зарегистрированные флаги.

def __init__(*flags: Flag)

Параметры:

get_flags
def get_flags() -> list[Flag]

Возвращает список флагов.

Возвращает:

  • Список флагов
add_flag
def add_flag(flag: Flag) -> None

Добавляет флаг в список флагов.

Параметры:

  • flag - добавляемый флаг
add_flags
def add_flags(flags: list[Flag]) -> None

Добавляет список флагов в список флагов.

Параметры:

  • flags - список добавляемых флагов
get_flag
def get_flag(name: str) -> Flag | None

Возвращает сущность флага по его имени или None, если не найден.

Параметры:

  • name - имя флага для получения

Возвращает:

  • Сущность флага или None

InputFlag

Класс InputFlag представляет сущность флага введенной команды.

def __init__(name: str,
           prefix: Literal['-', '--', '---'] = '--',
           value: str = None)

Параметры:

get_value
def get_value() -> str | None

Возвращает значение флага.

Возвращает:

  • Значение флага как строка

PredefinedFlags

Класс PredefinedFlags — это датакласс с предопределенными флагами и наиболее часто используемыми флагами для быстрого использования.

Пример использования:

import re
from argenta.command.flag import Flag, Flags, InputFlags
from argenta.command.flag.defaults import PredefinedFlags

# Создание флага без значения (булев флаг)
verbose_flag = Flag('verbose', '--', False)

# Создание флага со значением из списка
log_level_flag = Flag('log-level', '--', ['debug', 'info', 'warning', 'error'])

# Создание флага со значением, соответствующим регулярному выражению
port_flag = Flag('port', '--', re.compile(r'^\d{1,5}$'))

# Использование предопределенных флагов
host_flag = PredefinedFlags.HOST

# Создание группы флагов
connection_flags = Flags(
    host_flag,
    port_flag,
    Flag('user', '--'),
    Flag('password', '--')
)

# Обработка входных флагов в обработчике команды
@router.command(Command("connect", "Подключение к серверу", flags=connection_flags))
def connect_handler(flags: InputFlags):
    host = flags.get_flag('host').get_value()
    port = flags.get_flag('port').get_value() if flags.get_flag('port') else '22'
    user = flags.get_flag('user').get_value() if flags.get_flag('user') else 'root'

    print(f"Подключение к {host}:{port} как {user}")

5.5 Orchestrator (Оркестратор)

Orchestrator — это класс, который определяет поведение интегрированной системы, на уровень выше, чем App.

Конструктор

def __init__(arg_parser: ArgParse = False)

Параметры:

Основные методы

start_polling

@staticmethod
def start_polling(app: App) -> None
                        

Запуск цикла обработки пользовательского ввода.

Параметры:

  • app - запускаемое приложение
get_input_args
def get_input_args() -> Namespace | None

Возвращает введённые при запуске аргументы.

Возвращает:

  • Введённые при запуске аргументы или None

Пример использования:

from argenta.app import App
from argenta.orchestrator import Orchestrator
from argenta.orchestrator.argparser import ArgParse
from argenta.orchestrator.argparser.arguments import OptionalArgument, BooleanArgument

# Создание парсера аргументов командной строки
arg_parser = ArgParse(
    [
        OptionalArgument('config', prefix='--'),
        BooleanArgument('verbose', prefix='--')
    ],
    name="Мое приложение",
    description="Описание моего приложения",
    epilog="© 2025 MyApp"
)

# Создание оркестратора с парсером аргументов
orchestrator = Orchestrator(arg_parser)

# Создание и настройка приложения
app = App(initial_message="Добро пожаловать!")

# Получение аргументов командной строки
args = orchestrator.get_input_args()
if args and args.verbose:
    print("Включен подробный режим логирования")

# Запуск цикла обработки ввода
orchestrator.start_polling(app)

5.6 ArgParse (Парсер аргументов)

ArgParse — это класс для конфигурации аргументов командной строки при запуске.

Конструктор

def __init__(processed_args: list[PositionalArgument | OptionalArgument | BooleanArgument],
             name: str = 'Argenta',
             description: str = 'Argenta available arguments',
             epilog: str = 'github.com/koloideal/Argenta | made by kolo') -> None

Параметры:

Основные методы

set_args
def set_args(*args: PositionalArgument | OptionalArgument | BooleanArgument) -> None

Устанавливает аргументы для обработки.

Параметры:

  • args - обрабатываемые аргументы

Типы аргументов

PositionalArgument

Обязательный аргумент при запуске.

def __init__(name: str)

Параметры:

  • name - имя аргумента, не должно начинаться с минуса (-)
OptionalArgument

Необязательный аргумент, должен иметь значение.

def __init__(name: str, prefix: Literal['-', '--', '---'] = '--')

Параметры:

  • name - имя аргумента
  • prefix - префикс аргумента
BooleanArgument

Булевый аргумент, не требует значения.

def __init__(name: str, prefix: Literal['-', '--', '---'] = '--')

Параметры:

  • name - имя аргумента
  • prefix - префикс аргумента

Пример использования:

from argenta.orchestrator.argparser import ArgParse
from argenta.orchestrator.argparse.arguments import PositionalArgument, OptionalArgument, BooleanArgument

# Создание парсера аргументов с разными типами аргументов
parser = ArgParse(
    [
        PositionalArgument('command'),
        OptionalArgument('config', '--'),
        OptionalArgument('output', '-'),
        BooleanArgument('verbose', '--'),
        BooleanArgument('help', '-')
    ],
    name="Мое приложение",
    description="Пример использования ArgParse",
    epilog="Дополнительная информация"
)

5.7 DividingLine (Разделительная линия)

Библиотека предоставляет два типа разделительных линий: StaticDividingLine и DynamicDividingLine.

StaticDividingLine

Статическая разделительная линия с фиксированной длиной.

def __init__(unit_part: str = '-', length: int = 25) -> None

Параметры:

DynamicDividingLine

Динамическая разделительная линия, длина которой равна длине самой длинной выводимой строки.

def __init__(unit_part: str = '-') -> None

Параметры:

Пример использования:

from argenta.app import App
from argenta.app.dividing_line import StaticDividingLine, DynamicDividingLine

# Использование статической разделительной линии
app_with_static_line = App(
    dividing_line=StaticDividingLine(unit_part='=', length=40)
)

# Использование динамической разделительной линии
app_with_dynamic_line = App(
    dividing_line=DynamicDividingLine(unit_part='*')
)

5.9 Response (Ответ)

Response — это объект, который передается в обработчик команды и содержит информацию о статусе обработки флагов и сами флаги, разделенные на категории в зависимости от результатов валидации.

Важно: При вводе команды, если команда найдена и имеет корректный синтаксис, управление передаётся обработчику (handler) ДАЖЕ в случае неправильного значения флагов или использования незарегистрированных флагов. Обработчик всегда получает объект Response, который содержит информацию обо всех введенных флагах и их статусе.

Конструктор

def __init__(self,
             status: Status = None,
             valid_flags: ValidInputFlags = ValidInputFlags(),
             undefined_flags: UndefinedInputFlags = UndefinedInputFlags(),
             invalid_value_flags: InvalidValueInputFlags = InvalidValueInputFlags()):
Параметры:

Атрибуты класса Response:

Пример использования:

@router.command(Command(
    trigger="connect",
    description="Подключение к серверу",
    flags=Flags(
        Flag("host", "--"),
        Flag("port", "--", re.compile(r"^\d{1,5}$")),
        Flag("user", "--")
    )
))
def connect_handler(response: Response):
    # Работа с корректными флагами
    print("Корректные флаги:")
    for flag in response.valid_flags:
        print(f"{flag.get_name()}: {flag.get_value()}")

    # Проверка наличия незарегистрированных флагов
    if response.undefined_flags:
        print("\nОбнаружены незарегистрированные флаги:")
        for flag in response.undefined_flags:
            print(f"{flag.get_name()}: {flag.get_value()}")

    # Проверка наличия флагов с некорректными значениями
    if response.invalid_value_flags:
        print("\nОбнаружены флаги с некорректными значениями:")
        for flag in response.invalid_value_flags:
            print(f"{flag.get_name()}: {flag.get_value()}")

    # Использование статуса для принятия решений
    if response.status == Status.ALL_FLAGS_VALID:
        print("\nВсе флаги корректны, выполняем операцию...")
    else:
        print("\nПредупреждение: обнаружены проблемы с флагами.")

5.8 AutoCompleter (Автодополнение)

AutoCompleter — это класс, который настраивает и реализует автодополнение вводимых команд.

Важное примечание: На платформе Linux история автодополнителя работает только в рамках текущего сеанса приложения и не сохраняется на уровне системы.

Конструктор

def __init__(history_filename: str = False, autocomplete_button: str = 'tab') -> None
Параметры:

Пример использования:

from argenta.app import App
from argenta.app.autocompleter import AutoCompleter

# Создание автодополнителя с историей
completer = AutoCompleter(
    history_filename=".myapp_history",
    autocomplete_button="tab"
)

# Использование автодополнителя при создании приложения
app = App(
    prompt="Введите команду:",
    autocompleter=completer
)

6. Расширенные примеры

6.1 Маршрутизатор с обработкой флагов

Пример реализации маршрутизатора с командами, которые обрабатывают различные типы флагов и используют объект Response.

import re
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
from argenta.response.status import Status
from argenta.command.flag import Flag, Flags
from argenta.command.flag.defaults import PredefinedFlags

# Создание маршрутизатора
file_router = Router("Операции с файлами")

# Определение флагов для команды копирования
copy_flags = Flags(
    Flag('source', '--'),
    Flag('destination', '--'),
    Flag('recursive', '--', False),  # Булевый флаг без значения
    Flag('force', '-', False)        # Короткий булевый флаг
)

# Регистрация команды копирования
@file_router.command(Command(
    trigger="copy",
    description="Копирование файлов",
    flags=copy_flags,
    aliases=["cp"]
))
def copy_files(response: Response):
    # Получаем значения корректных флагов
    source = None
    destination = None
    recursive = False
    force = False

    for flag in response.valid_flags:
        if flag.get_name() == "source":
            source = flag.get_value()
        elif flag.get_name() == "destination":
            destination = flag.get_value()
        elif flag.get_name() == "recursive":
            recursive = True
        elif flag.get_name() == "force":
            force = True

    # Проверка обязательных параметров
    if not source or not destination:
        print("Ошибка: необходимо указать источник и назначение")
        return

    print(f"Копирование из {source} в {destination}")
    if recursive:
        print("Рекурсивное копирование включено")
    if force:
        print("Принудительное копирование включено")

    # Обработка неопределенных флагов
    if response.undefined_flags:
        print("\nПредупреждение: обнаружены незарегистрированные флаги:")
        for flag in response.undefined_flags:
            print(f"  - {flag.get_name()}" +
                 (f" = {flag.get_value()}" if flag.get_value() else ""))

    # Обработка флагов с некорректными значениями
    if response.invalid_value_flags:
        print("\nПредупреждение: обнаружены флаги с некорректными значениями:")
        for flag in response.invalid_value_flags:
            print(f"  - {flag.get_name()} = {flag.get_value()}")

    # Принятие решения на основе статуса
    if response.status != Status.ALL_FLAGS_VALID:
        print("\nВыполнение с предупреждениями из-за проблем с флагами.")

6.2 Множественные маршрутизаторы

Пример приложения с несколькими маршрутизаторами для различных групп функций.

from argenta.app import App
from argenta.router import Router
from argenta.command import Command
from argenta.orchestrator import Orchestrator
from argenta.app.dividing_line import DynamicDividingLine
from argenta.response import Response
import platform
import psutil
import os
import subprocess
import socket

# Маршрутизатор для работы с файлами
file_router = Router("Файловые операции")

@file_router.command(Command("list", "Список файлов"))
def list_files(response: Response):
    files = os.listdir()
    for file in files:
        print(file)

@file_router.command(Command("size", "Размер файла"))
def file_size(response: Response):
    file_name = input("Введите имя файла: ")
    if os.path.exists(file_name):
        size = os.path.getsize(file_name)
        print(f"Размер файла {file_name}: {size} байт")
    else:
        print(f"Файл {file_name} не найден")

# Маршрутизатор для системных операций
system_router = Router("Системные операции")

@system_router.command(Command("info", "Информация о системе"))
def system_info(response: Response):
    print(f"Система: {platform.system()}")
    print(f"Версия: {platform.version()}")
    print(f"Архитектура: {platform.architecture()}")
    print(f"Процессор: {platform.processor()}")

@system_router.command(Command("memory", "Информация о памяти"))
def memory_info(response: Response):
    memory = psutil.virtual_memory()
    print(f"Всего памяти: {memory.total / (1024**3):.2f} ГБ")
    print(f"Доступно: {memory.available / (1024**3):.2f} ГБ")
    print(f"Использовано: {memory.used / (1024**3):.2f} ГБ ({memory.percent}%)")

# Маршрутизатор для сетевых операций
network_router = Router("Сетевые операции")

@network_router.command(Command("ping", "Проверка доступности хоста"))
def ping_host(response: Response):
    host = input("Введите имя хоста: ")
    print(f"Пингую {host}...")
    subprocess.run(["ping", "-c", "4", host])

@network_router.command(Command("ip", "Показать IP-адреса"))
def show_ip(response: Response):
    hostname = socket.gethostname()
    print(f"Имя хоста: {hostname}")
    print(f"IP-адрес: {socket.gethostbyname(hostname)}")

# Создание приложения и регистрация маршрутизаторов
app = App(
    prompt="System> ",
    initial_message="Системный монитор v1.0",
    dividing_line=DynamicDividingLine("*")
)

# Добавляем все маршрутизаторы
app.include_routers(file_router, system_router, network_router)

# Добавляем сообщение при запуске
app.add_message_on_startup("Для просмотра доступных команд нажмите Enter")

# Запускаем приложение
orchestrator = Orchestrator()
orchestrator.start_polling(app)

6.3 Пользовательские обработчики ошибок

Пример приложения с пользовательскими обработчиками ошибок для улучшения отзывчивости пользовательского интерфейса.

from argenta.app import App
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
from argenta.orchestrator import Orchestrator
from rich.console import Console
from rich.panel import Panel
from rich.text import Text

# Создаем консоль Rich для более красивого вывода
console = Console()

# Создаем маршрутизатор
router = Router("Основные команды")

@router.command(Command("hello", "Приветствие"))
def hello(response: Response):
    console.print(Panel("Привет, мир!", style="green"))

@router.command(Command("bye", "Прощание"))
def bye(response: Response):
    console.print(Panel("До свидания!", style="blue"))

# Создаем приложение
app = App(
    prompt="MyApp> ",
    initial_message="Тестовое приложение v1.0",
    print_func=console.print
)

# Регистрируем маршрутизатор
app.include_router(router)

# Пользовательский обработчик неизвестных команд
def unknown_command_handler(cmd: InputCommand):
    text = Text()
    text.append("Неизвестная команда: ", style="bold red")
    text.append(cmd.get_trigger(), style="italic")
    text.append("\nДоступные команды: hello, bye, Q (выход)")
    console.print(Panel(text, title="Ошибка", border_style="red"))

# Пользовательский обработчик пустых команд
def empty_command_handler(response: Response):
    console.print(Panel("Для выхода введите Q", style="yellow"))

# Пользовательский обработчик неверных флагов
def incorrect_input_syntax_handler(raw_command: str):
    console.print(Panel(f"Некорректный синтаксис: {raw_command}", title="Ошибка", style="red"))

# Пользовательский обработчик повторяющихся флагов
def repeated_flag_handler(raw_command: str):
    console.print(Panel(f"Флаг указан повторно: {raw_command}", title="Предупреждение", style="yellow"))

# Пользовательский обработчик выхода
def exit_handler():
    console.print(Panel("Выход из приложения...", style="blue", title="До свидания"))

# Устанавливаем обработчики в приложение
app.set_unknown_command_handler(unknown_command_handler)
app.set_empty_command_handler(empty_command_handler)
app.set_incorrect_input_syntax_handler(incorrect_input_syntax_handler)
app.set_repeated_input_flags_handler(repeated_flag_handler)
app.set_exit_command_handler(exit_handler)

# Запускаем приложение
orchestrator = Orchestrator()
orchestrator.start_polling(app)

7. Исключения

Библиотека Argenta предоставляет набор исключений для обработки различных ситуаций ошибок. Ниже приведен список основных исключений и их назначение:

Исключение Описание
NoRegisteredHandlersException У маршрутизатора нет зарегистрированных обработчиков
TooManyTransferredArgsException Передано слишком много аргументов
RequiredArgumentNotPassedException Не передан обязательный аргумент
IncorrectNumberOfHandlerArgsException Передано неверное количество аргументов обработчику
TriggerContainSpacesException В регистрируемом триггере содержатся пробелы

8. Лучшие практики

При разработке приложений с использованием библиотеки Argenta рекомендуется следовать следующим практикам:

Обработка Response объекта

@router.command(Command("example", flags=my_flags))
def handler(response: Response):
    # 1. Сначала проверяем наличие критичных проблем
    if response.status != Status.ALL_FLAGS_VALID:
        # Выводим предупреждения
        if response.undefined_flags:
            print("Предупреждение: неизвестные флаги")
        if response.invalid_value_flags:
            print("Предупреждение: некорректные значения флагов")

    # 2. Продолжаем выполнение с корректными флагами
    for flag in response.valid_flags:
        print(f'Корректный флаг: {flag.get_string_entity()}')

Организация маршрутизаторов

Использование автодополнения

Важно: Учитывайте, что на Linux системах история автодополнителя работает только в рамках текущего сеанса приложения. Если вам требуется полноценная история между запусками, вам может потребоваться дополнительная настройка или использование внешних инструментов.

9. Тестирование

Библиотека Argenta включает набор тестов для проверки функциональности. Вы можете запустить тесты следующим образом:

python -m unittest discover

Для более подробного вывода результатов тестирования используйте флаг -v:

python -m unittest discover -v

При разработке собственных приложений на основе Argenta рекомендуется создавать модульные тесты для проверки функциональности ваших обработчиков команд и общей логики приложения.

Argenta

Python library for creating modular CLI applications

Python TUI CLI Shell v1.0.0

1. Introduction

Argenta — is a modern Python library for creating modular CLI applications. The library provides a simple and elegant way to create interactive console applications with rich functionality, including command completion, flag and argument handling, custom error handlers, and much more.

The main goal of Argenta is to simplify the process of creating complex console interfaces by providing a modular architecture and an intuitive API. The library is ideal for creating CLI utilities, administrative interfaces, and other applications that require interaction through the terminal.

Key features:

2. Installing

pip install argenta

Or with Poetry:

poetry add argenta

3. Quick start

Below are examples of creating simple CLI applications using Argenta.

Example 1: The simplest application with one command

Let's create a minimal application with one command hello, which prints the message "Hello, world!".

routers.py:

from argenta.router import Router
from argenta.command import Command
from argenta.response import Response

router = Router()

@router.command(Command("hello"))
def handler(response: Response):
    print("Hello, world!")

main.py:

from argenta.app import App
from argenta.orchestrator import Orchestrator
from routers import router

app: App = App()
orchestrator: Orchestrator = Orchestrator()

def main() -> None:
    app.include_router(router)
    orchestrator.start_polling(app)

if __name__ == '__main__':
    main()

Example 2: CLI with flag handling

Let's create an application with the ssh command that handles flags --host and --port.

import re
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
from argenta.orchestrator import Orchestrator
from argenta.command.flag.defaults import PredefinedFlags
from argenta.command.flag import Flags, Flag, InputFlags

router = Router()

registered_flags = Flags(PredefinedFlags.HOST,
                        Flag('port', '--', re.compile(r'^[0-9]{1,4}$')))

@router.command(Command("hello"))
def handler(response: Response):
    print("Hello, world!")

@router.command(Command(trigger="ssh",
                       description='connect via ssh',
                       flags=registered_flags))
def handler_with_flags(response: Response):
    # Working with the valid flags
    for flag in response.valid_flags:
        print(f'Flag name: {flag.get_name()}\n'
              f'Flag value: {flag.get_value()}')

    # Checking status and dealing with undefined Flags
    if response.status == Status.UNDEFINED_FLAGS or response.status == Status.UNDEFINED_AND_INVALID_FLAGS:
        print("\nUndefined flags:")
        for flag in response.undefined_flags:
            print(f'Undefined flag: {flag.get_name()}')

    # Working with flags with invalid values
    if response.status == Status.INVALID_VALUE_FLAGS or response.status == Status.UNDEFINED_AND_INVALID_FLAGS:
        print("\nFlags with invalid value:")
        for flag in response.invalid_value_flags:
          print(f'Invalid value flag: {flag.get_name()} = {flag.get_value()}')

Once you launch this application, you will be able to enter commands like:

ssh --host 192.168.1.10 --port 22

And your handler will receive these flags and their values for further processing. Even if there are incorrect flags or incorrect values, control will still be transferred to the handler.

4. Architecture

The Argenta library is built on a modular architecture that provides flexibility and extensibility. Below is a diagram of how the main components interact:

Main components:

The order of creating an application

  1. Creating an instance of Orchestrator and setting up startup arguments
  2. Creating an instance of App and setting up its parameters
  3. Creating instances of Router and registering handlers
  4. Registering routers (Router) in the application
  5. Reading passed arguments at startup
  6. Starting the input processing loop via orchestrator.start_polling(app)

5. API Documentation

This section describes in detail the main components of the library, their methods and usage.

5.1 App

App is the main application class that manages the lifecycle and behavior of the CLI. It is responsible for registering routers, handling system events, and configuring the user interface.

Constructor

def __init__(prompt: str = 'What do you want to do?',
             initial_message: str = 'Argenta',
             farewell_message: str = 'See you',
             exit_command: Command = Command('Q', 'Exit command'),
             system_router_title: str | None = 'System points:',
             ignore_command_register: bool = True,
             dividing_line: StaticDividingLine | DynamicDividingLine = StaticDividingLine(),
             repeat_command_groups: bool = True,
             override_system_messages: bool = False,
             autocompleter: AutoCompleter = AutoCompleter(),
             print_func: Callable[[str], None] = Console().print) -> None
Options:

Basic methods

set_description_message_pattern
def set_description_message_pattern(_: Callable[[str, str], str]) -> None

Sets the output template for available commands.

Options:

  • _ - template for output of available commands
set_incorrect_input_syntax_handler
def set_incorrect_input_syntax_handler(_: Callable[[str], None]) -> None

Installing a handler for incorrect syntax when entering a command.

Options:

  • _ - invalid flag handler
set_repeated_input_flags_handler
def set_repeated_input_flags_handler(_: Callable[[str], None]) -> None

Set the handler for repeating flags when entering a command.

Options:

  • _ - repeating flag handler
set_unknown_command_handler
def set_unknown_command_handler(_: Callable[[str], None]) -> None

Installing a handler for unknown commands.

Options:

  • _ - unknown command handler
set_empty_command_handler
def set_empty_command_handler(_: Callable[[], None]) -> None

Set up a handler for empty commands on input.

Options:

  • _ - empty command handler
set_exit_command_handler
def set_exit_command_handler(_: Callable[[], None]) -> None

Installing an exit command handler.

Options:

  • _ - exit command handler
include_router
def include_router(router: Router) -> None

Registering the router in the application.

Options:

  • router - registered router
include_routers
def include_routers(*routers: Router) -> None

Register multiple routers in the application.

Options:

  • routers - list of registered routers
add_message_on_startup
def add_message_on_startup(message: str) -> None

Add a message to be displayed when the application is launched.

Options:

  • message - message to be added

Usage example:

from argenta.app import App
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
from argenta.app.dividing_line import DynamicDividingLine

# Create a router with the command
router = Router("Base commands")
@router.command(Command("hello", "Greetings"))
def hello(response: Response):
    print("Hello, world!")

# Create an application with settings
app = App(
    prompt="Enter the command:",
    initial_message="Welcome to my app!",
    farewell_message="GoodBye!",
    dividing_line=DynamicDividingLine("="),
    ignore_command_register=True
)

# Registering a router
app.include_router(router)

# Setting up a handler for unknown commands
app.set_unknown_command_handler(lambda cmd: print(f"Unknown command: {cmd}"))

5.2 Router

Router — this is the class that actually configures and manages command handlers. It registers commands and associates them with handler functions.

Constructor

def __init__(title: str = None)

Options:

Base methods

@command
def command(command: Command) -> Callable

Decorator for registering a command handler.

Options:

  • command - registered command

Returns:

  • Decorated handler
get_triggers
def get_triggers() -> list[str]

Getting registered triggers.

Returns:

  • List of registered triggers
get_aliases
def get_aliases() -> list[str]

Getting registered aliases.

Returns:

  • List of registered aliases

Usage example:

from argenta.router import Router
from argenta.command import Command
from argenta.response import Response

# Create a router with a header
router = Router("File management")

# Register a team without flags
@router.command(Command("list", "List files"))
def list_files(response: Response):
    import os
    files = os.listdir()
    for file in files:
        print(file)

# Registering a command with aliases
@router.command(Command("info", "File information", aliases=["stat", "details"]))
def file_info(response: Response):
    print("This command shows information about a file.")

5.3 Command

Command — is a class representing a command that can and should be registered with Router. Defines the trigger, description, flags, and aliases of the command.

Constructor

def __init__(trigger: str,
             description: str = None,
             flags: Flag | Flags = None,
             aliases: list[str] = None)

Options:

Usage example:

from argenta.command import Command
from argenta.command.flag import Flag, Flags
import re

# Simple command without flags
simple_command = Command("hello", "Welcome command")

# Command with single flag
flag_command = Command(
    trigger="search",
    description="Search by keyword",
    flags=Flag("keyword", "--")
)

# Command with multiple flags and aliases
complex_command = Command(
    trigger="connect",
    description="Connecting to the server",
    flags=Flags(
        Flag("host", "--"),
        Flag("port", "--", re.compile(r"^\d{1,5}$")),
        Flag("user", "--")
    ),
    aliases=["conn", "c"]
)

5.4 Flags

Argenta has a sophisticated system for defining and handling command line flags. This system includes the following main classes:

Flag

Class Flag represents the essence of the flag registered for subsequent processing.

def __init__(name: str,
             prefix: Literal['-', '--', '---'] = '--',
             possible_values: list[str] | Pattern[str] | False = True) -> None

Options:

Flags

The class flags combines registered flags.

def __init__(*flags: Flag)

Параметры:

get_flags
def get_flags() -> list[Flag]

returns the list of flags

Возвращает:

  • list of flags
add_flag
def add_flag(flag: Flag) -> None

adds the flag to the flag list

Параметры:

  • flag - added flag
add_flags
def add_flags(flags: list[Flag]) -> None

adds a list of flags to the flag list

Options:

  • flags - list_of_added_flags
get_flag
def get_flag(name: str) -> Flag | None

Returns the essence of the flag by his name or None, if not found.

Options:

  • name - flag name for receiving

Returns:

  • The essence of the flag or none

InputFlag

The class inputflag represents the essence of the flag of the entered team.

def __init__(name: str,
           prefix: Literal['-', '--', '---'] = '--',
           value: str = None)

Parameters:

get_value
def get_value() -> str | None

Returns the value of the flag.

Returns:

  • The value of the flag as a string

PredefinedFlags

Class PredefinedFlags — This is dadaxlass with predetermined flags and the most commonly used Flags for fast use.

An example of use:

import re
from argenta.command.flag import Flag, Flags, InputFlags
from argenta.command.flag.defaults import PredefinedFlags

# Creating a flag without value (Boolean flag)
verbose_flag = Flag('verbose', '--', False)

# Creating a flag with a meaning from the list
log_level_flag = Flag('log-level', '--', ['debug', 'info', 'warning', 'error'])

# Creation of a flag with a meaning corresponding to the regular expression
port_flag = Flag('port', '--', re.compile(r'^\d{1,5}$'))

# Using predefined flags
host_flag = PredefinedFlags.HOST

# Creation of a group of flags
connection_flags = Flags(
    host_flag,
    port_flag,
    Flag('user', '--'),
    Flag('password', '--')
)

# Processing of the input flags in the command processor
@router.command(Command("connect", "Connection to the server", flags=connection_flags))
def connect_handler(flags: InputFlags):
    host = flags.get_flag('host').get_value()
    port = flags.get_flag('port').get_value() if flags.get_flag('port') else '22'
    user = flags.get_flag('user').get_value() if flags.get_flag('user') else 'root'

    print(f"Connection to {host}:{port} as {user}")

5.5 Orchestrator

Orchestrator — this is a class that determines the behavior of an integrated system, to a level higher than App.

Constructor

def __init__(arg_parser: ArgParse = False)

Parameters:

Basic methods

start_polling

@staticmethod
def start_polling(app: App) -> None
                        

Launching the user input processing cycle.

Parameters:

  • app - launched application
get_input_args
def get_input_args() -> Namespace | None

Returns the arguments introduced at launch.

Returns:

  • Arguments introduced at launch or None

Example of use:

from argenta.app import App
from argenta.orchestrator import Orchestrator
from argenta.orchestrator.argparser import ArgParse
from argenta.orchestrator.argparser.arguments import OptionalArgument, BooleanArgument

# Creation parser of the arguments of the command line
arg_parser = ArgParse(
    [
        OptionalArgument('config', prefix='--'),
        BooleanArgument('verbose', prefix='--')
    ],
    name="My application",
    description="Description of my application",
    epilog="© 2025 MyApp"
)

# Creation of an orchestra with a parser of arguments
orchestrator = Orchestrator(arg_parser)

# Creating and configuration of the application
app = App(initial_message="You are welcome!")

# Getting command line arguments
args = orchestrator.get_input_args()
if args and args.verbose:
    print("Detailed logistics mode is included")

# Launching the input processing cycle
orchestrator.start_polling(app)

5.6 ArgParse

ArgParse — This is a class for configuration of command line arguments at launch.

Constructor

def __init__(processed_args: list[PositionalArgument | OptionalArgument | BooleanArgument],
             name: str = 'Argenta',
             description: str = 'Argenta available arguments',
             epilog: str = 'github.com/koloideal/Argenta | made by kolo') -> None

Parameters:

Basic methods

set_args
def set_args(*args: PositionalArgument | OptionalArgument | BooleanArgument) -> None

Installs arguments for processing.

Parameters:

  • args - Processed arguments

Types of arguments

PositionalArgument

Mandatory argument at launch.

def __init__(name: str)

Parameters:

  • name - The name of the argument should not begin with the minus (-)
OptionalArgument

An optional argument should be important.

def __init__(name: str, prefix: Literal['-', '--', '---'] = '--')

Parameters:

  • name - the name of the argument
  • prefix - prefix argument
BooleanArgument

Boolean argument does not require value.

def __init__(name: str, prefix: Literal['-', '--', '---'] = '--')

Parameteres:

  • name - the name of the argument
  • prefix - prefix argument

Example of use:

from argenta.orchestrator.argparser import ArgParse
from argenta.orchestrator.argparse.arguments import PositionalArgument, OptionalArgument, BooleanArgument

# Creation of a parser of arguments with different types of arguments
parser = ArgParse(
    [
        PositionalArgument('command'),
        OptionalArgument('config', '--'),
        OptionalArgument('output', '-'),
        BooleanArgument('verbose', '--'),
        BooleanArgument('help', '-')
    ],
    name="My application",
    description="An example of using ArgParse",
    epilog="Additional information"
)

5.7 DividingLine

The library provides two types of dividing lines: StaticDividingLine and DynamicDividingLine.

StaticDividingLine

Static dividing line with a fixed length.

def __init__(unit_part: str = '-', length: int = 25) -> None

Parameters:

DynamicDividingLine

The dynamic dividing line, the length of which is equal to the length of the longest lines.

def __init__(unit_part: str = '-') -> None

Parameters:

An example of use:

from argenta.app import App
from argenta.app.dividing_line import StaticDividingLine, DynamicDividingLine

# Using a static dividing line
app_with_static_line = App(
    dividing_line=StaticDividingLine(unit_part='=', length=40)
)

# Using a dynamic dividing line
app_with_dynamic_line = App(
    dividing_line=DynamicDividingLine(unit_part='*')
)

5.9 Response

Response — This is an object that is transferred to the command handler and contains information about The status of flag processing and the flags themselves, divided into categories, depending on the results of validation.

Important: When entering the command, if the team is found and has the correct syntax, The control is transferred to the handler even in case of improper flag value or use unregistered flags. The processor always receives the Response object, which contains information about all introduced flags and their status.

Constructor

def __init__(self,
             status: Status = None,
             valid_flags: ValidInputFlags = ValidInputFlags(),
             undefined_flags: UndefinedInputFlags = UndefinedInputFlags(),
             invalid_value_flags: InvalidValueInputFlags = InvalidValueInputFlags()):
Parameters:

Response class attributes:

An example of use:

@router.command(Command(
    trigger="connect",
    description="Connection to the server",
    flags=Flags(
        Flag("host", "--"),
        Flag("port", "--", re.compile(r"^\d{1,5}$")),
        Flag("user", "--")
    )
))
def connect_handler(response: Response):
    # Working with correct flags
    print("Correct flags:")
    for flag in response.valid_flags:
        print(f"{flag.get_name()}: {flag.get_value()}")

    # Checking the availability of unregistered flags
    if response.undefined_flags:
        print("\nUnregistered flags were discovered:")
        for flag in response.undefined_flags:
            print(f"{flag.get_name()}: {flag.get_value()}")

    # Checking the presence of flags with incorrect values
    if response.invalid_value_flags:
        print("\nFlags with incorrect values were found:")
        for flag in response.invalid_value_flags:
            print(f"{flag.get_name()}: {flag.get_value()}")

    # Using status for decision making
    if response.status == Status.ALL_FLAGS_VALID:
        print("\nAll flags are correct, we perform the operation ...")
    else:
        print("\nWarning: Flag problems were discovered.")

5.8 AutoCompleter

AutoCompleter — This is a class that configures and implements autocompletion of input commands.

Important note: On the Linux platform, the autocomplementer history only works within the current application session and is not persisted at the system level.

Constructor

def __init__(history_filename: str = False, autocomplete_button: str = 'tab') -> None
Parameters:

Example of use:

from argenta.app import App
from argenta.app.autocompleter import AutoCompleter

# Create an auto-complement with a history
completer = AutoCompleter(
    history_filename=".myapp_history",
    autocomplete_button="tab"
)

# Use auto-complement when creating an app
app = App(
    prompt="Enter the command:",
    autocompleter=completer
)

6. Extended examples

6.1 Flag handling router

An example of a router implementation with commands that handle different types of flags and use the Response object.

import re
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
from argenta.response.status import Status
from argenta.command.flag import Flag, Flags
from argenta.command.flag.defaults import PredefinedFlags

# Creating a router
file_router = Router("File operations")

# Defining flags for a copy command
copy_flags = Flags(
    Flag('source', '--'),
    Flag('destination', '--'),
    Flag('recursive', '--', False),  # Boolean flag with no value
    Flag('force', '-', False)        # Short boolean flag
)

# Register a copy command
@file_router.command(Command(
    trigger="copy",
    description="Copying files",
    flags=copy_flags,
    aliases=["cp"]
))
def copy_files(response: Response):
    # Get the values of the correct flags
    source = None
    destination = None
    recursive = False
    force = False

    for flag in response.valid_flags:
        if flag.get_name() == "source":
            source = flag.get_value()
        elif flag.get_name() == "destination":
            destination = flag.get_value()
        elif flag.get_name() == "recursive":
            recursive = True
        elif flag.get_name() == "force":
            force = True

    # Checking required settings
    if not source or not destination:
        print("Error: Source and destination must be specified")
        return

    print(f"Copy from {source} to {destination}")
    if recursive:
        print("Recursive copying is enabled")
    if force:
        print("Force copy enabled")

    # Handling undefined flags
    if response.undefined_flags:
        print("\nWarning: Unregistered flags detected:")
        for flag in response.undefined_flags:
            print(f"  - {flag.get_name()}" +
                 (f" = {flag.get_value()}" if flag.get_value() else ""))

    # Handling flags with invalid values
    if response.invalid_value_flags:
        print("\nWarning: flags with incorrect values have been detected:")
        for flag in response.invalid_value_flags:
            print(f"  - {flag.get_name()} = {flag.get_value()}")

    # Status based decision making
    if response.status != Status.ALL_FLAGS_VALID:
        print("\nExecution with warnings due to issues with flags.")

6.2 Multiple Routers

A sample application with multiple routers for different groups of functions.

from argenta.app import App
from argenta.router import Router
from argenta.command import Command
from argenta.orchestrator import Orchestrator
from argenta.app.dividing_line import DynamicDividingLine
from argenta.response import Response
import platform
import psutil
import os
import subprocess
import socket

# File Router
file_router = Router("File Operations")

@file_router.command(Command("list", "File list"))
def list_files(response: Response):
    files = os.listdir()
    for file in files:
        print(file)

@file_router.command(Command("size", "File size"))
def file_size(response: Response):
    file_name = input("Enter a file name: ")
    if os.path.exists(file_name):
        size = os.path.getsize(file_name)
        print(f"File size {file_name}: {size} byte")
    else:
        print(f"File {file_name} not found")

# Router for system operations
system_router = Router("System operations")

@system_router.command(Command("info", "System information"))
def system_info(response: Response):
    print(f"System: {platform.system()}")
    print(f"Version: {platform.version()}")
    print(f"Architecture: {platform.architecture()}")
    print(f"Processor: {platform.processor()}")

@system_router.command(Command("memory", "Memory information"))
def memory_info(response: Response):
    memory = psutil.virtual_memory()
    print(f"Total memory: {memory.total / (1024**3):.2f} ГБ")
    print(f"Available: {memory.available / (1024**3):.2f} ГБ")
    print(f"Used: {memory.used / (1024**3):.2f} ГБ ({memory.percent}%)")

# Network operations Router
network_router = Router("Network Operations")

@network_router.command(Command("ping", "Checking Host Availability"))
def ping_host(response: Response):
    host = input("Enter the hostname: ")
    print(f"Ping {host}...")
    subprocess.run(["ping", "-c", "4", host])

@network_router.command(Command("ip", "Show IP Addresses"))
def show_ip(response: Response):
    hostname = socket.gethostname()
    print(f"Имя хоста: {hostname}")
    print(f"IP-адрес: {socket.gethostbyname(hostname)}")

# Creating an application and registering Routers
app = App(
    prompt="System> ",
    initial_message="Performance Monitor v1.0",
    dividing_line=DynamicDividingLine("*")
)

# Add all routers
app.include_routers(file_router, system_router, network_router)

# Launching the application
orchestrator = Orchestrator()
orchestrator.start_polling(app)

6.3 Custom error handlers

Sample app with custom error handlers to improve responsiveness UI.

from argenta.app import App
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
from argenta.orchestrator import Orchestrator
from rich.console import Console
from rich.panel import Panel
from rich.text import Text

# Creating a Rich console for a more beautiful output
console = Console()

# Creating a router
router = Router("Core commands")

@router.command(Command("hello", "Greeting"))
def hello(response: Response):
    console.print(Panel("Hello World!", style="green"))

@router.command(Command("bye", "Farewell"))
def bye(response: Response):
    console.print(Panel("Goodbye!", style="blue"))

# Creating an app
app = App(
    prompt="MyApp> ",
    initial_message="Test application v1.0",
    print_func=console.print
)

# Registering the router
app.include_router(router)

# Custom unknown command handler
def unknown_command_handler(cmd: InputCommand):
    text = Text()
    text.append("Unknown command: ", style="bold red")
    text.append(cmd.get_trigger(), style="italic")
    text.append("\nAvailable commands: hello, bye, Q (exit)")
    console.print(Panel(text, title="Error", border_style="red"))

# Custom empty command handler
def empty_command_handler(response: Response):
    console.print(Panel("Enter Q to exit", style="yellow"))

# Custom invalid flag handler
def incorrect_input_syntax_handler(raw_command: str):
    console.print(Panel(f"Incorrect syntax: {raw_command}", title="Error", style="red"))

# Custom duplicate flag handler
def repeated_flag_handler(raw_command: str):
    console.print(Panel(f"Flag re-specified: {raw_command}", title="Warning", style="yellow"))

# Custom output handler
def exit_handler():
    console.print(Panel("Log out of the app...", style="blue", title="GoodBye"))

# Installing handlers in the application
app.set_unknown_command_handler(unknown_command_handler)
app.set_empty_command_handler(empty_command_handler)
app.set_incorrect_input_syntax_handler(incorrect_input_syntax_handler)
app.set_repeated_input_flags_handler(repeated_flag_handler)
app.set_exit_command_handler(exit_handler)

# Launching the application
orchestrator = Orchestrator()
orchestrator.start_polling(app)

7. Exceptions

The Argenta library provides a set of exceptions for handling various error situations. Below is a list of the main exceptions and their purpose:

Exception Definition
NoRegisteredHandlersException The router has no registered handlers
TooManyTransferredArgsException Too many arguments are passed
RequiredArgumentNotPassedException Required argument not passed
IncorrectNumberOfHandlerArgsException Incorrect number of arguments passed to the handler
TriggerContainSpacesException The registered trigger contains spaces

8. Best practices

When developing applications using the Argenta library, we recommend that you follow these guidelines practices:

Handling the Response Object

@router.command(Command("example", flags=my_flags))
def handler(response: Response):
    # 1. First, check for critical issues
    if response.status != Status.ALL_FLAGS_VALID:
        # Displaying warnings
        if response.undefined_flags:
            print("Warning: unknown flags")
        if response.invalid_value_flags:
            print("Warning: invalid flag values")

    # 2. Continuing with the correct flags
    for flag in response.valid_flags:
        print(f'Correct flag: {flag.get_string_entity()}')

Organizing routers

Using Autocomplete

Important: Keep in mind that on Linux systems, the history of autocompletion works only within the framework of the current application session. If you need a full history between runs, you may need to Additional customization or use of external tools.

9. Testing

The Argenta library includes a set of tests to verify functionality. You can run the tests as follows:

python -m unittest discover

For a more detailed display of test results, use the -v:

python -m unittest discover -v

When developing your own Argenta-based applications, we recommend that you create unit tests to validate the functionality of your command handlers and the general logic of the application.