13 Commits

Author SHA1 Message Date
kolo c8e0729be8 fix bugs 2025-05-27 14:19:54 +03:00
kolo c2bbc5f15d first steps sor adding metrics tests 2025-05-24 00:39:39 +03:00
kolo 0acbf54e44 first steps sor adding metrics tests 2025-05-23 22:12:12 +03:00
kolo c3d9541330 working 2025-05-23 14:33:13 +03:00
kolo f6561de9b3 wotk 2025-05-22 20:26:48 +03:00
kolo bebd84969b add Enum PossibleValues for bool values as values of possible_values argument in Flag 2025-05-22 12:10:32 +03:00
kolo 365347ea7f some fix 2025-05-21 17:01:35 +03:00
kolo 33cb528b1d some fix 2025-05-21 13:32:45 +03:00
kolo fd287c5da0 fix type hints with ty help, add ability to configure stdout capture when handling input by the router 2025-05-20 22:44:47 +03:00
kolo 45f410e3e8 make pre_cycle_setup faster on 4 sec, start implemtnation disable redirect stdout in router 2025-05-19 10:31:05 +03:00
kolo 8b06e9cd39 add metrics concept 2025-05-12 16:22:29 +03:00
kolo c38fe10006 translate readme on de 2025-05-10 22:07:52 +03:00
kolo 03cbc64f48 translate readme 2025-05-10 21:56:34 +03:00
35 changed files with 438 additions and 309 deletions
+65
View File
@@ -0,0 +1,65 @@
# Argenta
### Bibliothek zum Erstellen modularer CLI-Anwendungen
![preview](https://github.com/koloideal/Argenta/blob/kolo/imgs/mock_app_preview4.png?raw=True)
---
# Installation
```bash
pip install argenta
```
or
```bash
poetry add argenta
```
---
# Schnellstart
Ein Beispiel für eine einfache Anwendung
```python
# routers.py
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
router = Router()
@router.command(Command("hello"))
def handler(response: Response):
print("Hello, world!")
```
```python
# main.py
from argenta.app import App
from argenta.orchestrator import Orchestrator
from routers import router
app: App = App()
orchestrator: Orchestrator = Orchestrator()
def main() -> None:
app.include_router(router)
orchestrator.start_polling(app)
if __name__ == '__main__':
main()
```
---
# Funktionen in der Entwicklung
- Vollständige Unterstützung für Autocompleter unter Linux
## Vollständige [Dokumentation](https://argenta-docs.vercel.app) | MIT 2025 kolo | made by [kolo](https://t.me/kolo_id)
+9 -8
View File
@@ -1,12 +1,14 @@
# Argenta
### Библиотека для создания модульных CLI приложeний
### Library for creating modular CLI applications
#### RU - [README.ru.md](https://github.com/koloideal/Argenta/blob/kolo/README.ru.md) • DE - [README.de.md](https://github.com/koloideal/Argenta/blob/kolo/README.de.md)
![preview](https://github.com/koloideal/Argenta/blob/kolo/imgs/mock_app_preview4.png?raw=True)
---
# Установка
# Installing
```bash
pip install argenta
```
@@ -17,9 +19,9 @@ poetry add argenta
---
# Быстрый старт
# Quick start
Пример простейшего приложения
An example of a simple application
```python
# routers.py
from argenta.router import Router
@@ -55,12 +57,11 @@ if __name__ == '__main__':
---
# Фичи в разработке
# Features in development
- Полноценная поддержка автокомплитера на Linux
- Возможность настройки захвата stdout при обработке хэндлером ввода
- Full support for autocompleter on Linux
## Полная [документация](https://argenta-docs.vercel.app) | MIT 2025 kolo | made by [kolo](https://t.me/kolo_id)
## Full [docs](https://argenta-docs.vercel.app) | MIT 2025 kolo | made by [kolo](https://t.me/kolo_id)
+65
View File
@@ -0,0 +1,65 @@
# Argenta
### Библиотека для создания модульных CLI приложeний
![preview](https://github.com/koloideal/Argenta/blob/kolo/imgs/mock_app_preview4.png?raw=True)
---
# Установка
```bash
pip install argenta
```
or
```bash
poetry add argenta
```
---
# Быстрый старт
Пример простейшего приложения
```python
# routers.py
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
router = Router()
@router.command(Command("hello"))
def handler(response: Response):
print("Hello, world!")
```
```python
# main.py
from argenta.app import App
from argenta.orchestrator import Orchestrator
from routers import router
app: App = App()
orchestrator: Orchestrator = Orchestrator()
def main() -> None:
app.include_router(router)
orchestrator.start_polling(app)
if __name__ == '__main__':
main()
```
---
# Фичи в разработке
- Полноценная поддержка автокомплитера на Linux
## Полная [документация](https://argenta-docs.vercel.app) | MIT 2025 kolo | made by [kolo](https://t.me/kolo_id)
@@ -0,0 +1,36 @@
from argenta.command import Command
from argenta.metrics import get_time_of_pre_cycle_setup
from argenta.response import Response
from argenta.router import Router
from argenta.app import App
class TimeOfPreCycleSetup:
@staticmethod
def commands_with_two_aliases(num_of_commands: int):
router = Router()
for i in range(num_of_commands):
@router.command(Command(f'cmd{i}', aliases=[f'cdr{i}', f'prt{i}']))
def handler(response: Response):
pass
app = App()
app.include_router(router)
return get_time_of_pre_cycle_setup(app)
@staticmethod
def commands_with_one_aliases(num_of_commands: int):
router = Router()
for i in range(num_of_commands):
@router.command(Command(f'cmd{i}', aliases=[f'cdr{i}']))
def handler(response: Response):
pass
app = App()
app.include_router(router)
return get_time_of_pre_cycle_setup(app)
-27
View File
@@ -1,27 +0,0 @@
from mock.mock_app.handlers.routers import work_router
from argenta.app import App
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
arg_parser = ArgParser(processed_args=[BooleanArgument("repeat")])
app: App = App(autocompleter=AutoCompleter(".hist"))
orchestrator: Orchestrator = Orchestrator()
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()
+11 -82
View File
@@ -1,89 +1,18 @@
from argenta.router import Router
from argenta.command import Command
from argenta.response import Response
from argenta.response.status import Status
from argenta.command.flag import Flag
from argenta.command.flags import Flags
from argenta.app import App
from argenta.command import Command
from argenta.orchestrator import Orchestrator
# Создание маршрутизатора
file_router = Router("Операции с файлами")
# Определение флагов для команды копирования
copy_flags = Flags(
Flag('source', '--'),
Flag('destination', '--'),
Flag('recursive', '--', False), # Булевый флаг без значения
Flag('force', '-', False) # Короткий булевый флаг
)
@file_router.command(Command('case', aliases=['cp', 'ch']))
def handler(response: Response):
print('test')
# Регистрация команды копирования
@file_router.command(Command(
trigger="ch",
description="Копирование файлов",
flags=copy_flags,
aliases=["cp"]
))
def copy_files(response: Response):
# Получаем значения корректных флагов
source = None
destination = None
recursive = False
force = False
for flag in response.valid_flags:
if flag.get_name() == "source":
source = flag.get_value()
elif flag.get_name() == "destination":
destination = flag.get_value()
elif flag.get_name() == "recursive":
recursive = True
elif flag.get_name() == "force":
force = True
# Проверка обязательных параметров
if not source or not destination:
print("Ошибка: необходимо указать источник и назначение")
return
print(f"Копирование из {source} в {destination}")
if recursive:
print("Рекурсивное копирование включено")
if force:
print("Принудительное копирование включено")
# Обработка неопределенных флагов
if response.undefined_flags:
print("\nПредупреждение: обнаружены незарегистрированные флаги:")
for flag in response.undefined_flags:
print(f" - {flag.get_name()}" +
(f" = {flag.get_value()}" if flag.get_value() else ""))
# Обработка флагов с некорректными значениями
if response.invalid_value_flags:
print("\nПредупреждение: обнаружены флаги с некорректными значениями:")
for flag in response.invalid_value_flags:
print(f" - {flag.get_name()} = {flag.get_value()}")
# Принятие решения на основе статуса
if response.status != Status.ALL_FLAGS_VALID:
print("\nВыполнение с предупреждениями из-за проблем с флагами.")
from argenta.router import Router
app = App()
app.include_router(file_router)
router = Router()
orchestrator = Orchestrator()
@router.command(Command('test'))
def test(response):
print('test command')
app = App(ignore_command_register=True,
override_system_messages=True,
print_func=print)
app.include_router(router)
orchestrator.start_polling(app)
View File
@@ -1,12 +0,0 @@
from rich.console import Console
console = Console()
def help_command():
console.print(
"[italic bold]The main functionality of the script is to convert an expression from a string "
"to a mathematical one and then calculate this expression. "
"Project GitHub: https://github.com/koloideal/WordMath[/italic bold]"
)
+1 -1
View File
@@ -1,4 +1,4 @@
from mock.mock_app.handlers.routers import work_router
from mock.mock_app.routers import work_router
from argenta.app import App
from argenta.app.defaults import PredefinedMessages
@@ -2,15 +2,17 @@ from rich.console import Console
from argenta.command import Command
from argenta.command.flag.defaults import PredefinedFlags
from argenta.command.flags import Flags
from argenta.command.flag import Flags, Flag, PossibleValues
from argenta.response import Response
from argenta.router import Router
work_router: Router = Router(title="Work points:")
work_router: Router = Router(title="Work points:", disable_redirect_stdout=True)
console = Console()
flag = Flag('csdv', possible_values=PossibleValues.DISABLE)
@work_router.command(
Command(
@@ -21,6 +23,8 @@ console = Console()
)
)
def command_help(response: Response):
case = input("test > ")
print(case)
print(response.status)
print(response.undefined_flags.get_flags())
print(response.valid_flags.get_flags())
+6 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "argenta"
version = "1.0.3"
version = "1.0.7"
description = "Python library for building modular CLI applications"
authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }]
requires-python = ">=3.8"
@@ -26,3 +26,8 @@ exclude = [
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"psutil>=7.0.0",
]
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Never
class AutoCompleter:
def __init__(
self, history_filename: str = False, autocomplete_button: str = "tab"
self, history_filename: str | None = None, autocomplete_button: str = "tab"
) -> None:
"""
Public. Configures and implements auto-completion of input command
+3 -2
View File
@@ -1,10 +1,11 @@
from enum import Enum
from enum import StrEnum
class PredefinedMessages(Enum):
class PredefinedMessages(StrEnum):
"""
Public. A dataclass with predetermined messages for quick use
"""
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]"
AUTOCOMPLETE = "[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>"
+106 -97
View File
@@ -21,9 +21,9 @@ from argenta.app.registered_routers.entity import RegisteredRouters
from argenta.response import Response
class BaseApp:
def __init__(self,
prompt: str,
def __init__(self, prompt: str,
initial_message: str,
farewell_message: str,
exit_command: Command,
@@ -47,18 +47,33 @@ class BaseApp:
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._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_case: list[str] = []
self._all_registered_triggers_in_default_case: list[str] = []
self._matching_lower_triggers_with_routers: dict[str, Router] = {}
self._matching_default_triggers_with_routers: dict[str, Router] = {}
self._incorrect_input_syntax_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))
if self._ignore_command_register:
self._current_matching_triggers_with_routers: dict[str, Router] = self._matching_lower_triggers_with_routers
else:
self._current_matching_triggers_with_routers: dict[str, Router] = self._matching_default_triggers_with_routers
self._incorrect_input_syntax_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:
"""
@@ -194,10 +209,10 @@ class BaseApp:
"""
input_command_trigger = command.get_trigger()
if self._ignore_command_register:
if input_command_trigger.lower() in self._all_registered_triggers_in_lower_case:
if input_command_trigger.lower() in list(self._current_matching_triggers_with_routers.keys()):
return False
else:
if input_command_trigger in self._all_registered_triggers_in_default_case:
if input_command_trigger in list(self._current_matching_triggers_with_routers.keys()):
return False
return True
@@ -210,13 +225,12 @@ class BaseApp:
:param raw_command: the raw input command
:return: None
"""
match error:
case UnprocessedInputFlagException():
self._incorrect_input_syntax_handler(raw_command)
case RepeatedInputFlagsException():
self._repeated_input_flags_handler(raw_command)
case EmptyInputCommandException():
self._empty_input_command_handler()
if isinstance(error, UnprocessedInputFlagException):
self._incorrect_input_syntax_handler(raw_command)
elif isinstance(error, RepeatedInputFlagsException):
self._repeated_input_flags_handler(raw_command)
elif isinstance(error, EmptyInputCommandException):
self._empty_input_command_handler()
def _setup_system_router(self) -> None:
"""
@@ -234,11 +248,8 @@ class BaseApp:
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_case
if self._ignore_command_register
else self._all_registered_triggers_in_default_case
)
all_commands = list(self._current_matching_triggers_with_routers.keys())
matches: list[str] | list = sorted(
cmd for cmd in all_commands if cmd.startswith(unknown_command)
)
@@ -258,35 +269,27 @@ class BaseApp:
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._prompt = f"[italic dim bold]{self._prompt}"
self._initial_message = ("\n" + f"[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"
"[bold red]\n\n"
+ text2art(self._farewell_message, font="chanky")
+ "\n[/bold red]\n"
"[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._incorrect_input_syntax_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"
)
self._incorrect_input_syntax_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]"
)
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
@@ -296,7 +299,7 @@ class BaseApp:
self._unknown_command_handler = unknown_command_handler
def _pre_cycle_setup(self) -> None:
def pre_cycle_setup(self) -> None:
"""
Private. Configures various aspects of the application before the start of the cycle
:return: None
@@ -304,22 +307,22 @@ class BaseApp:
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())
router_triggers = router_entity.get_triggers()
router_aliases = router_entity.get_aliases()
combined = router_triggers + router_aliases
self._all_registered_triggers_in_lower_case.extend([x.lower() for x in router_entity.get_triggers()])
self._all_registered_triggers_in_lower_case.extend([x.lower() for x in router_entity.get_aliases()])
for trigger in combined:
self._matching_default_triggers_with_routers[trigger] = router_entity
self._matching_lower_triggers_with_routers[trigger.lower()] = router_entity
self._autocompleter.initial_setup(self._all_registered_triggers_in_lower_case)
self._autocompleter.initial_setup(list(self._current_matching_triggers_with_routers.keys()))
if self._ignore_command_register:
for cmd in set(self._all_registered_triggers_in_lower_case):
if self._all_registered_triggers_in_lower_case.count(cmd) != 1:
Console().print(f"\n[b red]WARNING:[/b red] Overlapping trigger or alias: [b blue]{cmd}[/b blue]")
else:
for cmd in set(self._all_registered_triggers_in_default_case):
if self._all_registered_triggers_in_default_case.count(cmd) != 1:
Console().print(f"\n[b red]WARNING:[/b red] Overlapping trigger or alias: [b blue]{cmd}[/b blue]")
seen = {}
for item in list(self._current_matching_triggers_with_routers.keys()):
if item in seen:
Console().print(f"\n[b red]WARNING:[/b red] Overlapping trigger or alias: [b blue]{item}[/b blue]")
else:
seen[item] = True
if not self._override_system_messages:
self._setup_default_view()
@@ -330,24 +333,25 @@ class BaseApp:
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 = "Argenta\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:
def __init__(
self,
prompt: str = "What do you want to do?\n",
initial_message: str = "Argenta\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
@@ -364,24 +368,26 @@ class App(BaseApp):
: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)
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()
self.pre_cycle_setup()
while True:
if self._repeat_command_groups_description:
self._print_command_group_description()
@@ -389,9 +395,7 @@ class App(BaseApp):
raw_command: str = Console().input(self._prompt)
try:
input_command: InputCommand = InputCommand.parse(
raw_command=raw_command
)
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)
@@ -401,14 +405,7 @@ class App(BaseApp):
if self._is_exit_command(input_command):
system_router.finds_appropriate_handler(input_command)
if self._ignore_command_register:
self._autocompleter.exit_setup(
self._all_registered_triggers_in_lower_case
)
else:
self._autocompleter.exit_setup(
self._all_registered_triggers_in_default_case
)
self._autocompleter.exit_setup(list(self._current_matching_triggers_with_routers.keys()))
return
if self._is_unknown_command(input_command):
@@ -418,11 +415,23 @@ class App(BaseApp):
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)
processing_router = self._current_matching_triggers_with_routers[input_command.get_trigger().lower()]
if processing_router.disable_redirect_stdout:
if isinstance(self._dividing_line, StaticDividingLine):
self._print_func(self._dividing_line.get_full_static_line(self._override_system_messages))
processing_router.finds_appropriate_handler(input_command)
self._print_func(self._dividing_line.get_full_static_line(self._override_system_messages))
else:
self._print_func(StaticDividingLine(self._dividing_line.get_unit_part()).get_full_static_line(self._override_system_messages))
processing_router.finds_appropriate_handler(input_command)
self._print_func(StaticDividingLine(self._dividing_line.get_unit_part()).get_full_static_line(self._override_system_messages))
else:
with redirect_stdout(io.StringIO()) as f:
processing_router.finds_appropriate_handler(input_command)
res: str = f.getvalue()
if res:
self._print_framed_text(res)
def include_router(self, router: Router) -> None:
"""
+1 -1
View File
@@ -4,7 +4,7 @@ from argenta.router import Router
class RegisteredRouters:
def __init__(self, registered_routers: list[Router] = None) -> None:
def __init__(self, registered_routers: list[Router] | None = None) -> None:
"""
Private. Combines registered routers
:param registered_routers: list of the registered routers
+15 -2
View File
@@ -1,4 +1,17 @@
__all__ = ["Flag", "InputFlag"]
__all__ = [
"Flag",
"InputFlag",
"UndefinedInputFlags",
"ValidInputFlags",
"InvalidValueInputFlags",
"Flags", "PossibleValues"
]
from argenta.command.flag.models import Flag, InputFlag
from argenta.command.flag.models import Flag, InputFlag, PossibleValues
from argenta.command.flag.flags.models import (
UndefinedInputFlags,
ValidInputFlags,
Flags,
InvalidValueInputFlags,
)
+8 -7
View File
@@ -1,22 +1,23 @@
from dataclasses import dataclass
from argenta.command.flag.models import Flag
from argenta.command.flag.models import Flag, PossibleValues
import re
@dataclass
class PredefinedFlags:
"""
Public. A dataclass with predefined flags and most frequently used flags for quick use
"""
HELP = Flag(name="help", possible_values=False)
SHORT_HELP = Flag(name="H", prefix="-", possible_values=False)
HELP = Flag(name="help", possible_values=PossibleValues.DISABLE)
SHORT_HELP = Flag(name="H", prefix="-", possible_values=PossibleValues.DISABLE)
INFO = Flag(name="info", possible_values=False)
SHORT_INFO = Flag(name="I", prefix="-", possible_values=False)
INFO = Flag(name="info", possible_values=PossibleValues.DISABLE)
SHORT_INFO = Flag(name="I", prefix="-", possible_values=PossibleValues.DISABLE)
ALL = Flag(name="all", possible_values=False)
SHORT_ALL = Flag(name="A", prefix="-", possible_values=False)
ALL = Flag(name="all", possible_values=PossibleValues.DISABLE)
SHORT_ALL = Flag(name="A", prefix="-", possible_values=PossibleValues.DISABLE)
HOST = Flag(
name="host", possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
@@ -7,7 +7,7 @@ __all__ = [
]
from argenta.command.flags.models import (
from argenta.command.flag.flags.models import (
Flags,
InputFlags,
UndefinedInputFlags,
+16 -3
View File
@@ -1,6 +1,16 @@
from enum import Enum
from typing import Literal, Pattern
class PossibleValues(Enum):
DISABLE: Literal[False] = False
ALL: Literal[True] = True
def __eq__(self, other: bool) -> bool:
return self.value == other
class BaseFlag:
def __init__(self, name: str, prefix: Literal["-", "--", "---"] = "--") -> None:
"""
@@ -43,7 +53,7 @@ class Flag(BaseFlag):
self,
name: str,
prefix: Literal["-", "--", "---"] = "--",
possible_values: list[str] | Pattern[str] | False = True,
possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
) -> None:
"""
Public. The entity of the flag being registered for subsequent processing
@@ -61,7 +71,7 @@ class Flag(BaseFlag):
: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 self.possible_values == PossibleValues.DISABLE:
if input_flag_value is None:
return True
else:
@@ -87,7 +97,10 @@ class Flag(BaseFlag):
class InputFlag(BaseFlag):
def __init__(
self, name: str, prefix: Literal["-", "--", "---"] = "--", value: str = None
self,
name: str,
prefix: Literal["-", "--", "---"] = "--",
value: str | None = None,
):
"""
Public. The entity of the flag of the entered command
+9 -11
View File
@@ -1,14 +1,11 @@
from argenta.command.flag.models import Flag, InputFlag
from argenta.command.flags.models import InputFlags, Flags
from argenta.command.flag.flags.models import InputFlags, Flags
from argenta.command.exceptions import (
UnprocessedInputFlagException,
RepeatedInputFlagsException,
EmptyInputCommandException,
)
from typing import Generic, TypeVar, cast, Literal
InputCommandType = TypeVar("InputCommandType")
from typing import cast, Literal
class BaseCommand:
@@ -31,9 +28,9 @@ class Command(BaseCommand):
def __init__(
self,
trigger: str,
description: str = None,
flags: Flag | Flags = None,
aliases: list[str] = None,
description: str | None = None,
flags: Flag | Flags | None = None,
aliases: list[str] | None = None,
):
"""
Public. The command that can and should be registered in the Router
@@ -94,6 +91,7 @@ class Command(BaseCommand):
is_valid = registered_flag.validate_input_flag_value(
flag.get_value()
)
if is_valid:
return "Valid"
else:
@@ -109,8 +107,8 @@ class Command(BaseCommand):
return self._description
class InputCommand(BaseCommand, Generic[InputCommandType]):
def __init__(self, trigger: str, input_flags: InputFlag | InputFlags = None):
class InputCommand(BaseCommand):
def __init__(self, trigger: str, input_flags: InputFlag | InputFlags | None = None):
"""
Private. The model of the input command, after parsing
:param trigger:the trigger of the command
@@ -142,7 +140,7 @@ class InputCommand(BaseCommand, Generic[InputCommandType]):
return self._input_flags
@staticmethod
def parse(raw_command: str) -> InputCommandType:
def parse(raw_command: str) -> "InputCommand":
"""
Private. Parse the raw input command
:param raw_command: raw input command
+4
View File
@@ -0,0 +1,4 @@
__all__ = ["get_time_of_pre_cycle_setup"]
from argenta.metrics.main import get_time_of_pre_cycle_setup
+18
View File
@@ -0,0 +1,18 @@
import io
from contextlib import redirect_stdout
from time import time
from argenta.app import App
def get_time_of_pre_cycle_setup(app: App) -> float:
"""
Public. Return time of pre cycle setup
:param app: app instance for testing time of pre cycle setup
:return: time of pre cycle setup as float
"""
start = time()
with redirect_stdout(io.StringIO()):
app.pre_cycle_setup()
end = time()
return end - start
+2 -2
View File
@@ -5,13 +5,13 @@ from argenta.orchestrator.argparser import ArgParser
class Orchestrator:
def __init__(self, arg_parser: ArgParser = False):
def __init__(self, arg_parser: ArgParser | None = None):
"""
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
self.arg_parser: ArgParser | None = arg_parser
if arg_parser:
self.arg_parser.register_args()
+2 -2
View File
@@ -1,5 +1,5 @@
from argenta.response.status import Status
from argenta.command.flags import (
from argenta.command.flag.flags import (
ValidInputFlags,
UndefinedInputFlags,
InvalidValueInputFlags,
@@ -11,7 +11,7 @@ class Response:
def __init__(
self,
status: Status = None,
status: Status | None = None,
valid_flags: ValidInputFlags = ValidInputFlags(),
undefined_flags: UndefinedInputFlags = UndefinedInputFlags(),
invalid_value_flags: InvalidValueInputFlags = InvalidValueInputFlags(),
+1 -1
View File
@@ -38,7 +38,7 @@ class CommandHandler:
class CommandHandlers:
def __init__(self, command_handlers: list[CommandHandler] = None):
def __init__(self, command_handlers: list[CommandHandler] | None = None):
"""
Private. The model that unites all CommandHandler of the routers
:param command_handlers: list of CommandHandlers for register
+36 -32
View File
@@ -6,7 +6,7 @@ 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 (
from argenta.command.flag.flags import (
Flags,
InputFlags,
UndefinedInputFlags,
@@ -22,13 +22,21 @@ from argenta.router.exceptions import (
class Router:
def __init__(self, title: str | None = "Awesome title"):
def __init__(
self, title: str | None = "Awesome title", disable_redirect_stdout: bool = False
):
"""
Public. Directly configures and manages handlers
:param title: the title of the router, displayed when displaying the available commands
:param disable_redirect_stdout: Disables stdout forwarding, if the argument value is True,
the StaticDividingLine will be forced to be used as a line separator for this router,
disabled forwarding is needed when there is text output in conjunction with a text input request (for example, input()),
if the argument value is True, the output of the input() prompt is intercepted and not displayed,
which is ambiguous behavior and can lead to unexpected work
:return: None
"""
self.title = title
self.disable_redirect_stdout = disable_redirect_stdout
self._command_handlers: CommandHandlers = CommandHandlers()
self._ignore_command_register: bool = False
@@ -39,13 +47,15 @@ class Router:
:param command: Registered command
:return: decorated handler as Callable
"""
self._validate_command(command)
if isinstance(command, str):
command = Command(command)
redefined_command = Command(command)
else:
redefined_command = command
self._validate_command(redefined_command)
def command_decorator(func):
Router._validate_func_args(func)
self._command_handlers.add_handler(CommandHandler(func, command))
self._command_handlers.add_handler(CommandHandler(func, redefined_command))
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
@@ -83,9 +93,7 @@ class Router:
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
)
response: Response = self._structuring_input_flags( handle_command, input_command_flags )
command_handler.handling(response)
else:
response.status = Status.ALL_FLAGS_VALID
@@ -117,13 +125,12 @@ class Router:
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 flag_status == "Valid":
valid_input_flags.add_flag(flag)
elif flag_status == "Undefined":
undefined_input_flags.add_flag(flag)
elif flag_status == "Invalid":
invalid_value_input_flags.add_flag(flag)
if (
not invalid_value_input_flags.get_flags()
@@ -151,25 +158,20 @@ class Router:
)
@staticmethod
def _validate_command(command: Command | str) -> None:
def _validate_command(command: Command) -> 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()
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()
@staticmethod
def _validate_func_args(func: Callable) -> None:
@@ -191,13 +193,15 @@ class Router:
if arg_annotation is Response:
pass
else:
file_path: str = getsourcefile(func)
file_path: str | None = getsourcefile(func)
source_line: int = getsourcelines(func)[1]
fprint = Console().print
fprint(f'\nFile "{file_path}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint '
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]",
highlight=False)
highlight=False,
)
def set_command_register_ignore(self, _: bool) -> None:
"""
@@ -7,7 +7,7 @@ import re
from argenta.app import App
from argenta.command import Command
from argenta.router import Router
from argenta.command.flags.models import Flags
from argenta.command.flag.flags.models import Flags
from argenta.command.flag.defaults import PredefinedFlags
from argenta.orchestrator import Orchestrator
from argenta.response import Response
@@ -10,7 +10,7 @@ from argenta.response import Response
from argenta.router import Router
from argenta.orchestrator import Orchestrator
from argenta.command.flag import Flag
from argenta.command.flags import Flags
from argenta.command.flag.flags import Flags
from argenta.command.flag.defaults import PredefinedFlags
+6 -4
View File
@@ -3,6 +3,8 @@ from argenta.app import App
import unittest
from argenta.router import Router
class MyTestCase(unittest.TestCase):
def test_is_exit_command1(self):
@@ -33,25 +35,25 @@ class MyTestCase(unittest.TestCase):
def test_is_unknown_command1(self):
app = App()
app.set_unknown_command_handler(lambda command: None)
app._all_registered_triggers_in_lower_case = ['fr', 'tr', 'de']
app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()}
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_case = ['fr', 'tr', 'de']
app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()}
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']
app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()}
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']
app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()}
self.assertEqual(app._is_unknown_command(InputCommand('tW')), False)
+1 -1
View File
@@ -1,5 +1,5 @@
from argenta.command.flag import Flag, InputFlag
from argenta.command.flags import Flags
from argenta.command.flag.flags import Flags
from argenta.command.models import InputCommand, Command
from argenta.command.exceptions import (UnprocessedInputFlagException,
RepeatedInputFlagsException,
+6 -6
View File
@@ -1,5 +1,5 @@
from argenta.command.flag import Flag, InputFlag
from argenta.command.flags import InputFlags, Flags
from argenta.command.flag import Flag, InputFlag, PossibleValues
from argenta.command.flag.flags import InputFlags, Flags
import unittest
import re
@@ -54,19 +54,19 @@ class TestFlag(unittest.TestCase):
self.assertEqual(flag.validate_input_flag_value('192.168.9.8'), True)
def test_validate_correct_empty_flag_value_without_possible_flag_values(self):
flag = Flag(name='test', possible_values=False)
flag = Flag(name='test', possible_values=PossibleValues.DISABLE)
self.assertEqual(flag.validate_input_flag_value(None), True)
def test_validate_correct_empty_flag_value_with_possible_flag_values(self):
flag = Flag(name='test', possible_values=True)
flag = Flag(name='test', possible_values=PossibleValues.DISABLE)
self.assertEqual(flag.validate_input_flag_value(None), True)
def test_validate_incorrect_random_flag_value_without_possible_flag_values(self):
flag = Flag(name='test', possible_values=False)
flag = Flag(name='test', possible_values=PossibleValues.DISABLE)
self.assertEqual(flag.validate_input_flag_value('random value'), False)
def test_validate_correct_random_flag_value_with_possible_flag_values(self):
flag = Flag(name='test', possible_values=True)
flag = Flag(name='test', possible_values=PossibleValues.ALL)
self.assertEqual(flag.validate_input_flag_value('random value'), True)
def test_get_input_flag1(self):
+1 -1
View File
@@ -1,5 +1,5 @@
from argenta.command.flag import InputFlag, Flag
from argenta.command.flags import Flags, InputFlags, UndefinedInputFlags, InvalidValueInputFlags, ValidInputFlags
from argenta.command.flag.flags import Flags, InputFlags, UndefinedInputFlags, InvalidValueInputFlags, ValidInputFlags
from argenta.router import Router
from argenta.command import Command
from argenta.router.exceptions import (TriggerContainSpacesException,