40 Commits

Author SHA1 Message Date
kolo 07ac2af71e some fix 2025-05-07 19:14:19 +03:00
kolo c4b3aa7db8 working 2025-05-07 02:15:42 +03:00
kolo 61ef6a6466 all tests passed 2025-05-06 21:53:53 +03:00
kolo 477f3a7dec starting refactor tests 2025-05-04 16:40:10 +03:00
kolo adf3431388 more beautiful typehints warning 2025-05-04 03:08:54 +03:00
kolo 83955aa046 first beta - adding hints for similar commands, now - feature freezing 2025-05-04 02:13:05 +03:00
kolo 5a17e916eb work on stable major version 2025-04-30 15:48:38 +03:00
kolo 1159dda16e work on Response model 2025-04-30 00:08:49 +03:00
kolo 315508a36e work on Response 2025-04-29 20:40:47 +03:00
kolo 9d6598c4e0 work on Response model 2025-04-29 00:07:32 +03:00
kolo eb43806da6 new model - Response 2025-04-28 02:21:34 +03:00
kolo e076dbf84f new img in docs 2025-04-27 23:43:14 +03:00
kolo 2f090b6b47 new img in docs 2025-04-27 23:42:56 +03:00
kolo c9dbf2bbae fix print framed text with static dividing line 2025-04-27 23:27:08 +03:00
kolo e768c1bd2c fix 2025-04-27 21:29:14 +03:00
kolo 408450ec12 fix 2025-04-27 21:20:44 +03:00
kolo 106ca058be new tests 2025-04-27 14:11:01 +03:00
kolo b5ddfb3b35 new tests 2025-04-27 13:28:11 +03:00
kolo 61e4502e41 work, fix etc. 2025-04-26 22:23:35 +03:00
kolo 9b2fc87e33 release v1.0.0a1 2025-04-25 02:29:44 +03:00
kolo 89f09c42f8 pre-release v1.0.0 2025-04-24 21:26:41 +03:00
kolo 5bcae8fe68 Update README.md 2025-04-24 17:40:53 +03:00
kolo ca58008431 fix 2025-04-23 22:07:57 +03:00
kolo 30974f48eb fix 2025-04-23 22:06:10 +03:00
kolo df4ba080b0 new docs 2025-04-23 22:02:02 +03:00
kolo f93930d712 work 2025-04-23 21:45:04 +03:00
kolo 036c17ec9a last steps work on new docs, full complete write docstring for all objects 2025-04-23 20:54:03 +03:00
kolo 7281fdeabf new app preview img in docs 2025-04-23 20:50:31 +03:00
kolo 051ec6df28 steps 2025-04-20 23:58:15 +03:00
kolo 00a1e11fc1 new docs 2025-04-19 12:13:29 +03:00
kolo 584df9ba69 new docs 2025-04-18 12:50:30 +03:00
kolo a649022f1d work on docstrings 2025-04-15 23:49:51 +03:00
kolo 26a9d8a6da work on 2025-04-15 22:26:20 +03:00
kolo 9522b0161a work on 2025-04-15 01:09:03 +03:00
kolo e189f8d9aa big step 2025-04-14 16:38:53 +03:00
kolo 3ef8707cfa big step 2025-04-14 14:54:17 +03:00
kolo a5fdcab862 work on 2025-04-14 01:03:24 +03:00
kolo ba035881ee work on support args 2025-04-13 19:24:03 +03:00
kolo 34ebe55531 first steps 2025-04-13 14:39:53 +03:00
kolo 01c9d2dc6d first step 2025-04-13 14:12:08 +03:00
64 changed files with 3002 additions and 1305 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
.venv .venv
.idea .idea
dist dist
poetry.lock uv.lock
*__pycache__/ *__pycache__/
*.hist* *.hist*
build
source
+1151 -407
View File
File diff suppressed because it is too large Load Diff
-24
View File
@@ -1,24 +0,0 @@
class BaseDividingLine:
def __init__(self, unit_part: str = '-'):
self.unit_part = unit_part
def get_unit_part(self):
if len(self.unit_part) == 0:
return ' '
else:
return self.unit_part[0]
class StaticDividingLine(BaseDividingLine):
def __init__(self, unit_part: str = '-', length: int = 25):
super().__init__(unit_part)
self.length = length
def get_full_line(self):
return f'\n[dim]{self.length * self.get_unit_part()}[/dim]\n'
class DynamicDividingLine(BaseDividingLine):
def get_full_line(self, length: int):
return f'\n[dim]{self.get_unit_part() * length}[/dim]\n'
-10
View File
@@ -1,10 +0,0 @@
class NoRegisteredRoutersException(Exception):
def __str__(self):
return "No Registered Router Found"
class NoRegisteredHandlersException(Exception):
def __init__(self, router_name):
self.router_name = router_name
def __str__(self):
return f"No Registered Handlers Found For '{self.router_name}'"
-265
View File
@@ -1,265 +0,0 @@
from typing import Callable
from rich.console import Console
from rich.markup import escape
from art import text2art
from contextlib import redirect_stdout
import io
import re
from argenta.command.models import Command, InputCommand
from argenta.router import Router
from argenta.router.defaults import system_router
from argenta.app.autocompleter import AutoCompleter
from argenta.app.dividing_line.models import StaticDividingLine, DynamicDividingLine
from argenta.command.exceptions import (UnprocessedInputFlagException,
RepeatedInputFlagsException,
EmptyInputCommandException,
BaseInputCommandException)
from argenta.app.exceptions import (NoRegisteredRoutersException,
NoRegisteredHandlersException)
from argenta.app.registered_routers.entity import RegisteredRouters
class AppInit:
def __init__(self,
prompt: str = '[italic dim bold]What do you want to do?\n',
initial_message: str = '\nArgenta\n',
farewell_message: str = '\nSee you\n',
exit_command: Command = Command('Q', 'Exit command'),
system_points_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:
self._prompt = prompt
self._print_func = print_func
self._exit_command = exit_command
self._system_points_title = system_points_title
self._dividing_line = dividing_line
self._ignore_command_register = ignore_command_register
self._repeat_command_groups_description = repeat_command_groups
self._override_system_messages = override_system_messages
self._autocompleter = autocompleter
self._farewell_message = farewell_message
self._initial_message = initial_message
self._description_message_gen: Callable[[str, str], str] = lambda command, description: f'[bold red]{escape('['+command+']')}[/bold red] [blue dim]*=*=*[/blue dim] [bold yellow italic]{escape(description)}'
self._registered_routers: RegisteredRouters = RegisteredRouters()
self._messages_on_startup = []
self._invalid_input_flags_handler: Callable[[str], None] = lambda raw_command: print_func(f'[red bold]Incorrect flag syntax: {escape(raw_command)}')
self._repeated_input_flags_handler: Callable[[str], None] = lambda raw_command: print_func(f'[red bold]Repeated input flags: {escape(raw_command)}')
self._empty_input_command_handler: Callable[[], None] = lambda: print_func('[red bold]Empty input command')
self._unknown_command_handler: Callable[[InputCommand], None] = lambda command: print_func(f"[red bold]Unknown command: {escape(command.get_trigger())}")
self._exit_command_handler: Callable[[], None] = lambda: print_func(self._farewell_message)
class AppSetters(AppInit):
def set_description_message_pattern(self, pattern: Callable[[str, str], str]) -> None:
self._description_message_gen: Callable[[str, str], str] = pattern
def set_invalid_input_flags_handler(self, handler: Callable[[str], None]) -> None:
self._invalid_input_flags_handler = handler
def set_repeated_input_flags_handler(self, handler: Callable[[str], None]) -> None:
self._repeated_input_flags_handler = handler
def set_unknown_command_handler(self, handler: Callable[[str], None]) -> None:
self._unknown_command_handler = handler
def set_empty_command_handler(self, handler: Callable[[], None]) -> None:
self._empty_input_command_handler = handler
def set_exit_command_handler(self, handler: Callable[[], None]) -> None:
self._exit_command_handler = handler
class AppPrinters(AppInit):
def _print_command_group_description(self):
for registered_router in self._registered_routers:
if registered_router.get_title():
self._print_func(registered_router.get_title())
for command_handler in registered_router.get_command_handlers():
self._print_func(self._description_message_gen(
command_handler.get_handled_command().get_trigger(),
command_handler.get_handled_command().get_description()))
self._print_func('')
def _print_framed_text_with_dynamic_line(self, text: str):
clear_text = re.sub(r'\u001b\[[0-9;]*m', '', text)
max_length_line = max([len(line) for line in clear_text.split('\n')])
max_length_line = max_length_line if 10 <= max_length_line <= 80 else 80 if max_length_line > 80 else 10
self._print_func(self._dividing_line.get_full_line(max_length_line))
print(text.strip('\n'))
self._print_func(self._dividing_line.get_full_line(max_length_line))
def _print_framed_text(self, text: str):
if isinstance(self._dividing_line, StaticDividingLine):
self._print_func(self._dividing_line.get_full_line())
self._print_func(text)
self._print_func(self._dividing_line.get_full_line())
elif isinstance(self._dividing_line, DynamicDividingLine):
self._print_framed_text_with_dynamic_line(text)
class AppNonStandardHandlers(AppPrinters):
def _is_exit_command(self, command: InputCommand):
if command.get_trigger().lower() == self._exit_command.get_trigger().lower():
if self._ignore_command_register:
system_router.input_command_handler(command)
return True
elif command.get_trigger() == self._exit_command.get_trigger():
system_router.input_command_handler(command)
return True
return False
def _is_unknown_command(self, command: InputCommand):
for router_entity in self._registered_routers:
for command_handler in router_entity.get_command_handlers():
handled_command_trigger = command_handler.get_handled_command().get_trigger()
handled_command_aliases = command_handler.get_handled_command().get_aliases()
if handled_command_trigger.lower() == command.get_trigger().lower() and self._ignore_command_register:
return False
elif handled_command_trigger == command.get_trigger():
return False
elif handled_command_aliases:
if (command.get_trigger().lower() in [x.lower() for x in handled_command_aliases]) and self._ignore_command_register:
return False
elif command.get_trigger() in handled_command_trigger:
return False
if isinstance(self._dividing_line, StaticDividingLine):
self._print_func(self._dividing_line.get_full_line())
self._unknown_command_handler(command)
self._print_func(self._dividing_line.get_full_line())
elif isinstance(self._dividing_line, DynamicDividingLine):
with redirect_stdout(io.StringIO()) as f:
self._unknown_command_handler(command)
res: str = f.getvalue()
self._print_framed_text_with_dynamic_line(res)
return True
def _error_handler(self, error: BaseInputCommandException, raw_command: str) -> None:
match error:
case UnprocessedInputFlagException():
self._invalid_input_flags_handler(raw_command)
case RepeatedInputFlagsException():
self._repeated_input_flags_handler(raw_command)
case EmptyInputCommandException():
self._empty_input_command_handler()
class AppValidators(AppInit):
def _validate_number_of_routers(self) -> None:
if not self._registered_routers:
raise NoRegisteredRoutersException()
def _validate_included_routers(self) -> None:
for router in self._registered_routers:
if not router.get_command_handlers():
raise NoRegisteredHandlersException(router.get_name())
class AppSetups(AppValidators, AppPrinters):
def _setup_system_router(self):
system_router.set_title(self._system_points_title)
@system_router.command(self._exit_command)
def exit_command():
self._exit_command_handler()
if system_router not in self._registered_routers.get_registered_routers():
system_router.set_ignore_command_register(self._ignore_command_register)
self._registered_routers.add_registered_router(system_router)
def _setup_default_view(self):
if not self._override_system_messages:
self._initial_message = f'\n[bold red]{text2art(self._initial_message, font='tarty1')}\n\n'
self._farewell_message = (
f'[bold red]\n{text2art(f'\n{self._farewell_message}\n', font='chanky')}[/bold red]\n'
f'[red i]github.com/koloideal/Argenta[/red i] | [red bold i]made by kolo[/red bold i]\n')
def _pre_cycle_setup(self):
self._setup_default_view()
self._setup_system_router()
self._validate_number_of_routers()
self._validate_included_routers()
all_triggers: list[str] = []
for router_entity in self._registered_routers:
all_triggers.extend(router_entity.get_triggers())
all_triggers.extend(router_entity.get_aliases())
self._autocompleter.initial_setup(all_triggers)
self._print_func(self._initial_message)
for message in self._messages_on_startup:
self._print_func(message)
print('\n\n')
if not self._repeat_command_groups_description:
self._print_command_group_description()
class App(AppSetters, AppNonStandardHandlers, AppSetups):
def start_polling(self) -> None:
self._pre_cycle_setup()
while True:
if self._repeat_command_groups_description:
self._print_command_group_description()
raw_command: str = Console().input(self._prompt)
try:
input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
except BaseInputCommandException as error:
with redirect_stdout(io.StringIO()) as f:
self._error_handler(error, raw_command)
res: str = f.getvalue()
self._print_framed_text(res)
continue
if self._is_exit_command(input_command):
self._autocompleter.exit_setup()
return
if self._is_unknown_command(input_command):
continue
with redirect_stdout(io.StringIO()) as f:
for registered_router in self._registered_routers:
registered_router.input_command_handler(input_command)
res: str = f.getvalue()
self._print_framed_text(res)
if not self._repeat_command_groups_description:
self._print_func(self._prompt)
def include_router(self, router: Router) -> None:
router.set_ignore_command_register(self._ignore_command_register)
self._registered_routers.add_registered_router(router)
def include_routers(self, *routers: Router) -> None:
for router in routers:
self.include_router(router)
def add_message_on_startup(self, message: str) -> None:
self._messages_on_startup.append(message)
-21
View File
@@ -1,21 +0,0 @@
from argenta.router import Router
class RegisteredRouters:
def __init__(self, registered_routers: list[Router] = None) -> None:
self._registered_routers = registered_routers if registered_routers else []
def get_registered_routers(self) -> list[Router]:
return self._registered_routers
def add_registered_router(self, router: Router):
self._registered_routers.append(router)
def add_registered_routers(self, *routers: Router):
self._registered_routers.extend(routers)
def __iter__(self):
return iter(self._registered_routers)
def __next__(self):
return next(iter(self._registered_routers))
-4
View File
@@ -1,4 +0,0 @@
__all__ = ('InputFlags', 'InputFlag', 'Flag', 'Flags')
from argenta.command.flag.models import InputFlags, InputFlag, Flags, Flag
-140
View File
@@ -1,140 +0,0 @@
from typing import Literal, Pattern
from abc import ABC, abstractmethod
class BaseFlag:
def __init__(self, name: str,
prefix: Literal['-', '--', '---'] = '--'):
self._name = name
self._prefix = prefix
def get_string_entity(self):
string_entity: str = self._prefix + self._name
return string_entity
def get_name(self):
return self._name
def get_prefix(self):
return self._prefix
class InputFlag(BaseFlag):
def __init__(self, name: str,
prefix: Literal['-', '--', '---'] = '--',
value: str = None):
super().__init__(name, prefix)
self._flag_value = value
def get_value(self) -> str | None:
return self._flag_value
def set_value(self, value):
self._flag_value = value
class Flag(BaseFlag):
def __init__(self, name: str,
prefix: Literal['-', '--', '---'] = '--',
possible_values: list[str] | Pattern[str] | False = True):
super().__init__(name, prefix)
self.possible_values = possible_values
def validate_input_flag_value(self, input_flag_value: str | None):
if self.possible_values is False:
if input_flag_value is None:
return True
else:
return False
elif isinstance(self.possible_values, Pattern):
if isinstance(input_flag_value, str):
is_valid = bool(self.possible_values.match(input_flag_value))
if bool(is_valid):
return True
else:
return False
else:
return False
elif isinstance(self.possible_values, list):
if input_flag_value in self.possible_values:
return True
else:
return False
else:
return True
class BaseFlags(ABC):
__slots__ = ('_flags',)
@abstractmethod
def get_flags(self):
pass
@abstractmethod
def add_flag(self, flag: Flag | InputFlag):
pass
@abstractmethod
def add_flags(self, flags: list[Flag] | list[InputFlag]):
pass
@abstractmethod
def get_flag(self, name: str):
pass
def __iter__(self):
return iter(self._flags)
def __next__(self):
return next(iter(self))
def __getitem__(self, item):
return self._flags[item]
class Flags(BaseFlags, ABC):
def __init__(self, *flags: Flag):
self._flags = flags if flags else []
def get_flags(self) -> list[Flag]:
return self._flags
def add_flag(self, flag: Flag):
self._flags.append(flag)
def add_flags(self, flags: list[Flag]):
self._flags.extend(flags)
def get_flag(self, name: str) -> Flag | None:
if name in [flag.get_name() for flag in self._flags]:
return list(filter(lambda flag: flag.get_name() == name, self._flags))[0]
else:
return None
class InputFlags(BaseFlags, ABC):
def __init__(self, *flags: InputFlag):
self._flags = flags if flags else []
def get_flags(self) -> list[InputFlag]:
return self._flags
def add_flag(self, flag: InputFlag):
self._flags.append(flag)
def add_flags(self, flags: list[InputFlag]):
self._flags.extend(flags)
def get_flag(self, name: str) -> InputFlag | None:
if name in [flag.get_name() for flag in self._flags]:
return list(filter(lambda flag: flag.get_name() == name, self._flags))[0]
else:
return None
-21
View File
@@ -1,21 +0,0 @@
from typing import Callable
from argenta.command import Command
from argenta.command.flag.models import InputFlags
class CommandHandler:
def __init__(self, handler: Callable[[], None] | Callable[[InputFlags], None], handled_command: Command):
self._handler = handler
self._handled_command = handled_command
def handling(self, input_flags: InputFlags = None):
if input_flags is not None:
self._handler(input_flags)
else:
self._handler()
def get_handler(self):
return self._handler
def get_handled_command(self):
return self._handled_command
-21
View File
@@ -1,21 +0,0 @@
from argenta.router.command_handler.entity import CommandHandler
class CommandHandlers:
def __init__(self, command_handlers: list[CommandHandler] = None):
self.command_handlers = command_handlers if command_handlers else []
def get_command_handlers(self) -> list[CommandHandler]:
return self.command_handlers
def add_command_handler(self, command_handler: CommandHandler):
self.command_handlers.append(command_handler)
def add_command_handlers(self, *command_handlers: CommandHandler):
self.command_handlers.extend(command_handlers)
def __iter__(self):
return iter(self.command_handlers)
def __next__(self):
return next(iter(self.command_handlers))
-5
View File
@@ -1,5 +0,0 @@
from argenta.router import Router
system_router = Router(title='System points:',
name='System')
-149
View File
@@ -1,149 +0,0 @@
from typing import Callable, Any
from inspect import getfullargspec
from argenta.command import Command
from argenta.command.models import InputCommand
from argenta.router.command_handlers.entity import CommandHandlers
from argenta.router.command_handler.entity import CommandHandler
from argenta.command.flag.models import Flag, Flags, InputFlags
from argenta.router.exceptions import (RepeatedFlagNameException,
TooManyTransferredArgsException,
RequiredArgumentNotPassedException,
IncorrectNumberOfHandlerArgsException,
TriggerCannotContainSpacesException)
class Router:
def __init__(self,
title: str = None,
name: str = 'Default'):
self._title = title
self._name = name
self._command_handlers: CommandHandlers = CommandHandlers()
self._ignore_command_register: bool = False
self._not_valid_flag_handler: Callable[[Flag], None] = lambda flag: print(f"Undefined or incorrect input flag: {flag.get_string_entity()}{(' '+flag.get_value()) if flag.get_value() else ''}")
def command(self, command: Command) -> Callable[[Any], Any]:
self._validate_command(command)
def command_decorator(func):
Router._validate_func_args(command, func)
self._command_handlers.add_command_handler(CommandHandler(func, command))
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return command_decorator
def set_invalid_input_flag_handler(self, func):
processed_args = getfullargspec(func).args
if len(processed_args) != 1:
raise IncorrectNumberOfHandlerArgsException()
else:
self._not_valid_flag_handler = func
def input_command_handler(self, input_command: InputCommand):
input_command_name: str = input_command.get_trigger()
input_command_flags: InputFlags = input_command.get_input_flags()
for command_handler in self._command_handlers:
handle_command = command_handler.get_handled_command()
if input_command_name.lower() == handle_command.get_trigger().lower():
self._validate_input_command(input_command_flags, command_handler)
elif handle_command.get_aliases():
if input_command_name.lower() in handle_command.get_aliases():
self._validate_input_command(input_command_flags, command_handler)
def _validate_input_command(self, input_command_flags: InputFlags, command_handler: CommandHandler):
handle_command = command_handler.get_handled_command()
if handle_command.get_registered_flags().get_flags():
if input_command_flags.get_flags():
if self._validate_input_flags(handle_command, input_command_flags):
command_handler.handling(input_command_flags)
return
else:
command_handler.handling(input_command_flags)
return
else:
if input_command_flags.get_flags():
self._not_valid_flag_handler(input_command_flags[0])
return
else:
command_handler.handling()
return
def _validate_input_flags(self, handle_command: Command, input_flags: InputFlags):
for flag in input_flags:
is_valid = handle_command.validate_input_flag(flag)
if not is_valid:
self._not_valid_flag_handler(flag)
return False
return True
@staticmethod
def _validate_command(command: Command):
command_name: str = command.get_trigger()
if command_name.find(' ') != -1:
raise TriggerCannotContainSpacesException()
flags: Flags = command.get_registered_flags()
if flags:
flags_name: list = [x.get_string_entity().lower() for x in flags]
if len(set(flags_name)) < len(flags_name):
raise RepeatedFlagNameException()
@staticmethod
def _validate_func_args(command: Command, func: Callable):
registered_args = command.get_registered_flags()
transferred_args = getfullargspec(func).args
if registered_args.get_flags() and transferred_args:
if len(transferred_args) != 1:
raise TooManyTransferredArgsException()
elif registered_args.get_flags() and not transferred_args:
raise RequiredArgumentNotPassedException()
elif not registered_args.get_flags() and transferred_args:
raise TooManyTransferredArgsException()
def set_ignore_command_register(self, ignore_command_register: bool):
self._ignore_command_register = ignore_command_register
def get_triggers(self):
all_triggers: list[str] = []
for command_handler in self._command_handlers:
all_triggers.append(command_handler.get_handled_command().get_trigger())
return all_triggers
def get_aliases(self):
all_aliases: list[str] = []
for command_handler in self._command_handlers:
if command_handler.get_handled_command().get_aliases():
all_aliases.extend(command_handler.get_handled_command().get_aliases())
return all_aliases
def get_command_handlers(self) -> CommandHandlers:
return self._command_handlers
def get_name(self) -> str:
return self._name
def get_title(self) -> str | None:
return self._title
def set_title(self, title: str):
self._title = title
-23
View File
@@ -1,23 +0,0 @@
class RepeatedFlagNameException(Exception):
def __str__(self):
return "Repeated registered_flag name in register command"
class TooManyTransferredArgsException(Exception):
def __str__(self):
return "Too many transferred arguments"
class RequiredArgumentNotPassedException(Exception):
def __str__(self):
return "Required argument not passed"
class IncorrectNumberOfHandlerArgsException(Exception):
def __str__(self):
return "Handler has incorrect number of arguments"
class TriggerCannotContainSpacesException(Exception):
def __str__(self):
return "Command trigger cannot contain spaces"

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

+21 -4
View File
@@ -1,9 +1,26 @@
from mock.mock_app.handlers.routers import work_router
from argenta.app import App from argenta.app import App
from argenta.app.defaults import PredeterminedMessages from argenta.app.defaults import PredefinedMessages
from argenta.app.autocompleter import AutoCompleter
from argenta.orchestrator import Orchestrator
from argenta.orchestrator.argparser import ArgParser
from argenta.orchestrator.argparser.arguments import BooleanArgument
app = App(repeat_command_groups=True) arg_parser = ArgParser(processed_args=[BooleanArgument('repeat')])
app: App = App(autocompleter=AutoCompleter('.hist'))
orchestrator: Orchestrator = Orchestrator()
app.add_message_on_startup(PredeterminedMessages.USAGE + '\n\n')
app.start_polling() def main():
app.include_router(work_router)
app.add_message_on_startup(PredefinedMessages.USAGE)
app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE)
app.add_message_on_startup(PredefinedMessages.HELP)
orchestrator.start_polling(app)
if __name__ == "__main__":
main()
+16 -7
View File
@@ -1,14 +1,23 @@
from argenta.response import Response, Status
from argenta.app import App from argenta.app import App
from argenta.app.dividing_line import StaticDividingLine, DynamicDividingLine
from argenta.app.autocompleter import AutoCompleter
from argenta.app.defaults import PredefinedMessages
from argenta.command import Command from argenta.command import Command
from argenta.command.flags import Flags, InputFlags, InvalidValueInputFlags, UndefinedInputFlags, ValidInputFlags
from argenta.command.flag import Flag, InputFlag
from argenta.command.flag.defaults import PredefinedFlags
from argenta.router import Router from argenta.router import Router
from argenta.orchestrator import Orchestrator
from argenta.command.models import InputCommand
import inspect
router = Router() router = Router()
@router.command(Command('test'))
def test():
print('test command')
app = App(ignore_command_register=False) @router.command(Command('some'))
app.include_router(router) def handler(res: Response) -> Response:
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.get_trigger()}')) pass
app.start_polling()
+15 -19
View File
@@ -1,35 +1,31 @@
from rich.console import Console from rich.console import Console
from argenta.command import Command from argenta.command import Command
from argenta.command.flag import Flags, InputFlags from argenta.command.flag.defaults import PredefinedFlags
from argenta.command.flag.defaults import PredeterminedFlags from argenta.command.flags import Flags
from argenta.response import Response
from argenta.router import Router from argenta.router import Router
from .handlers_implementation.help_command import help_command
work_router: Router = Router(title='Work points:') work_router: Router = Router(title='Work points:')
settings_router: Router = Router()
console = Console() console = Console()
@work_router.command(Command('get', 'Get Help', aliases=['help', 'Get_help'])) @work_router.command(Command('get', 'Get Help', aliases=['help', 'Get_help'], flags=Flags(PredefinedFlags.PORT, PredefinedFlags.HOST)))
def command_help(): def command_help(response: Response):
help_command() print(response.status)
print(response.undefined_flags.get_flags())
print(response.valid_flags.get_flags())
print(response.invalid_value_flags.get_flags())
@work_router.command(Command('start', 'Start Solving', @work_router.command('run')
flags=Flags(PredeterminedFlags.HOST, PredeterminedFlags.PORT), def command_start_solving(response: Response):
aliases=['starting'])) print(response.status)
def command_start_solving(args: InputFlags): print(response.undefined_flags.get_flags())
print(args.get_flag('host')) print(response.valid_flags.get_flags())
print(response.invalid_value_flags.get_flags())
@settings_router.command(Command('update', 'Update WordMath'))
def command_update():
print('eeeeeee')
+14 -9
View File
@@ -1,24 +1,29 @@
from mock.mock_app.handlers.routers import work_router, settings_router from mock.mock_app.handlers.routers import work_router
from argenta.app import App from argenta.app import App
from argenta.app.defaults import PredeterminedMessages from argenta.app.defaults import PredefinedMessages
from argenta.app.dividing_line import DynamicDividingLine from argenta.app.dividing_line import DynamicDividingLine
from argenta.app.autocompleter import AutoCompleter from argenta.app.autocompleter import AutoCompleter
from argenta.orchestrator import Orchestrator
from argenta.orchestrator.argparser import ArgParser
from argenta.orchestrator.argparser.arguments import BooleanArgument
autocompleter = AutoCompleter('./mock/.hist') arg_parser = ArgParser(processed_args=[BooleanArgument('repeat')])
app: App = App(dividing_line=DynamicDividingLine(), app: App = App(dividing_line=DynamicDividingLine(),
autocompleter=autocompleter) autocompleter=AutoCompleter('./mock/.hist'),
repeat_command_groups=False,)
orchestrator: Orchestrator = Orchestrator(arg_parser)
def main(): def main():
app.include_routers(work_router, settings_router) app.include_router(work_router)
app.add_message_on_startup(PredeterminedMessages.USAGE) app.add_message_on_startup(PredefinedMessages.USAGE)
app.add_message_on_startup(PredeterminedMessages.AUTOCOMPLETE) app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE)
app.add_message_on_startup(PredeterminedMessages.HELP) app.add_message_on_startup(PredefinedMessages.HELP)
app.start_polling() orchestrator.start_polling(app)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+16 -14
View File
@@ -1,15 +1,21 @@
[project] [project]
name = "argenta" name = "argenta"
version = "0.5.0" version = "1.0.0-beta2"
description = "Python library for creating TUI" description = "Python library for building modular CLI applications"
authors = [ authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }]
{name = "kolo", email = "kolo.is.main@gmail.com"} requires-python = ">=3.11, <4.0"
]
license = {text = "MIT"}
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" license = { text = "MIT" }
dependencies = ["rich (>=14.0.0,<15.0.0)", "art (>=6.4,<7.0)"] dependencies = [
"rich (>=14.0.0,<15.0.0)",
"art (>=6.4,<7.0)",
"pyreadline3>=3.5.4",
]
[dependency-groups]
dev = [
"pydoc-markdown>=4.8.2,<5",
]
[tool.ruff] [tool.ruff]
exclude = [ exclude = [
@@ -21,11 +27,7 @@ exclude = [
"tests" "tests"
] ]
[build-system] [build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"] requires = ["hatchling"]
build-backend = "poetry.core.masonry.api" build-backend = "hatchling.build"
[tool.poetry.group.dev.dependencies]
pyreadline3 = "^3.5.4"
@@ -3,13 +3,25 @@ import readline
class AutoCompleter: class AutoCompleter:
def __init__(self, history_filename: str = False, autocomplete_button: str = 'tab'): def __init__(self, history_filename: str = False, autocomplete_button: str = 'tab') -> None:
"""
Public. Configures and implements auto-completion of input command
:param history_filename: the name of the file for saving the history of the autocompleter
:param autocomplete_button: the button for auto-completion
:return: None
"""
self.history_filename = history_filename self.history_filename = history_filename
self.autocomplete_button = autocomplete_button self.autocomplete_button = autocomplete_button
self.matches = [] self.matches: list[str] = []
def complete(self, text, state): def _complete(self, text, state) -> str | None:
matches = sorted(cmd for cmd in self.get_history_items() if cmd.startswith(text)) """
Private. Auto-completion function
:param text: part of the command being entered
:param state: the current cursor position is relative to the beginning of the line
:return: the desired candidate as str or None
"""
matches: list[str] = sorted(cmd for cmd in self.get_history_items() if cmd.startswith(text))
if len(matches) > 1: if len(matches) > 1:
common_prefix = matches[0] common_prefix = matches[0]
for match in matches[1:]: for match in matches[1:]:
@@ -26,7 +38,12 @@ class AutoCompleter:
else: else:
return None return None
def initial_setup(self, all_commands: list[str]): def initial_setup(self, all_commands: list[str]) -> None:
"""
Private. Initial setup function
:param all_commands: Registered commands for adding them to the autocomplete history
:return: None
"""
if self.history_filename: if self.history_filename:
if os.path.exists(self.history_filename): if os.path.exists(self.history_filename):
readline.read_history_file(self.history_filename) readline.read_history_file(self.history_filename)
@@ -34,14 +51,22 @@ class AutoCompleter:
for line in all_commands: for line in all_commands:
readline.add_history(line) readline.add_history(line)
readline.set_completer(self.complete) readline.set_completer(self._complete)
readline.set_completer_delims(readline.get_completer_delims().replace(' ', '')) readline.set_completer_delims(readline.get_completer_delims().replace(' ', ''))
readline.parse_and_bind(f'{self.autocomplete_button}: complete') readline.parse_and_bind(f'{self.autocomplete_button}: complete')
def exit_setup(self): def exit_setup(self) -> None:
"""
Private. Exit setup function
:return: None
"""
if self.history_filename: if self.history_filename:
readline.write_history_file(self.history_filename) readline.write_history_file(self.history_filename)
@staticmethod @staticmethod
def get_history_items(): def get_history_items() -> list[str] | list:
"""
Private. Returns a list of all commands entered by the user
:return: all commands entered by the user as list[str]
"""
return [readline.get_history_item(i) for i in range(1, readline.get_current_history_length() + 1)] return [readline.get_history_item(i) for i in range(1, readline.get_current_history_length() + 1)]
@@ -2,7 +2,10 @@ from dataclasses import dataclass
@dataclass @dataclass
class PredeterminedMessages: class PredefinedMessages:
"""
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>'
+68
View File
@@ -0,0 +1,68 @@
from abc import ABC
class BaseDividingLine(ABC):
def __init__(self, unit_part: str = '-') -> None:
"""
Private. The basic dividing line
:param unit_part: the single part of the dividing line
:return: None
"""
self._unit_part = unit_part
def get_unit_part(self) -> str:
"""
Private. Returns the unit part of the dividing line
:return: unit_part of dividing line as str
"""
if len(self._unit_part) == 0:
return ' '
else:
return self._unit_part[0]
class StaticDividingLine(BaseDividingLine):
def __init__(self, unit_part: str = '-', length: int = 25) -> None:
"""
Public. The static dividing line
:param unit_part: the single part of the dividing line
:param length: the length of the dividing line
:return: None
"""
super().__init__(unit_part)
self.length = length
def get_full_static_line(self, is_override: bool) -> str:
"""
Private. Returns the full line of the dividing line
:param is_override: has the default text layout been redefined
:return: full line of dividing line as str
"""
if is_override:
return f'\n{self.length * self.get_unit_part()}\n'
else:
return f'\n[dim]{self.length * self.get_unit_part()}[/dim]\n'
class DynamicDividingLine(BaseDividingLine):
def __init__(self, unit_part: str = '-') -> None:
"""
Public. The dynamic dividing line
:param unit_part: the single part of the dividing line
:return: None
"""
super().__init__(unit_part)
def get_full_dynamic_line(self, length: int, is_override: bool) -> str:
"""
Private. Returns the full line of the dividing line
:param length: the length of the dividing line
:param is_override: has the default text layout been redefined
:return: full line of dividing line as str
"""
if is_override:
return f'\n{length * self.get_unit_part()}\n'
else:
return f'\n[dim]{self.get_unit_part() * length}[/dim]\n'
+401
View File
@@ -0,0 +1,401 @@
from typing import Callable
from rich.console import Console
from rich.markup import escape
from art import text2art
from contextlib import redirect_stdout
import io
import re
from argenta.command.models import Command, InputCommand
from argenta.router import Router
from argenta.router.defaults import system_router
from argenta.app.autocompleter import AutoCompleter
from argenta.app.dividing_line.models import StaticDividingLine, DynamicDividingLine
from argenta.command.exceptions import (UnprocessedInputFlagException,
RepeatedInputFlagsException,
EmptyInputCommandException,
BaseInputCommandException)
from argenta.app.registered_routers.entity import RegisteredRouters
from argenta.response import Response
class BaseApp:
def __init__(self,
prompt: str,
initial_message: str,
farewell_message: str,
exit_command: Command,
system_router_title: str | None,
ignore_command_register: bool,
dividing_line: StaticDividingLine | DynamicDividingLine,
repeat_command_groups: bool,
override_system_messages: bool,
autocompleter: AutoCompleter,
print_func: Callable[[str], None]) -> None:
self._prompt = prompt
self._print_func = print_func
self._exit_command = exit_command
self._system_router_title = system_router_title
self._dividing_line = dividing_line
self._ignore_command_register = ignore_command_register
self._repeat_command_groups_description = repeat_command_groups
self._override_system_messages = override_system_messages
self._autocompleter = autocompleter
self._farewell_message = farewell_message
self._initial_message = initial_message
self._description_message_gen: Callable[[str, str], str] = lambda command, description: f'[{command}] *=*=* {description}'
self._registered_routers: RegisteredRouters = RegisteredRouters()
self._messages_on_startup: list[str] = []
self._all_registered_triggers_in_lower: list[str] = []
self._all_registered_triggers_in_default_case: list[str] = []
self._invalid_input_flags_handler: Callable[[str], None] = 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._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:
"""
Public. Sets the output pattern of the available commands
:param _: output pattern of the available commands
:return: None
"""
self._description_message_gen: Callable[[str, str], str] = _
def set_invalid_input_flags_handler(self, _: Callable[[str], None]) -> None:
"""
Public. Sets the handler for incorrect flags when entering a command
:param _: handler for incorrect flags when entering a command
:return: None
"""
self._invalid_input_flags_handler = _
def set_repeated_input_flags_handler(self, _: Callable[[str], None]) -> None:
"""
Public. Sets the handler for repeated flags when entering a command
:param _: handler for repeated flags when entering a command
:return: None
"""
self._repeated_input_flags_handler = _
def set_unknown_command_handler(self, _: Callable[[str], None]) -> None:
"""
Public. Sets the handler for unknown commands when entering a command
:param _: handler for unknown commands when entering a command
:return: None
"""
self._unknown_command_handler = _
def set_empty_command_handler(self, _: Callable[[], None]) -> None:
"""
Public. Sets the handler for empty commands when entering a command
:param _: handler for empty commands when entering a command
:return: None
"""
self._empty_input_command_handler = _
def set_exit_command_handler(self, _: Callable[[], None]) -> None:
"""
Public. Sets the handler for exit command when entering a command
:param _: handler for exit command when entering a command
:return: None
"""
self._exit_command_handler = _
def _print_command_group_description(self) -> None:
"""
Private. Prints the description of the available commands
:return: None
"""
for registered_router in self._registered_routers:
if registered_router.get_title():
self._print_func(registered_router.get_title())
for command_handler in registered_router.get_command_handlers():
self._print_func(self._description_message_gen(
command_handler.get_handled_command().get_trigger(),
command_handler.get_handled_command().get_description()))
self._print_func('')
def _print_framed_text(self, text: str) -> None:
"""
Private. Outputs text by framing it in a static or dynamic split strip
:param text: framed text
:return: None
"""
if isinstance(self._dividing_line, StaticDividingLine):
self._print_func(self._dividing_line.get_full_static_line(self._override_system_messages))
print(text.strip('\n'))
self._print_func(self._dividing_line.get_full_static_line(self._override_system_messages))
elif isinstance(self._dividing_line, DynamicDividingLine):
clear_text = re.sub(r'\u001b\[[0-9;]*m', '', text)
max_length_line = max([len(line) for line in clear_text.split('\n')])
max_length_line = max_length_line if 10 <= max_length_line <= 80 else 80 if max_length_line > 80 else 10
self._print_func(self._dividing_line.get_full_dynamic_line(max_length_line, self._override_system_messages))
print(text.strip('\n'))
self._print_func(self._dividing_line.get_full_dynamic_line(max_length_line, self._override_system_messages))
def _is_exit_command(self, command: InputCommand) -> bool:
"""
Private. Checks if the given command is an exit command
:param command: command to check
:return: is it an exit command or not as bool
"""
if self._ignore_command_register:
if command.get_trigger().lower() == self._exit_command.get_trigger().lower():
return True
elif command.get_trigger().lower() in [x.lower() for x in self._exit_command.get_aliases()]:
return True
else:
if command.get_trigger() == self._exit_command.get_trigger():
return True
elif command.get_trigger() in self._exit_command.get_aliases():
return True
return False
def _is_unknown_command(self, command: InputCommand) -> bool:
"""
Private. Checks if the given command is an unknown command
:param command: command to check
:return: is it an unknown command or not as bool
"""
input_command_trigger = command.get_trigger()
if self._ignore_command_register:
if input_command_trigger.lower() in self._all_registered_triggers_in_lower:
return False
else:
if input_command_trigger in self._all_registered_triggers_in_default_case:
return False
return True
def _error_handler(self, error: BaseInputCommandException, raw_command: str) -> None:
"""
Private. Handles parsing errors of the entered command
:param error: error being handled
:param raw_command: the raw input command
:return: None
"""
match error:
case UnprocessedInputFlagException():
self._invalid_input_flags_handler(raw_command)
case RepeatedInputFlagsException():
self._repeated_input_flags_handler(raw_command)
case EmptyInputCommandException():
self._empty_input_command_handler()
def _setup_system_router(self) -> None:
"""
Private. Sets up system router
:return: None
"""
system_router.set_title(self._system_router_title)
@system_router.command(self._exit_command)
def exit_command(response: Response) -> None:
self._exit_command_handler(response)
if system_router not in self._registered_routers.get_registered_routers():
system_router.set_command_register_ignore(self._ignore_command_register)
self._registered_routers.add_registered_router(system_router)
def _most_similar_command(self, unknown_command: str) -> str | None:
all_commands = self._all_registered_triggers_in_lower if self._ignore_command_register else self._all_registered_triggers_in_default_case
matches: list[str] | list = sorted(cmd for cmd in all_commands if cmd.startswith(unknown_command))
if not matches:
matches: list[str] | list = sorted(cmd for cmd in all_commands if unknown_command.startswith(cmd))
if len(matches) == 1:
return matches[0]
elif len(matches) > 1:
return sorted(matches, key=lambda cmd: len(cmd))[0]
else:
return None
def _setup_default_view(self) -> None:
"""
Private. Sets up default app view
:return: None
"""
self._prompt = '[italic dim bold]What do you want to do?\n'
self._initial_message = f'\n[bold red]{text2art(self._initial_message, font="tarty1")}\n'
self._farewell_message = (f'[bold red]\n{text2art(f"\n{self._farewell_message}\n", font="chanky")}[/bold red]\n'
f'[red i]github.com/koloideal/Argenta[/red i] | [red bold i]made by kolo[/red bold i]\n')
self._description_message_gen = lambda command, description: (f'[bold red]{escape("[" + command + "]")}[/bold red] '
f'[blue dim]*=*=*[/blue dim] '
f'[bold yellow italic]{escape(description)}')
self._invalid_input_flags_handler = lambda raw_command: self._print_func(f'[red bold]Incorrect flag syntax: {escape(raw_command)}')
self._repeated_input_flags_handler = lambda raw_command: self._print_func(f'[red bold]Repeated input flags: {escape(raw_command)}')
self._empty_input_command_handler = lambda: self._print_func('[red bold]Empty input command')
def unknown_command_handler(command: InputCommand) -> None:
cmd_trg: str = command.get_trigger()
mst_sim_cmd: str | None = self._most_similar_command(cmd_trg)
first_part_of_text = f"[red]Unknown command:[/red] [blue]{escape(cmd_trg)}[/blue]"
second_part_of_text = ('[red], most similar:[/red] ' + ('[blue]' + mst_sim_cmd + '[/blue]')) if mst_sim_cmd else ''
self._print_func(first_part_of_text + second_part_of_text)
self._unknown_command_handler = unknown_command_handler
def _pre_cycle_setup(self) -> None:
"""
Private. Configures various aspects of the application before the start of the cycle
:return: None
"""
self._setup_system_router()
for router_entity in self._registered_routers:
self._all_registered_triggers_in_default_case.extend(router_entity.get_triggers())
self._all_registered_triggers_in_default_case.extend(router_entity.get_aliases())
self._all_registered_triggers_in_lower.extend([x.lower() for x in router_entity.get_triggers()])
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)
if not self._override_system_messages:
self._setup_default_view()
self._print_func(self._initial_message)
for message in self._messages_on_startup:
self._print_func(message)
if self._messages_on_startup:
print('\n')
if not self._repeat_command_groups_description:
self._print_command_group_description()
class App(BaseApp):
def __init__(self,
prompt: str = 'What do you want to do?\n',
initial_message: str = '\nArgenta\n',
farewell_message: str = '\nSee you\n',
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:
"""
Public. The essence of the application itself.
Configures and manages all aspects of the behavior and presentation of the user interacting with the user
:param prompt: displayed before entering the command
:param initial_message: displayed at the start of the app
:param farewell_message: displayed at the end of the app
:param exit_command: the entity of the command that will be terminated when entered
:param system_router_title: system router title
:param ignore_command_register: whether to ignore the case of the entered commands
:param dividing_line: the entity of the dividing line
:param repeat_command_groups: whether to repeat the available commands and their description
:param override_system_messages: whether to redefine the default formatting of system messages
:param autocompleter: the entity of the autocompleter
:param print_func: system messages text output function
:return: None
"""
super().__init__(prompt=prompt,
initial_message=initial_message,
farewell_message=farewell_message,
exit_command=exit_command,
system_router_title=system_router_title,
ignore_command_register=ignore_command_register,
dividing_line=dividing_line,
repeat_command_groups=repeat_command_groups,
override_system_messages=override_system_messages,
autocompleter=autocompleter,
print_func=print_func)
def run_polling(self) -> None:
"""
Private. Starts the user input processing cycle
:return: None
"""
self._pre_cycle_setup()
while True:
if self._repeat_command_groups_description:
self._print_command_group_description()
raw_command: str = Console().input(self._prompt)
try:
input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
except BaseInputCommandException as error:
with redirect_stdout(io.StringIO()) as f:
self._error_handler(error, raw_command)
res: str = f.getvalue()
self._print_framed_text(res)
continue
if self._is_exit_command(input_command):
system_router.finds_appropriate_handler(input_command)
self._autocompleter.exit_setup()
return
if self._is_unknown_command(input_command):
with redirect_stdout(io.StringIO()) as f:
self._unknown_command_handler(input_command)
res: str = f.getvalue()
self._print_framed_text(res)
continue
with redirect_stdout(io.StringIO()) as f:
for registered_router in self._registered_routers:
registered_router.finds_appropriate_handler(input_command)
res: str = f.getvalue()
self._print_framed_text(res)
def include_router(self, router: Router) -> None:
"""
Public. Registers the router in the application
:param router: registered router
:return: None
"""
router.set_command_register_ignore(self._ignore_command_register)
self._registered_routers.add_registered_router(router)
def include_routers(self, *routers: Router) -> None:
"""
Public. Registers the routers in the application
:param routers: registered routers
:return: None
"""
for router in routers:
self.include_router(router)
def add_message_on_startup(self, message: str) -> None:
"""
Public. Adds a message that will be displayed when the application is launched
:param message: the message being added
:return: None
"""
self._messages_on_startup.append(message)
@@ -0,0 +1,34 @@
from typing import Iterator
from argenta.router import Router
class RegisteredRouters:
def __init__(self, registered_routers: list[Router] = None) -> None:
"""
Private. Combines registered routers
:param registered_routers: list of the registered routers
:return: None
"""
self._registered_routers = registered_routers if registered_routers else []
def get_registered_routers(self) -> list[Router]:
"""
Private. Returns the registered routers
:return: registered routers as list[Router]
"""
return self._registered_routers
def add_registered_router(self, router: Router) -> None:
"""
Private. Adds a new registered router
:param router: registered router
:return: None
"""
self._registered_routers.append(router)
def __iter__(self) -> Iterator[Router]:
return iter(self._registered_routers)
def __next__(self) -> Router:
return next(iter(self._registered_routers))
@@ -1,16 +1,25 @@
from argenta.command.flag.models import InputFlag, Flag from argenta.command.flag.models import Flag, InputFlag
class BaseInputCommandException(Exception): class BaseInputCommandException(Exception):
"""
Private. Base exception class for all exceptions raised when parse input command
"""
pass pass
class UnprocessedInputFlagException(BaseInputCommandException): class UnprocessedInputFlagException(BaseInputCommandException):
"""
Private. Raised when an unprocessed input flag is detected
"""
def __str__(self): def __str__(self):
return "Unprocessed Input Flags" return "Unprocessed Input Flags"
class RepeatedInputFlagsException(BaseInputCommandException): class RepeatedInputFlagsException(BaseInputCommandException):
"""
Private. Raised when repeated input flags are detected
"""
def __init__(self, flag: Flag | InputFlag): def __init__(self, flag: Flag | InputFlag):
self.flag = flag self.flag = flag
def __str__(self): def __str__(self):
@@ -19,5 +28,8 @@ class RepeatedInputFlagsException(BaseInputCommandException):
class EmptyInputCommandException(BaseInputCommandException): class EmptyInputCommandException(BaseInputCommandException):
"""
Private. Raised when an empty input command is detected
"""
def __str__(self): def __str__(self):
return "Input Command is empty" return "Input Command is empty"
+4
View File
@@ -0,0 +1,4 @@
__all__ = ["Flag", "InputFlag"]
from argenta.command.flag.models import Flag, InputFlag
@@ -4,18 +4,21 @@ import re
@dataclass @dataclass
class PredeterminedFlags: class PredefinedFlags:
"""
Public. A dataclass with predefined flags and most frequently used flags for quick use
"""
HELP = Flag(name='help', possible_values=False) HELP = Flag(name='help', possible_values=False)
SHORT_HELP = Flag(name='h', prefix='-', possible_values=False) SHORT_HELP = Flag(name='H', prefix='-', possible_values=False)
INFO = Flag(name='info', possible_values=False) INFO = Flag(name='info', possible_values=False)
SHORT_INFO = Flag(name='i', prefix='-', possible_values=False) SHORT_INFO = Flag(name='I', prefix='-', possible_values=False)
ALL = Flag(name='all', possible_values=False) ALL = Flag(name='all', possible_values=False)
SHORT_ALL = Flag(name='a', prefix='-', possible_values=False) SHORT_ALL = Flag(name='A', prefix='-', possible_values=False)
HOST = Flag(name='host', possible_values=re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')) HOST = Flag(name='host', possible_values=re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'))
SHORT_HOST = Flag(name='h', prefix='-', possible_values=re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')) SHORT_HOST = Flag(name='H', prefix='-', possible_values=re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'))
PORT = Flag(name='port', possible_values=re.compile(r'^\d{1,5}$')) PORT = Flag(name='port', possible_values=re.compile(r'^\d{1,5}$'))
SHORT_PORT = Flag(name='p', prefix='-', possible_values=re.compile(r'^\d{1,5}$')) SHORT_PORT = Flag(name='P', prefix='-', possible_values=re.compile(r'^\d{1,5}$'))
+118
View File
@@ -0,0 +1,118 @@
from typing import Literal, Pattern
class BaseFlag:
def __init__(self, name: str,
prefix: Literal['-', '--', '---'] = '--') -> None:
"""
Private. Base class for flags
:param name: the name of the flag
:param prefix: the prefix of the flag
:return: None
"""
self._name = name
self._prefix = prefix
def get_string_entity(self) -> str:
"""
Public. Returns a string representation of the flag
:return: string representation of the flag as str
"""
string_entity: str = self._prefix + self._name
return string_entity
def get_name(self) -> str:
"""
Public. Returns the name of the flag
:return: the name of the flag as str
"""
return self._name
def get_prefix(self) -> str:
"""
Public. Returns the prefix of the flag
:return: the prefix of the flag as str
"""
return self._prefix
def __eq__(self, other) -> bool:
return self.get_string_entity() == other.get_string_entity()
class Flag(BaseFlag):
def __init__(self, name: str,
prefix: Literal['-', '--', '---'] = '--',
possible_values: list[str] | Pattern[str] | False = True) -> None:
"""
Public. The entity of the flag being registered for subsequent processing
:param name: The name of the flag
:param prefix: The prefix of the flag
:param possible_values: The possible values of the flag, if False then the flag cannot have a value
:return: None
"""
super().__init__(name, prefix)
self.possible_values = possible_values
def validate_input_flag_value(self, input_flag_value: str | None):
"""
Private. Validates the input flag value
:param input_flag_value: The input flag value to validate
:return: whether the entered flag is valid as bool
"""
if self.possible_values is False:
if input_flag_value is None:
return True
else:
return False
elif isinstance(self.possible_values, Pattern):
if isinstance(input_flag_value, str):
is_valid = bool(self.possible_values.match(input_flag_value))
if bool(is_valid):
return True
else:
return False
else:
return False
elif isinstance(self.possible_values, list):
if input_flag_value in self.possible_values:
return True
else:
return False
else:
return True
class InputFlag(BaseFlag):
def __init__(self, name: str,
prefix: Literal['-', '--', '---'] = '--',
value: str = None):
"""
Public. The entity of the flag of the entered command
:param name: the name of the input flag
:param prefix: the prefix of the input flag
:param value: the value of the input flag
:return: None
"""
super().__init__(name, prefix)
self._flag_value = value
def get_value(self) -> str | None:
"""
Public. Returns the value of the flag
:return: the value of the flag as str
"""
return self._flag_value
def set_value(self, value):
"""
Private. Sets the value of the flag
:param value: the fag value to set
:return: None
"""
self._flag_value = value
def __eq__(self, other) -> bool:
return self.get_string_entity() == other.get_string_entity() and self.get_value() == other.get_value()
+10
View File
@@ -0,0 +1,10 @@
__all__ = ["Flags", "InputFlags",
"UndefinedInputFlags",
"InvalidValueInputFlags",
"ValidInputFlags"]
from argenta.command.flags.models import (Flags, InputFlags,
UndefinedInputFlags,
InvalidValueInputFlags,
ValidInputFlags)
+87
View File
@@ -0,0 +1,87 @@
from argenta.command.flag.models import InputFlag, Flag
from typing import Generic, TypeVar
FlagType = TypeVar('FlagType')
class BaseFlags(Generic[FlagType]):
def __init__(self, *flags: FlagType):
"""
Public. A model that combines the registered flags
:param flags: the flags that will be registered
:return: None
"""
self._flags = flags if flags else []
def get_flags(self) -> list[FlagType]:
"""
Public. Returns a list of flags
:return: list of flags as list[FlagType]
"""
return self._flags
def add_flag(self, flag: FlagType):
"""
Public. Adds a flag to the list of flags
:param flag: flag to add
:return: None
"""
self._flags.append(flag)
def add_flags(self, flags: list[FlagType]):
"""
Public. Adds a list of flags to the list of flags
:param flags: list of flags to add
:return: None
"""
self._flags.extend(flags)
def get_flag(self, name: str) -> FlagType | None:
"""
Public. Returns the flag entity by its name or None if not found
:param name: the name of the flag to get
:return: entity of the flag or None
"""
if name in [flag.get_name() for flag in self._flags]:
return list(filter(lambda flag: flag.get_name() == name, self._flags))[0]
else:
return None
def __iter__(self):
return iter(self._flags)
def __next__(self):
return next(iter(self))
def __getitem__(self, item):
return self._flags[item]
def __bool__(self):
return bool(self._flags)
def __eq__(self, other):
if len(self.get_flags()) != len(other.get_flags()):
return False
else:
for flag, other_flag in zip(self.get_flags(), other.get_flags()):
if not flag == other_flag:
return False
return True
class Flags(BaseFlags[Flag]): pass
class InputFlags(BaseFlags[InputFlag]): pass
class ValidInputFlags(InputFlags): pass
class UndefinedInputFlags(InputFlags): pass
class InvalidValueInputFlags(InputFlags): pass
@@ -1,4 +1,5 @@
from argenta.command.flag.models import Flag, InputFlag, Flags, InputFlags from argenta.command.flag.models import Flag, InputFlag
from argenta.command.flags.models import InputFlags, Flags
from argenta.command.exceptions import (UnprocessedInputFlagException, from argenta.command.exceptions import (UnprocessedInputFlagException,
RepeatedInputFlagsException, RepeatedInputFlagsException,
EmptyInputCommandException) EmptyInputCommandException)
@@ -9,10 +10,18 @@ InputCommandType = TypeVar('InputCommandType')
class BaseCommand: class BaseCommand:
def __init__(self, trigger: str): def __init__(self, trigger: str) -> None:
"""
Private. Base class for all commands
:param trigger: A string trigger, which, when entered by the user, indicates that the input corresponds to the command
"""
self._trigger = trigger self._trigger = trigger
def get_trigger(self) -> str: def get_trigger(self) -> str:
"""
Public. Returns the trigger of the command
:return: the trigger of the command as str
"""
return self._trigger return self._trigger
@@ -21,34 +30,65 @@ class Command(BaseCommand):
description: str = None, description: str = None,
flags: Flag | Flags = None, flags: Flag | Flags = None,
aliases: list[str] = None): aliases: list[str] = None):
"""
Public. The command that can and should be registered in the Router
:param trigger: A string trigger, which, when entered by the user, indicates that the input corresponds to the command
:param description: the description of the command
:param flags: processed commands
:param aliases: string synonyms for the main trigger
"""
super().__init__(trigger) super().__init__(trigger)
self._registered_flags: Flags = flags if isinstance(flags, Flags) else Flags(flags) if isinstance(flags, Flag) else Flags() self._registered_flags: Flags = flags if isinstance(flags, Flags) else Flags(flags) if isinstance(flags, Flag) else Flags()
self._description = f'Description for "{self._trigger}" command' if not description else description self._description = f'Very useful command' if not description else description
self._aliases = aliases self._aliases = aliases if isinstance(aliases, list) else []
def get_registered_flags(self) -> Flags: def get_registered_flags(self) -> Flags:
"""
Private. Returns the registered flags
:return: the registered flags as Flags
"""
return self._registered_flags return self._registered_flags
def get_aliases(self) -> list[str] | None: def get_aliases(self) -> list[str] | list:
"""
Public. Returns the aliases of the command
:return: the aliases of the command as list[str] | list
"""
return self._aliases return self._aliases
def validate_input_flag(self, flag: InputFlag): def validate_input_flag(self, flag: InputFlag) -> Literal['Undefined', 'Valid', 'Invalid']:
"""
Private. Validates the input flag
:param flag: input flag for validation
:return: is input flag valid as bool
"""
registered_flags: Flags | None = self.get_registered_flags() registered_flags: Flags | None = self.get_registered_flags()
if registered_flags: if registered_flags:
if isinstance(registered_flags, Flag): if isinstance(registered_flags, Flag):
if registered_flags.get_string_entity() == flag.get_string_entity(): if registered_flags.get_string_entity() == flag.get_string_entity():
is_valid = registered_flags.validate_input_flag_value(flag.get_value()) is_valid = registered_flags.validate_input_flag_value(flag.get_value())
if is_valid: if is_valid:
return True return 'Valid'
else:
return 'Invalid'
else:
return 'Undefined'
else: else:
for registered_flag in registered_flags: for registered_flag in registered_flags:
if registered_flag.get_string_entity() == flag.get_string_entity(): if registered_flag.get_string_entity() == flag.get_string_entity():
is_valid = registered_flag.validate_input_flag_value(flag.get_value()) is_valid = registered_flag.validate_input_flag_value(flag.get_value())
if is_valid: if is_valid:
return True return 'Valid'
return False else:
return 'Invalid'
return 'Undefined'
return 'Undefined'
def get_description(self) -> str: def get_description(self) -> str:
"""
Private. Returns the description of the command
:return: the description of the command as str
"""
return self._description return self._description
@@ -56,18 +96,38 @@ class Command(BaseCommand):
class InputCommand(BaseCommand, Generic[InputCommandType]): class InputCommand(BaseCommand, Generic[InputCommandType]):
def __init__(self, trigger: str, def __init__(self, trigger: str,
input_flags: InputFlag | InputFlags = None): input_flags: InputFlag | InputFlags = None):
"""
Private. The model of the input command, after parsing
:param trigger:the trigger of the command
:param input_flags: the input flags
:return: None
"""
super().__init__(trigger) super().__init__(trigger)
self._input_flags: InputFlags = input_flags if isinstance(input_flags, InputFlags) else InputFlags(input_flags) if isinstance(input_flags, InputFlag) else InputFlags() self._input_flags: InputFlags = input_flags if isinstance(input_flags, InputFlags) else InputFlags(input_flags) if isinstance(input_flags, InputFlag) else InputFlags()
def _set_input_flags(self, input_flags: InputFlags): def _set_input_flags(self, input_flags: InputFlags) -> None:
"""
Private. Sets the input flags
:param input_flags: the input flags to set
:return: None
"""
self._input_flags = input_flags self._input_flags = input_flags
def get_input_flags(self) -> InputFlags: def get_input_flags(self) -> InputFlags:
"""
Private. Returns the input flags
:return: the input flags as InputFlags
"""
return self._input_flags return self._input_flags
@staticmethod @staticmethod
def parse(raw_command: str) -> InputCommandType: def parse(raw_command: str) -> InputCommandType:
"""
Private. Parse the raw input command
:param raw_command: raw input command
:return: model of the input command, after parsing as InputCommand
"""
if not raw_command: if not raw_command:
raise EmptyInputCommandException() raise EmptyInputCommandException()
@@ -79,11 +139,11 @@ class InputCommand(BaseCommand, Generic[InputCommandType]):
for k, _ in enumerate(list_of_tokens): for k, _ in enumerate(list_of_tokens):
if _.startswith('-'): if _.startswith('-'):
if current_flag_name or len(_) < 2 or len(_[:_.rfind('-')]) > 3: if len(_) < 2 or len(_[:_.rfind('-')]) > 3:
raise UnprocessedInputFlagException() raise UnprocessedInputFlagException()
current_flag_name = _ current_flag_name = _
else: else:
if not current_flag_name: if not current_flag_name or current_flag_value:
raise UnprocessedInputFlagException() raise UnprocessedInputFlagException()
current_flag_value = _ current_flag_value = _
@@ -92,7 +152,7 @@ class InputCommand(BaseCommand, Generic[InputCommandType]):
if not list_of_tokens[k+1].startswith('-'): if not list_of_tokens[k+1].startswith('-'):
continue continue
input_flag = InputFlag(name=current_flag_name[current_flag_name.rfind('-')+1:], input_flag = InputFlag(name=current_flag_name[current_flag_name.rfind('-') + 1:],
prefix=cast(Literal['-', '--', '---'], prefix=cast(Literal['-', '--', '---'],
current_flag_name[:current_flag_name.rfind('-')+1]), current_flag_name[:current_flag_name.rfind('-')+1]),
value=current_flag_value) value=current_flag_value)
+4
View File
@@ -0,0 +1,4 @@
__all__ = ["Orchestrator"]
from argenta.orchestrator.entity import Orchestrator
@@ -0,0 +1,4 @@
__all__ = ["ArgParser"]
from argenta.orchestrator.argparser.entity import ArgParser
@@ -0,0 +1,6 @@
__all__ = ["BooleanArgument", "PositionalArgument", "OptionalArgument"]
from argenta.orchestrator.argparser.arguments.models import (BooleanArgument,
PositionalArgument,
OptionalArgument)
@@ -0,0 +1,55 @@
from abc import ABC, abstractmethod
from typing import Literal
class BaseArgument(ABC):
"""
Private. Base class for all arguments
"""
@abstractmethod
def get_string_entity(self) -> str:
"""
Public. Returns the string representation of the argument
:return: the string representation as a str
"""
pass
class PositionalArgument(BaseArgument):
def __init__(self, name: str):
"""
Public. Required argument at startup
:param name: name of the argument, must not start with minus (-)
"""
self.name = name
def get_string_entity(self):
return self.name
class OptionalArgument(BaseArgument):
def __init__(self, name: str, prefix: Literal['-', '--', '---'] = '--'):
"""
Public. Optional argument, must have the value
:param name: name of the argument
:param prefix: prefix of the argument
"""
self.name = name
self.prefix = prefix
def get_string_entity(self):
return self.prefix + self.name
class BooleanArgument(BaseArgument):
def __init__(self, name: str, prefix: Literal['-', '--', '---'] = '--'):
"""
Public. Boolean argument, does not require a value
:param name: name of the argument
:param prefix: prefix of the argument
"""
self.name = name
self.prefix = prefix
def get_string_entity(self):
return self.prefix + self.name
@@ -0,0 +1,49 @@
from argparse import ArgumentParser
from argenta.orchestrator.argparser.arguments.models import (BooleanArgument,
OptionalArgument,
PositionalArgument)
class ArgParser:
def __init__(self,
processed_args: list[PositionalArgument | OptionalArgument | BooleanArgument],
name: str = 'Argenta',
description: str = 'Argenta available arguments',
epilog: str = 'github.com/koloideal/Argenta | made by kolo') -> None:
"""
Public. Cmd argument parser and configurator at startup
:param name: the name of the ArgParse instance
:param description: the description of the ArgParse instance
:param epilog: the epilog of the ArgParse instance
:param processed_args: registered and processed arguments
"""
self.name = name
self.description = description
self.epilog = epilog
self.entity: ArgumentParser = ArgumentParser(prog=name, description=description, epilog=epilog)
self.args: list[PositionalArgument | OptionalArgument | BooleanArgument] | None = processed_args
def set_args(self, *args: PositionalArgument | OptionalArgument | BooleanArgument) -> None:
"""
Public. Sets the arguments to be processed
:param args: processed arguments
:return: None
"""
self.args.extend(args)
def register_args(self) -> None:
"""
Private. Registers initialized command line arguments
:return: None
"""
if not self.args:
return
for arg in self.args:
if type(arg) is PositionalArgument:
self.entity.add_argument(arg.get_string_entity())
elif type(arg) is OptionalArgument:
self.entity.add_argument(arg.get_string_entity())
elif type(arg) is BooleanArgument:
self.entity.add_argument(arg.get_string_entity(), action='store_true')
+36
View File
@@ -0,0 +1,36 @@
from argparse import Namespace
from argenta.app import App
from argenta.orchestrator.argparser import ArgParser
class Orchestrator:
def __init__(self, arg_parser: ArgParser = False):
"""
Public. An orchestrator and configurator that defines the behavior of an integrated system, one level higher than the App
:param arg_parser: Cmd argument parser and configurator at startup
:return: None
"""
self.arg_parser: ArgParser | False = arg_parser
if arg_parser:
self.arg_parser.register_args()
@staticmethod
def start_polling(app: App) -> None:
"""
Public. Starting the user input processing cycle
:param app: a running application
:return: None
"""
app.run_polling()
def get_input_args(self) -> Namespace | None:
"""
Public. Returns the arguments parsed
:return: None
"""
if self.arg_parser:
return self.arg_parser.entity.parse_args()
else:
return None
+5
View File
@@ -0,0 +1,5 @@
__all__ = ["Response", "Status"]
from argenta.response.entity import Response
from argenta.response.status import Status
+27
View File
@@ -0,0 +1,27 @@
from argenta.response.status import Status
from argenta.command.flags import (ValidInputFlags,
UndefinedInputFlags,
InvalidValueInputFlags)
class Response:
__slots__ = ('status',
'valid_flags',
'undefined_flags',
'invalid_value_flags')
def __init__(self, status: Status = None,
valid_flags: ValidInputFlags = ValidInputFlags(),
undefined_flags: UndefinedInputFlags = UndefinedInputFlags(),
invalid_value_flags: InvalidValueInputFlags = InvalidValueInputFlags()):
"""
Public. The entity of the user input sent to the handler
:param status: the status of the response
:param valid_flags: valid input flags
:param undefined_flags: undefined input flags
:param invalid_value_flags: input flags with invalid values
"""
self.status = status
self.valid_flags = valid_flags
self.undefined_flags = undefined_flags
self.invalid_value_flags = invalid_value_flags
+9
View File
@@ -0,0 +1,9 @@
from enum import Enum
class Status(Enum):
ALL_FLAGS_VALID = 'ALL_FLAGS_VALID'
UNDEFINED_FLAGS = 'UNDEFINED_FLAGS'
INVALID_VALUE_FLAGS = 'INVALID_VALUE_FLAGS'
UNDEFINED_AND_INVALID_FLAGS = 'UNDEFINED_AND_INVALID_FLAGS'
@@ -0,0 +1,67 @@
from typing import Callable, Iterator
from argenta.command import Command
from argenta.response import Response
class CommandHandler:
def __init__(self, handler: Callable[[Response], None], handled_command: Command):
"""
Private. Entity of the model linking the handler and the command being processed
:param handler: the handler being called
:param handled_command: the command being processed
"""
self._handler = handler
self._handled_command = handled_command
def handling(self, response: Response) -> None:
"""
Private. Direct processing of an input command
:param response: the entity of response: various groups of flags and status of response
:return: None
"""
self._handler(response)
def get_handler(self) -> Callable[[Response], None]:
"""
Private. Returns the handler being called
:return: the handler being called as Callable[[Response], None]
"""
return self._handler
def get_handled_command(self) -> Command:
"""
Private. Returns the command being processed
:return: the command being processed as Command
"""
return self._handled_command
class CommandHandlers:
def __init__(self, command_handlers: list[CommandHandler] = None):
"""
Private. The model that unites all CommandHandler of the routers
:param command_handlers: list of CommandHandlers for register
"""
self.command_handlers = command_handlers if command_handlers else []
def get_handlers(self) -> list[CommandHandler]:
"""
Private. Returns the list of CommandHandlers
:return: the list of CommandHandlers as list[CommandHandler]
"""
return self.command_handlers
def add_handler(self, command_handler: CommandHandler) -> None:
"""
Private. Adds a CommandHandler to the list of CommandHandlers
:param command_handler: CommandHandler to be added
:return: None
"""
self.command_handlers.append(command_handler)
def __iter__(self) -> Iterator[CommandHandler]:
return iter(self.command_handlers)
def __next__(self) -> CommandHandler:
return next(iter(self.command_handlers))
+4
View File
@@ -0,0 +1,4 @@
from argenta.router import Router
system_router = Router(title='System points:')
+238
View File
@@ -0,0 +1,238 @@
from typing import Callable, Literal, Type
from inspect import getfullargspec, get_annotations, getsourcefile, getsourcelines
from rich.console import Console
from argenta.command import Command
from argenta.command.models import InputCommand
from argenta.response import Response, Status
from argenta.router.command_handler.entity import CommandHandlers, CommandHandler
from argenta.command.flags.models import (Flags, InputFlags,
UndefinedInputFlags,
ValidInputFlags,
InvalidValueInputFlags)
from argenta.router.exceptions import (RepeatedFlagNameException,
TooManyTransferredArgsException,
RequiredArgumentNotPassedException,
TriggerContainSpacesException)
class Router:
def __init__(self, title: str = None):
"""
Public. Directly configures and manages handlers
:param title: the title of the router, displayed when displaying the available commands
:return: None
"""
self._title = title
self._command_handlers: CommandHandlers = CommandHandlers()
self._ignore_command_register: bool = False
def command(self, command: Command | str) -> Callable:
"""
Public. Registers handler
:param command: Registered command
:return: decorated handler as Callable
"""
self._validate_command(command)
if isinstance(command, str):
command = Command(command)
def command_decorator(func):
Router._validate_func_args(func)
self._command_handlers.add_handler(CommandHandler(func, command))
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return command_decorator
def finds_appropriate_handler(self, input_command: InputCommand) -> None:
"""
Private. Finds the appropriate handler for given input command and passes control to it
:param input_command: input command as InputCommand
:return: None
"""
input_command_name: str = input_command.get_trigger()
input_command_flags: InputFlags = input_command.get_input_flags()
for command_handler in self._command_handlers:
handle_command = command_handler.get_handled_command()
if input_command_name.lower() == handle_command.get_trigger().lower():
self.process_input_command(input_command_flags, command_handler)
if input_command_name.lower() in handle_command.get_aliases():
self.process_input_command(input_command_flags, command_handler)
def process_input_command(self, input_command_flags: InputFlags, command_handler: CommandHandler) -> None:
"""
Private. Processes input command with the appropriate handler
:param input_command_flags: input command flags as InputFlags
:param command_handler: command handler for input command as CommandHandler
:return: None
"""
handle_command = command_handler.get_handled_command()
response: Response = Response()
if handle_command.get_registered_flags().get_flags():
if input_command_flags.get_flags():
response: Response = self._structuring_input_flags(handle_command, input_command_flags)
command_handler.handling(response)
else:
response.status = Status.ALL_FLAGS_VALID
command_handler.handling(response)
else:
if input_command_flags.get_flags():
response.status = Status.UNDEFINED_FLAGS
response.undefined_flags = UndefinedInputFlags()
response.undefined_flags.add_flags(input_command_flags.get_flags())
command_handler.handling(response)
else:
response.status = Status.ALL_FLAGS_VALID
command_handler.handling(response)
@staticmethod
def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response:
"""
Private. Validates flags of input command
:param handled_command: entity of the handled command
:param input_flags:
:return: entity of response as Response
"""
valid_input_flags: ValidInputFlags = ValidInputFlags()
invalid_value_input_flags: InvalidValueInputFlags = InvalidValueInputFlags()
undefined_input_flags: UndefinedInputFlags = UndefinedInputFlags()
for flag in input_flags:
flag_status: Literal['Undefined', 'Valid', 'Invalid'] = handled_command.validate_input_flag(flag)
match flag_status:
case 'Valid':
valid_input_flags.add_flag(flag)
case 'Undefined':
undefined_input_flags.add_flag(flag)
case 'Invalid':
invalid_value_input_flags.add_flag(flag)
if not invalid_value_input_flags.get_flags() and not undefined_input_flags.get_flags():
status = Status.ALL_FLAGS_VALID
elif invalid_value_input_flags.get_flags() and not undefined_input_flags.get_flags():
status = Status.INVALID_VALUE_FLAGS
elif not invalid_value_input_flags.get_flags() and undefined_input_flags.get_flags():
status = Status.UNDEFINED_FLAGS
else:
status = Status.UNDEFINED_AND_INVALID_FLAGS
return Response(invalid_value_flags=invalid_value_input_flags,
valid_flags=valid_input_flags,
status=status,
undefined_flags=undefined_input_flags)
@staticmethod
def _validate_command(command: Command | str) -> None:
"""
Private. Validates the command registered in handler
:param command: validated command
:return: None if command is valid else raise exception
"""
match type(command).__name__:
case 'Command':
command_name: str = command.get_trigger()
if command_name.find(' ') != -1:
raise TriggerContainSpacesException()
flags: Flags = command.get_registered_flags()
if flags:
flags_name: list = [x.get_string_entity().lower() for x in flags]
if len(set(flags_name)) < len(flags_name):
raise RepeatedFlagNameException()
case 'str':
if command.find(' ') != -1:
raise TriggerContainSpacesException()
@staticmethod
def _validate_func_args(func: Callable) -> None:
"""
Private. Validates the arguments of the handler
:param func: entity of the handler func
:return: None if func is valid else raise exception
"""
transferred_args = getfullargspec(func).args
if len(transferred_args) > 1:
raise TooManyTransferredArgsException()
elif len(transferred_args) == 0:
raise RequiredArgumentNotPassedException()
transferred_arg: str = transferred_args[0]
func_annotations: dict[str, Type] = get_annotations(func)
if arg_annotation := func_annotations.get(transferred_arg):
if arg_annotation is Response:
pass
else:
file_path: str = getsourcefile(func)
source_line: int = getsourcelines(func)[1]+1
fprint = Console().print
fprint(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' [i]but[/i] [bold blue]{arg_annotation}[/bold blue] [i]is specified[/i]\n', highlight=False)
def set_command_register_ignore(self, _: bool) -> None:
"""
Private. Sets the router behavior on the input commands register
:param _: is command register ignore
:return: None
"""
self._ignore_command_register = _
def get_triggers(self) -> list[str]:
"""
Public. Gets registered triggers
:return: registered in router triggers as list[str]
"""
all_triggers: list[str] = []
for command_handler in self._command_handlers:
all_triggers.append(command_handler.get_handled_command().get_trigger())
return all_triggers
def get_aliases(self) -> list[str]:
"""
Public. Gets registered aliases
:return: registered in router aliases as list[str]
"""
all_aliases: list[str] = []
for command_handler in self._command_handlers:
if command_handler.get_handled_command().get_aliases():
all_aliases.extend(command_handler.get_handled_command().get_aliases())
return all_aliases
def get_command_handlers(self) -> CommandHandlers:
"""
Private. Gets registered command handlers
:return: registered command handlers as CommandHandlers
"""
return self._command_handlers
def get_title(self) -> str | None:
"""
Public. Gets title of the router
:return: the title of the router as str or None
"""
return self._title
def set_title(self, title: str) -> None:
"""
Public. Sets the title of the router
:param title: title that will be setted
:return: None
"""
self._title = title
+30
View File
@@ -0,0 +1,30 @@
class RepeatedFlagNameException(Exception):
"""
Private. Raised when a repeated flag name is registered
"""
def __str__(self):
return "Repeated registered flag names in register command"
class TooManyTransferredArgsException(Exception):
"""
Private. Raised when too many arguments are passed
"""
def __str__(self):
return "Too many transferred arguments"
class RequiredArgumentNotPassedException(Exception):
"""
Private. Raised when a required argument is not passed
"""
def __str__(self):
return "Required argument not passed"
class TriggerContainSpacesException(Exception):
"""
Private. Raised when there is a space in the trigger being registered
"""
def __str__(self):
return "Command trigger cannot contain spaces"
@@ -1,32 +1,35 @@
import _io import _io
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
import unittest from unittest import TestCase
import io import io
import re import re
from argenta.app import App from argenta.app import App
from argenta.command import Command from argenta.command import Command
from argenta.router import Router from argenta.router import Router
from argenta.command.flag.models import Flags, InputFlags from argenta.command.flags.models import Flags
from argenta.command.flag.defaults import PredeterminedFlags from argenta.command.flag.defaults import PredefinedFlags
from argenta.orchestrator import Orchestrator
from argenta.response import Response
class TestSystemHandlerNormalWork(unittest.TestCase): class TestSystemHandlerNormalWork(TestCase):
@patch("builtins.input", side_effect=["help", "q"]) @patch("builtins.input", side_effect=["help", "q"])
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_incorrect_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_incorrect_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response: Response):
print('test command') print('test command')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.get_trigger()}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.get_trigger()}'))
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -37,9 +40,10 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_incorrect_command2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_incorrect_command2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response: Response):
print('test command') print('test command')
app = App(ignore_command_register=False, app = App(ignore_command_register=False,
@@ -47,7 +51,7 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.get_trigger()}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.get_trigger()}'))
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -58,74 +62,80 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_unregistered_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_unregistered_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response: Response):
print(f'test command') print(f'test command with undefined flag: {response.undefined_flags.get_flag('help').get_string_entity()}')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
self.assertIn('\nUndefined or incorrect input flag: --help\n', output) self.assertIn('\ntest command with undefined flag: --help\n', output)
@patch("builtins.input", side_effect=["test --port 22", "q"]) @patch("builtins.input", side_effect=["test --port 22", "q"])
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_unregistered_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_unregistered_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response: Response):
print('test command') flag = response.undefined_flags.get_flag("port")
print(f'test command with undefined flag with value: {flag.get_string_entity()} {flag.get_value()}')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
self.assertIn('\nUndefined or incorrect input flag: --port 22\n', output) self.assertIn('\ntest command with undefined flag with value: --port 22\n', output)
@patch("builtins.input", side_effect=["test --host 192.168.32.1 --port 132", "q"]) @patch("builtins.input", side_effect=["test --host 192.168.32.1 --port 132", "q"])
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_one_correct_flag_an_one_incorrect_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_one_correct_flag_an_one_incorrect_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
flags = Flags(PredeterminedFlags.HOST) orchestrator = Orchestrator()
flags = Flags(PredefinedFlags.HOST)
@router.command(Command('test', flags=flags)) @router.command(Command('test', flags=flags))
def test(args: InputFlags): def test(response: Response):
print(f'connecting to host {args.get_flag('host').get_value()}') flag = response.undefined_flags.get_flag("port")
print(f'connecting to host with flag: {flag.get_string_entity()} {flag.get_value()}')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
self.assertIn('\nUndefined or incorrect input flag: --port 132\n', output) self.assertIn('\nconnecting to host with flag: --port 132\n', output)
@patch("builtins.input", side_effect=["test", "some", "q"]) @patch("builtins.input", side_effect=["test", "some", "q"])
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_one_correct_command_and_one_incorrect_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_one_correct_command_and_one_incorrect_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response: Response):
print(f'test command') print(f'test command')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.get_trigger()}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.get_trigger()}'))
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -136,20 +146,21 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_two_correct_commands_and_one_incorrect_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_two_correct_commands_and_one_incorrect_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response: Response):
print(f'test command') print(f'test command')
@router.command(Command('more')) @router.command(Command('more'))
def test(): def test(response: Response):
print(f'more command') print(f'more command')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.get_trigger()}')) app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.get_trigger()}'))
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -160,16 +171,17 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_incorrect_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_incorrect_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response: Response):
print(f'test command') print(f'test command')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.set_invalid_input_flags_handler(lambda command: print(f'Incorrect flag syntax: "{command}"')) app.set_invalid_input_flags_handler(lambda command: print(f'Incorrect flag syntax: "{command}"'))
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -180,16 +192,17 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_empty_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_empty_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response: Response):
print(f'test command') print(f'test command')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.set_empty_command_handler(lambda: print('Empty input command')) app.set_empty_command_handler(lambda: print('Empty input command'))
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -200,16 +213,17 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_repeated_flags(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_repeated_flags(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test', flags=PredeterminedFlags.PORT)) @router.command(Command('test', flags=PredefinedFlags.PORT))
def test(args: InputFlags): def test(response: Response):
print('test command') print('test command')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"')) app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"'))
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -1,31 +1,35 @@
import _io import _io
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
import unittest from unittest import TestCase
import io import io
import re import re
from argenta.app import App from argenta.app import App
from argenta.command.models import Command from argenta.command import Command
from argenta.response import Response
from argenta.router import Router from argenta.router import Router
from argenta.command.flag.models import Flag, Flags, InputFlags from argenta.orchestrator import Orchestrator
from argenta.command.flag.defaults import PredeterminedFlags from argenta.command.flag import Flag
from argenta.command.flags import Flags
from argenta.command.flag.defaults import PredefinedFlags
class TestSystemHandlerNormalWork(unittest.TestCase): class TestSystemHandlerNormalWork(TestCase):
@patch("builtins.input", side_effect=["test", "q"]) @patch("builtins.input", side_effect=["test", "q"])
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response):
print('test command') print('test command')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -36,16 +40,17 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response):
print('test command') print('test command')
app = App(ignore_command_register=True, app = App(ignore_command_register=True,
override_system_messages=True, override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -56,16 +61,17 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_custom_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_custom_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
flag = Flag('help', '--', False) flag = Flag('help', '--', False)
@router.command(Command('test', flags=flag)) @router.command(Command('test', flags=flag))
def test(args: InputFlags): def test(response: Response):
print(f'\nhelp for {args.get_flag('help').get_name()} flag\n') print(f'\nhelp for {response.valid_flags.get_flag('help').get_name()} flag\n')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -75,57 +81,61 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_custom_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_custom_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
flag = Flag('port', '--', re.compile(r'^\d{1,5}$')) flag = Flag('port', '--', re.compile(r'^\d{1,5}$'))
@router.command(Command('test', flags=flag)) @router.command(Command('test', flags=flag))
def test(args: InputFlags): def test(response: Response):
print(f'flag value for {args.get_flag('port').get_name()} flag : {args.get_flag('port').get_value()}') input_flag = response.valid_flags.get_flag('port')
print(f'flag value for {input_flag.get_name()} flag : {input_flag.get_value()}')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
self.assertIn('\nflag value for port flag : 22\n', output) self.assertIn('\nflag value for port flag : 22\n', output)
@patch("builtins.input", side_effect=["test -h", "q"]) @patch("builtins.input", side_effect=["test -H", "q"])
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_default_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_default_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
flag = PredeterminedFlags.SHORT_HELP orchestrator = Orchestrator()
flag = PredefinedFlags.SHORT_HELP
@router.command(Command('test', flags=flag)) @router.command(Command('test', flags=flag))
def test(args: InputFlags): def test(response: Response):
print(f'help for {args.get_flag('h').get_name()} flag') print(f'help for {response.valid_flags.get_flag('H').get_name()} flag')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
self.assertIn('\nhelp for h flag\n', output) self.assertIn('\nhelp for H flag\n', output)
@patch("builtins.input", side_effect=["test --info", "q"]) @patch("builtins.input", side_effect=["test --info", "q"])
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_default_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_default_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
flag = PredeterminedFlags.INFO orchestrator = Orchestrator()
flag = PredefinedFlags.INFO
@router.command(Command('test', flags=flag)) @router.command(Command('test', flags=flag))
def test(args: InputFlags): def test(response: Response):
if args.get_flag('info'): if response.valid_flags.get_flag('info'):
print('info about test command') print('info about test command')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -136,16 +146,17 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_default_flag3(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_default_flag3(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
flag = PredeterminedFlags.HOST orchestrator = Orchestrator()
flag = PredefinedFlags.HOST
@router.command(Command('test', flags=flag)) @router.command(Command('test', flags=flag))
def test(args: InputFlags): def test(response: Response):
print(f'connecting to host {args[0].get_value()}') print(f'connecting to host {response.valid_flags.get_flag('host').get_value()}')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -156,16 +167,18 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_correct_command_with_two_flags(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_correct_command_with_two_flags(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
flags = Flags(PredeterminedFlags.HOST, PredeterminedFlags.PORT) orchestrator = Orchestrator()
flags = Flags(PredefinedFlags.HOST, PredefinedFlags.PORT)
@router.command(Command('test', flags=flags)) @router.command(Command('test', flags=flags))
def test(args: InputFlags): def test(response: Response):
print(f'connecting to host {args[0].get_value()} and port {args[1].get_value()}') valid_flags = response.valid_flags
print(f'connecting to host {valid_flags.get_flag('host').get_value()} and port {valid_flags.get_flag('port').get_value()}')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -176,19 +189,20 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_two_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_two_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response):
print(f'test command') print(f'test command')
@router.command(Command('some')) @router.command(Command('some'))
def test2(): def test2(response):
print(f'some command') print(f'some command')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
@@ -199,23 +213,24 @@ class TestSystemHandlerNormalWork(unittest.TestCase):
@patch("sys.stdout", new_callable=io.StringIO) @patch("sys.stdout", new_callable=io.StringIO)
def test_input_three_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): def test_input_three_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
router = Router() router = Router()
orchestrator = Orchestrator()
@router.command(Command('test')) @router.command(Command('test'))
def test(): def test(response):
print(f'test command') print(f'test command')
@router.command(Command('some')) @router.command(Command('some'))
def test(): def test(response):
print(f'some command') print(f'some command')
@router.command(Command('more')) @router.command(Command('more'))
def test(): def test(response):
print(f'more command') print(f'more command')
app = App(override_system_messages=True, app = App(override_system_messages=True,
print_func=print) print_func=print)
app.include_router(router) app.include_router(router)
app.start_polling() orchestrator.start_polling(app)
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
+73
View File
@@ -0,0 +1,73 @@
from argenta.command.models import InputCommand, Command
from argenta.app import App
import unittest
class MyTestCase(unittest.TestCase):
def test_is_exit_command1(self):
app = App()
self.assertEqual(app._is_exit_command(InputCommand('q')), True)
def test_is_exit_command5(self):
app = App()
self.assertEqual(app._is_exit_command(InputCommand('Q')), True)
def test_is_exit_command2(self):
app = App(ignore_command_register=False)
self.assertEqual(app._is_exit_command(InputCommand('q')), False)
def test_is_exit_command3(self):
app = App(exit_command=Command('quit'))
self.assertEqual(app._is_exit_command(InputCommand('quit')), True)
def test_is_exit_command4(self):
app = App(exit_command=Command('quit'))
self.assertEqual(app._is_exit_command(InputCommand('qUIt')), True)
def test_is_exit_command6(self):
app = App(ignore_command_register=False,
exit_command=Command('quit'))
self.assertEqual(app._is_exit_command(InputCommand('qUIt')), False)
def test_is_unknown_command1(self):
app = App()
app.set_unknown_command_handler(lambda command: None)
app._all_registered_triggers_in_lower = ['fr', 'tr', 'de']
self.assertEqual(app._is_unknown_command(InputCommand('fr')), False)
def test_is_unknown_command2(self):
app = App()
app.set_unknown_command_handler(lambda command: None)
app._all_registered_triggers_in_lower = ['fr', 'tr', 'de']
self.assertEqual(app._is_unknown_command(InputCommand('cr')), True)
def test_is_unknown_command3(self):
app = App(ignore_command_register=False)
app.set_unknown_command_handler(lambda command: None)
app._all_registered_triggers_in_default_case = ['Pr', 'tW', 'deQW']
self.assertEqual(app._is_unknown_command(InputCommand('pr')), True)
def test_is_unknown_command4(self):
app = App(ignore_command_register=False)
app.set_unknown_command_handler(lambda command: None)
app._all_registered_triggers_in_default_case = ['Pr', 'tW', 'deQW']
self.assertEqual(app._is_unknown_command(InputCommand('tW')), False)
+40 -1
View File
@@ -1,9 +1,12 @@
from argenta.command.models import InputCommand from argenta.command.flag import Flag, InputFlag
from argenta.command.flags import Flags
from argenta.command.models import InputCommand, Command
from argenta.command.exceptions import (UnprocessedInputFlagException, from argenta.command.exceptions import (UnprocessedInputFlagException,
RepeatedInputFlagsException, RepeatedInputFlagsException,
EmptyInputCommandException) EmptyInputCommandException)
import unittest import unittest
import re
class TestInputCommand(unittest.TestCase): class TestInputCommand(unittest.TestCase):
@@ -22,3 +25,39 @@ class TestInputCommand(unittest.TestCase):
with self.assertRaises(EmptyInputCommandException): with self.assertRaises(EmptyInputCommandException):
InputCommand.parse('') InputCommand.parse('')
def test_validate_valid_input_flag1(self):
command = Command('some', flags=Flag('test'))
self.assertEqual(command.validate_input_flag(InputFlag('test')), 'Valid')
def test_validate_valid_input_flag2(self):
command = Command('some', flags=Flags(Flag('test'), Flag('more')))
self.assertEqual(command.validate_input_flag(InputFlag('more')), 'Valid')
def test_validate_undefined_input_flag1(self):
command = Command('some', flags=Flag('test'))
self.assertEqual(command.validate_input_flag(InputFlag('more')), 'Undefined')
def test_validate_undefined_input_flag2(self):
command = Command('some', flags=Flags(Flag('test'), Flag('more')))
self.assertEqual(command.validate_input_flag(InputFlag('case')), 'Undefined')
def test_validate_undefined_input_flag3(self):
command = Command('some')
self.assertEqual(command.validate_input_flag(InputFlag('case')), 'Undefined')
def test_invalid_input_flag1(self):
command = Command('some', flags=Flag('test', possible_values=False))
self.assertEqual(command.validate_input_flag(InputFlag('test', value='example')), 'Invalid')
def test_invalid_input_flag2(self):
command = Command('some', flags=Flag('test', possible_values=['some', 'case']))
self.assertEqual(command.validate_input_flag(InputFlag('test', value='slay')), 'Invalid')
def test_invalid_input_flag3(self):
command = Command('some', flags=Flag('test', possible_values=re.compile(r'^ex\d{, 2}op$')))
self.assertEqual(command.validate_input_flag(InputFlag('test', value='example')), 'Invalid')
def test_isinstance_parse_correct_raw_command(self):
cmd = InputCommand.parse('ssh --host 192.168.0.3')
self.assertIsInstance(cmd, InputCommand)
+2 -2
View File
@@ -6,11 +6,11 @@ import unittest
class TestDividingLine(unittest.TestCase): class TestDividingLine(unittest.TestCase):
def test_get_static_dividing_line_full_line(self): def test_get_static_dividing_line_full_line(self):
line = StaticDividingLine('-') line = StaticDividingLine('-')
self.assertEqual(line.get_full_line().count('-'), 25) self.assertEqual(line.get_full_static_line(True).count('-'), 25)
def test_get_dynamic_dividing_line_full_line(self): def test_get_dynamic_dividing_line_full_line(self):
line = DynamicDividingLine() line = DynamicDividingLine()
self.assertEqual(line.get_full_line(20).count('-'), 20) self.assertEqual(line.get_full_dynamic_line(20, True).count('-'), 20)
def test_get_dividing_line_unit_part(self): def test_get_dividing_line_unit_part(self):
line = StaticDividingLine('') line = StaticDividingLine('')
+40 -1
View File
@@ -1,4 +1,5 @@
from argenta.command.flag.models import Flag, InputFlag from argenta.command.flag import Flag, InputFlag
from argenta.command.flags import InputFlags, Flags
import unittest import unittest
import re import re
@@ -68,6 +69,44 @@ class TestFlag(unittest.TestCase):
flag = Flag(name='test', possible_values=True) flag = Flag(name='test', possible_values=True)
self.assertEqual(flag.validate_input_flag_value('random value'), True) self.assertEqual(flag.validate_input_flag_value('random value'), True)
def test_get_input_flag1(self):
flag = InputFlag(name='test')
input_flags = InputFlags(flag)
self.assertEqual(input_flags.get_flag('test'), flag)
def test_get_input_flag2(self):
flag = InputFlag(name='test')
flag2 = InputFlag(name='some')
input_flags = InputFlags(flag, flag2)
self.assertEqual(input_flags.get_flag('some'), flag2)
def test_get_undefined_input_flag(self):
flag = InputFlag(name='test')
flag2 = InputFlag(name='some')
input_flags = InputFlags(flag, flag2)
self.assertEqual(input_flags.get_flag('case'), None)
def test_get_flags(self):
flags = Flags()
list_of_flags = [
Flag('test1'),
Flag('test2'),
Flag('test3'),
]
flags.add_flags(list_of_flags)
self.assertEqual(flags.get_flags(),
list_of_flags)
def test_add_flag(self):
flags = Flags()
flags.add_flag(Flag('test'))
self.assertEqual(len(flags.get_flags()), 1)
def test_add_flags(self):
flags = Flags()
flags.add_flags([Flag('test'), Flag('test2')])
self.assertEqual(len(flags.get_flags()), 2)
-26
View File
@@ -1,26 +0,0 @@
from argenta.command.flag.models import Flag, Flags
import unittest
class TestFlags(unittest.TestCase):
def test_get_flags(self):
flags = Flags()
list_of_flags = [
Flag('test1'),
Flag('test2'),
Flag('test3'),
]
flags.add_flags(list_of_flags)
self.assertEqual(flags.get_flags(),
list_of_flags)
def test_add_flag(self):
flags = Flags()
flags.add_flag(Flag('test'))
self.assertEqual(len(flags.get_flags()), 1)
def test_add_flags(self):
flags = Flags()
flags.add_flags([Flag('test'), Flag('test2')])
self.assertEqual(len(flags.get_flags()), 2)
+107 -15
View File
@@ -1,30 +1,122 @@
from argenta.command.flag import InputFlag, Flag
from argenta.command.flags import Flags, InputFlags, UndefinedInputFlags, InvalidValueInputFlags, ValidInputFlags
from argenta.response import Response
from argenta.router import Router from argenta.router import Router
from argenta.command import Command from argenta.command import Command
from argenta.router.exceptions import TriggerCannotContainSpacesException from argenta.router.exceptions import (TriggerContainSpacesException,
RepeatedFlagNameException,
TooManyTransferredArgsException,
RequiredArgumentNotPassedException)
import unittest import unittest
import re
class TestRouter(unittest.TestCase): class TestRouter(unittest.TestCase):
def test_get_router_name(self):
self.assertEqual(Router(name='test name').get_name(), 'test name')
def test_get_router_title(self): def test_get_router_title(self):
self.assertEqual(Router(title='test title').get_title(), 'test title') self.assertEqual(Router(title='test title').get_title(), 'test title')
def test_register_command_with_spaces_in_trigger(self): def test_register_command_with_spaces_in_trigger(self):
router = Router() router = Router()
with self.assertRaises(TriggerCannotContainSpacesException): with self.assertRaises(TriggerContainSpacesException):
@router.command(Command(trigger='command with spaces')) router._validate_command(Command(trigger='command with spaces'))
def test():
return 'correct result' def test_register_command_with_repeated_flags(self):
router = Router()
with self.assertRaises(RepeatedFlagNameException):
router._validate_command(Command(trigger='command', flags=Flags(Flag('test'), Flag('test'))))
def test_structuring_input_flags1(self):
router = Router()
cmd = Command('cmd')
input_flags = InputFlags(InputFlag('ssh'))
self.assertEqual(router._structuring_input_flags(cmd, input_flags).undefined_flags, UndefinedInputFlags(InputFlag('ssh')))
def test_structuring_input_flags2(self):
router = Router()
cmd = Command('cmd')
input_flags = InputFlags(InputFlag('ssh', value='some'))
self.assertEqual(router._structuring_input_flags(cmd, input_flags).undefined_flags, UndefinedInputFlags(InputFlag('ssh', value='some')))
def test_structuring_input_flags3(self):
router = Router()
cmd = Command('cmd', flags=Flag('port'))
input_flags = InputFlags(InputFlag('ssh', value='some2'))
self.assertEqual(router._structuring_input_flags(cmd, input_flags).undefined_flags, UndefinedInputFlags(InputFlag('ssh', value='some2')))
def test_structuring_input_flags4(self):
router = Router()
command = Command('cmd', flags=Flag('ssh', possible_values=False))
input_flags = InputFlags(InputFlag('ssh', value='some3'))
self.assertEqual(router._structuring_input_flags(command, input_flags).invalid_value_flags, InvalidValueInputFlags(InputFlag('ssh', value='some3')))
def test_structuring_input_flags5(self):
router = Router()
command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'some[1-5]$')))
input_flags = InputFlags(InputFlag('ssh', value='some40'))
self.assertEqual(router._structuring_input_flags(command, input_flags).invalid_value_flags, InvalidValueInputFlags(InputFlag('ssh', value='some40')))
def test_structuring_input_flags6(self):
router = Router()
command = Command('cmd', flags=Flag('ssh', possible_values=['example']))
input_flags = InputFlags(InputFlag('ssh', value='example2'))
self.assertEqual(router._structuring_input_flags(command, input_flags).invalid_value_flags, InvalidValueInputFlags(InputFlag('ssh', value='example2')))
def test_structuring_input_flags7(self):
command = Command('cmd', flags=Flag('port'))
input_flags = InputFlags(InputFlag('port', value='some2'))
self.assertEqual(Router()._structuring_input_flags(command, input_flags).valid_flags, ValidInputFlags(InputFlag('port', value='some2')))
def test_structuring_input_flags8(self):
command = Command('cmd', flags=Flag('port', possible_values=['some2', 'some3']))
input_flags = InputFlags(InputFlag('port', value='some2'))
self.assertEqual(Router()._structuring_input_flags(command, input_flags).valid_flags, ValidInputFlags(InputFlag('port', value='some2')))
def test_structuring_input_flags9(self):
command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'more[1-5]$')))
input_flags = InputFlags(InputFlag('ssh', value='more5'))
self.assertEqual(Router()._structuring_input_flags(command, input_flags).valid_flags, ValidInputFlags(InputFlag('ssh', value='more5')))
def test_structuring_input_flags10(self):
command = Command('cmd', flags=Flag('ssh', possible_values=False))
input_flags = InputFlags(InputFlag('ssh'))
self.assertEqual(Router()._structuring_input_flags(command, input_flags).valid_flags, ValidInputFlags(InputFlag('ssh')))
def test_validate_incorrect_func_args1(self):
def handler():
pass
with self.assertRaises(RequiredArgumentNotPassedException):
Router()._validate_func_args(handler)
def test_validate_incorrect_func_args2(self):
def handler(args, kwargs):
pass
with self.assertRaises(TooManyTransferredArgsException):
Router()._validate_func_args(handler)
def test_get_router_aliases(self):
router = Router()
@router.command(Command('some', aliases=['test', 'case']))
def handler(response):
pass
self.assertListEqual(router.get_aliases(), ['test', 'case'])
def test_get_router_aliases2(self):
router = Router()
@router.command(Command('some', aliases=['test', 'case']))
def handler(response):
pass
@router.command(Command('ext', aliases=['more', 'foo']))
def handler2(response):
pass
self.assertListEqual(router.get_aliases(), ['test', 'case', 'more', 'foo'])
def test_get_router_aliases3(self):
router = Router()
@router.command(Command('some'))
def handler(response):
pass
self.assertListEqual(router.get_aliases(), [])