4 Commits

Author SHA1 Message Date
kolo cbf7d3c578 new warning when triggers or aliases overlap 2025-05-10 21:31:59 +03:00
kolo ea2d068022 change dataclass to enum 2025-05-10 20:13:42 +03:00
kolo 5991851207 Update README.md 2025-05-10 00:29:03 +03:00
kolo f628c3b5b5 v1.0.2 2025-05-10 00:11:57 +03:00
7 changed files with 155 additions and 168 deletions
+8
View File
@@ -52,6 +52,14 @@ def main() -> None:
if __name__ == '__main__': if __name__ == '__main__':
main() main()
``` ```
---
# Фичи в разработке
- Полноценная поддержка автокомплитера на Linux
- Возможность настройки захвата stdout при обработке хэндлером ввода
## Полная [документация](https://argenta-docs.vercel.app) | MIT 2025 kolo | made by [kolo](https://t.me/kolo_id) ## Полная [документация](https://argenta-docs.vercel.app) | MIT 2025 kolo | made by [kolo](https://t.me/kolo_id)
+78 -78
View File
@@ -1,89 +1,89 @@
from argenta.app import App
from argenta.app.autocompleter import AutoCompleter
from argenta.router import Router from argenta.router import Router
from argenta.command import Command from argenta.command import Command
from argenta.orchestrator import Orchestrator
from argenta.app.dividing_line import DynamicDividingLine
from argenta.response import Response from argenta.response import Response
import platform from argenta.response.status import Status
import psutil from argenta.command.flag import Flag
import os from argenta.command.flags import Flags
import subprocess from argenta.app import App
import socket from argenta.orchestrator import Orchestrator
# Маршрутизатор для работы с файлами # Создание маршрутизатора
file_router = Router("Файловые операции") file_router = Router("Операции с файлами")
# Определение флагов для команды копирования
@file_router.command(Command("list", "Список файлов")) copy_flags = Flags(
def list_files(response: Response): Flag('source', '--'),
files = os.listdir() Flag('destination', '--'),
for file in files: Flag('recursive', '--', False), # Булевый флаг без значения
print(file) Flag('force', '-', False) # Короткий булевый флаг
@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="Pingator",
dividing_line=DynamicDividingLine("*"),
autocompleter=AutoCompleter(".hist", "e"),
) )
@file_router.command(Command('case', aliases=['cp', 'ch']))
def handler(response: Response):
print('test')
# Добавляем все маршрутизаторы # Регистрация команды копирования
app.include_routers(file_router, system_router, network_router) @file_router.command(Command(
trigger="ch",
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:
app.add_message_on_startup("Для просмотра доступных команд нажмите Enter") 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Выполнение с предупреждениями из-за проблем с флагами.")
app = App()
app.include_router(file_router)
orchestrator = Orchestrator() orchestrator = Orchestrator()
orchestrator.start_polling(app) orchestrator.start_polling(app)
+2 -2
View File
@@ -1,9 +1,9 @@
[project] [project]
name = "argenta" name = "argenta"
version = "1.0.1" version = "1.0.3"
description = "Python library for building modular CLI applications" description = "Python library for building modular CLI applications"
authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }] authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }]
requires-python = ">=3.11, <4.0" requires-python = ">=3.8"
readme = "README.md" readme = "README.md"
license = { text = "MIT" } license = { text = "MIT" }
dependencies = [ dependencies = [
+2 -4
View File
@@ -1,12 +1,10 @@
from dataclasses import dataclass from enum import Enum
@dataclass class PredefinedMessages(Enum):
class PredefinedMessages:
""" """
Public. A dataclass with predetermined messages for quick use Public. A dataclass with predetermined messages for quick use
""" """
USAGE = "[b dim]Usage[/b dim]: [i]<command> <[green]flags[/green]>[/i]" USAGE = "[b dim]Usage[/b dim]: [i]<command> <[green]flags[/green]>[/i]"
HELP = "[b dim]Help[/b dim]: [i]<command>[/i] [b red]--help[/b red]" HELP = "[b dim]Help[/b dim]: [i]<command>[/i] [b red]--help[/b red]"
AUTOCOMPLETE = "[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>" AUTOCOMPLETE = "[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>"
+59 -76
View File
@@ -22,20 +22,18 @@ from argenta.response import Response
class BaseApp: class BaseApp:
def __init__( def __init__(self,
self, prompt: str,
prompt: str, initial_message: str,
initial_message: str, farewell_message: str,
farewell_message: str, exit_command: Command,
exit_command: Command, system_router_title: str | None,
system_router_title: str | None, ignore_command_register: bool,
ignore_command_register: bool, dividing_line: StaticDividingLine | DynamicDividingLine,
dividing_line: StaticDividingLine | DynamicDividingLine, repeat_command_groups: bool,
repeat_command_groups: bool, override_system_messages: bool,
override_system_messages: bool, autocompleter: AutoCompleter,
autocompleter: AutoCompleter, print_func: Callable[[str], None]) -> None:
print_func: Callable[[str], None],
) -> None:
self._prompt = prompt self._prompt = prompt
self._print_func = print_func self._print_func = print_func
self._exit_command = exit_command self._exit_command = exit_command
@@ -49,30 +47,18 @@ class BaseApp:
self._farewell_message = farewell_message self._farewell_message = farewell_message
self._initial_message = initial_message self._initial_message = initial_message
self._description_message_gen: Callable[[str, str], str] = ( self._description_message_gen: Callable[[str, str], str] = (lambda command, description: f"[{command}] *=*=* {description}")
lambda command, description: f"[{command}] *=*=* {description}"
)
self._registered_routers: RegisteredRouters = RegisteredRouters() self._registered_routers: RegisteredRouters = RegisteredRouters()
self._messages_on_startup: list[str] = [] self._messages_on_startup: list[str] = []
self._all_registered_triggers_in_lower: list[str] = [] self._all_registered_triggers_in_lower_case: list[str] = []
self._all_registered_triggers_in_default_case: list[str] = [] self._all_registered_triggers_in_default_case: list[str] = []
self._incorrect_input_syntax_handler: Callable[[str], None] = ( self._incorrect_input_syntax_handler: Callable[[str], None] = (lambda raw_command: print_func(f"Incorrect flag syntax: {raw_command}"))
lambda raw_command: print_func(f"Incorrect flag syntax: {raw_command}") self._repeated_input_flags_handler: Callable[[str], None] = (lambda raw_command: print_func(f"Repeated input flags: {raw_command}"))
) self._empty_input_command_handler: Callable[[], None] = lambda: print_func("Empty input command")
self._repeated_input_flags_handler: Callable[[str], None] = ( self._unknown_command_handler: Callable[[InputCommand], None] = (lambda command: print_func(f"Unknown command: {command.get_trigger()}"))
lambda raw_command: print_func(f"Repeated input flags: {raw_command}") self._exit_command_handler: Callable[[Response], None] = (lambda response: print_func(self._farewell_message))
)
self._empty_input_command_handler: Callable[[], None] = lambda: print_func(
"Empty input command"
)
self._unknown_command_handler: Callable[[InputCommand], None] = (
lambda command: print_func(f"Unknown command: {command.get_trigger()}")
)
self._exit_command_handler: Callable[[Response], None] = (
lambda response: print_func(self._farewell_message)
)
def set_description_message_pattern(self, _: Callable[[str, str], str]) -> None: def set_description_message_pattern(self, _: Callable[[str, str], str]) -> None:
""" """
@@ -208,7 +194,7 @@ class BaseApp:
""" """
input_command_trigger = command.get_trigger() input_command_trigger = command.get_trigger()
if self._ignore_command_register: if self._ignore_command_register:
if input_command_trigger.lower() in self._all_registered_triggers_in_lower: if input_command_trigger.lower() in self._all_registered_triggers_in_lower_case:
return False return False
else: else:
if input_command_trigger in self._all_registered_triggers_in_default_case: if input_command_trigger in self._all_registered_triggers_in_default_case:
@@ -249,7 +235,7 @@ class BaseApp:
def _most_similar_command(self, unknown_command: str) -> str | None: def _most_similar_command(self, unknown_command: str) -> str | None:
all_commands = ( all_commands = (
self._all_registered_triggers_in_lower self._all_registered_triggers_in_lower_case
if self._ignore_command_register if self._ignore_command_register
else self._all_registered_triggers_in_default_case else self._all_registered_triggers_in_default_case
) )
@@ -318,21 +304,22 @@ class BaseApp:
self._setup_system_router() self._setup_system_router()
for router_entity in self._registered_routers: for router_entity in self._registered_routers:
self._all_registered_triggers_in_default_case.extend( self._all_registered_triggers_in_default_case.extend(router_entity.get_triggers())
router_entity.get_triggers() self._all_registered_triggers_in_default_case.extend(router_entity.get_aliases())
)
self._all_registered_triggers_in_default_case.extend(
router_entity.get_aliases()
)
self._all_registered_triggers_in_lower.extend( self._all_registered_triggers_in_lower_case.extend([x.lower() for x in router_entity.get_triggers()])
[x.lower() for x in router_entity.get_triggers()] self._all_registered_triggers_in_lower_case.extend([x.lower() for x in router_entity.get_aliases()])
)
self._all_registered_triggers_in_lower.extend(
[x.lower() for x in router_entity.get_aliases()]
)
self._autocompleter.initial_setup(self._all_registered_triggers_in_lower) self._autocompleter.initial_setup(self._all_registered_triggers_in_lower_case)
if self._ignore_command_register:
for cmd in set(self._all_registered_triggers_in_lower_case):
if self._all_registered_triggers_in_lower_case.count(cmd) != 1:
Console().print(f"\n[b red]WARNING:[/b red] Overlapping trigger or alias: [b blue]{cmd}[/b blue]")
else:
for cmd in set(self._all_registered_triggers_in_default_case):
if self._all_registered_triggers_in_default_case.count(cmd) != 1:
Console().print(f"\n[b red]WARNING:[/b red] Overlapping trigger or alias: [b blue]{cmd}[/b blue]")
if not self._override_system_messages: if not self._override_system_messages:
self._setup_default_view() self._setup_default_view()
@@ -349,20 +336,18 @@ class BaseApp:
class App(BaseApp): class App(BaseApp):
def __init__( def __init__(self,
self, prompt: str = "What do you want to do?\n",
prompt: str = "What do you want to do?\n", initial_message: str = "Argenta\n",
initial_message: str = "\nArgenta\n", farewell_message: str = "\nSee you\n",
farewell_message: str = "\nSee you\n", exit_command: Command = Command("Q", "Exit command"),
exit_command: Command = Command("Q", "Exit command"), system_router_title: str | None = "System points:",
system_router_title: str | None = "System points:", ignore_command_register: bool = True,
ignore_command_register: bool = True, dividing_line: StaticDividingLine | DynamicDividingLine = StaticDividingLine(),
dividing_line: StaticDividingLine | DynamicDividingLine = StaticDividingLine(), repeat_command_groups: bool = True,
repeat_command_groups: bool = True, override_system_messages: bool = False,
override_system_messages: bool = False, autocompleter: AutoCompleter = AutoCompleter(),
autocompleter: AutoCompleter = AutoCompleter(), print_func: Callable[[str], None] = Console().print) -> None:
print_func: Callable[[str], None] = Console().print,
) -> None:
""" """
Public. The essence of the application itself. Public. The essence of the application itself.
Configures and manages all aspects of the behavior and presentation of the user interacting with the user Configures and manages all aspects of the behavior and presentation of the user interacting with the user
@@ -379,19 +364,17 @@ class App(BaseApp):
:param print_func: system messages text output function :param print_func: system messages text output function
:return: None :return: None
""" """
super().__init__( super().__init__(prompt=prompt,
prompt=prompt, initial_message=initial_message,
initial_message=initial_message, farewell_message=farewell_message,
farewell_message=farewell_message, exit_command=exit_command,
exit_command=exit_command, system_router_title=system_router_title,
system_router_title=system_router_title, ignore_command_register=ignore_command_register,
ignore_command_register=ignore_command_register, dividing_line=dividing_line,
dividing_line=dividing_line, repeat_command_groups=repeat_command_groups,
repeat_command_groups=repeat_command_groups, override_system_messages=override_system_messages,
override_system_messages=override_system_messages, autocompleter=autocompleter,
autocompleter=autocompleter, print_func=print_func)
print_func=print_func,
)
def run_polling(self) -> None: def run_polling(self) -> None:
""" """
@@ -420,7 +403,7 @@ class App(BaseApp):
system_router.finds_appropriate_handler(input_command) system_router.finds_appropriate_handler(input_command)
if self._ignore_command_register: if self._ignore_command_register:
self._autocompleter.exit_setup( self._autocompleter.exit_setup(
self._all_registered_triggers_in_lower self._all_registered_triggers_in_lower_case
) )
else: else:
self._autocompleter.exit_setup( self._autocompleter.exit_setup(
+4 -6
View File
@@ -192,14 +192,12 @@ class Router:
pass pass
else: else:
file_path: str = getsourcefile(func) file_path: str = getsourcefile(func)
source_line: int = getsourcelines(func)[1] + 1 source_line: int = getsourcelines(func)[1]
fprint = Console().print fprint = Console().print
fprint( fprint(f'\nFile "{file_path}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint '
f'\nFile "{file_path}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint '
f"of argument([green]{transferred_arg}[/green]) passed to the handler is [/i][bold blue]{Response}[/bold blue]," f"of argument([green]{transferred_arg}[/green]) passed to the handler is [/i][bold blue]{Response}[/bold blue],"
f" [i]but[/i] [bold blue]{arg_annotation}[/bold blue] [i]is specified[/i]\n", f" [i]but[/i] [bold blue]{arg_annotation}[/bold blue] [i]is specified[/i]",
highlight=False, highlight=False)
)
def set_command_register_ignore(self, _: bool) -> None: def set_command_register_ignore(self, _: bool) -> None:
""" """
+2 -2
View File
@@ -33,13 +33,13 @@ class MyTestCase(unittest.TestCase):
def test_is_unknown_command1(self): def test_is_unknown_command1(self):
app = App() app = App()
app.set_unknown_command_handler(lambda command: None) app.set_unknown_command_handler(lambda command: None)
app._all_registered_triggers_in_lower = ['fr', 'tr', 'de'] app._all_registered_triggers_in_lower_case = ['fr', 'tr', 'de']
self.assertEqual(app._is_unknown_command(InputCommand('fr')), False) self.assertEqual(app._is_unknown_command(InputCommand('fr')), False)
def test_is_unknown_command2(self): def test_is_unknown_command2(self):
app = App() app = App()
app.set_unknown_command_handler(lambda command: None) app.set_unknown_command_handler(lambda command: None)
app._all_registered_triggers_in_lower = ['fr', 'tr', 'de'] app._all_registered_triggers_in_lower_case = ['fr', 'tr', 'de']
self.assertEqual(app._is_unknown_command(InputCommand('cr')), True) self.assertEqual(app._is_unknown_command(InputCommand('cr')), True)
def test_is_unknown_command3(self): def test_is_unknown_command3(self):