Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77416cf22c | |||
| b6c84f1a91 | |||
| c2d235e576 | |||
| f7f5db58aa | |||
| 73303b1c08 | |||
| 22f1171192 | |||
| a844095fdc | |||
| a7c6a14705 | |||
| cfdb37330e | |||
| aef6a9ca38 | |||
| c8e0729be8 | |||
| c2bbc5f15d | |||
| 0acbf54e44 | |||
| c3d9541330 | |||
| f6561de9b3 | |||
| bebd84969b | |||
| 365347ea7f | |||
| 33cb528b1d | |||
| fd287c5da0 | |||
| 45f410e3e8 | |||
| 8b06e9cd39 | |||
| c38fe10006 | |||
| 03cbc64f48 | |||
| cbf7d3c578 | |||
| ea2d068022 | |||
| 5991851207 | |||
| f628c3b5b5 | |||
| 05379712f4 | |||
| ed1cbf0fcf | |||
| 471f05369b | |||
| 13f7e33db1 | |||
| 9a78aa9263 | |||
| 58ccd6b26d | |||
| 73144f7ba4 | |||
| 650f4c9036 | |||
| 393f5c7d81 | |||
| 9eb2bb6c46 | |||
| 79b275eac7 | |||
| 07ac2af71e | |||
| c4b3aa7db8 | |||
| 61ef6a6466 | |||
| 477f3a7dec | |||
| adf3431388 | |||
| 83955aa046 | |||
| 5a17e916eb | |||
| 1159dda16e | |||
| 315508a36e | |||
| 9d6598c4e0 | |||
| eb43806da6 | |||
| e076dbf84f | |||
| 2f090b6b47 | |||
| c9dbf2bbae | |||
| e768c1bd2c | |||
| 408450ec12 | |||
| 106ca058be | |||
| b5ddfb3b35 | |||
| 61e4502e41 | |||
| 9b2fc87e33 | |||
| 89f09c42f8 | |||
| 5bcae8fe68 | |||
| ca58008431 | |||
| 30974f48eb | |||
| df4ba080b0 | |||
| f93930d712 | |||
| 036c17ec9a | |||
| 7281fdeabf | |||
| 051ec6df28 | |||
| 00a1e11fc1 | |||
| 584df9ba69 | |||
| a649022f1d | |||
| 26a9d8a6da | |||
| 9522b0161a | |||
| e189f8d9aa | |||
| 3ef8707cfa | |||
| a5fdcab862 | |||
| ba035881ee | |||
| 34ebe55531 | |||
| 01c9d2dc6d | |||
| a6db733204 | |||
| 8506e4ffcf | |||
| 04d3329572 | |||
| 5bfdde4bd9 | |||
| e2dd7e4aea | |||
| d1d644d422 | |||
| 8b496aa782 | |||
| 592d128ef6 | |||
| b44ee227fd | |||
| 0dce4a0d9e | |||
| ca6634c6f0 | |||
| c1805af420 | |||
| 0e308ce77f | |||
| ab1d335f8e | |||
| 1a2e9d1487 | |||
| 76bcba9340 | |||
| ae9795bd53 | |||
| 7540728f1b | |||
| 54da63dd03 | |||
| 8e08d0fe09 | |||
| 55b88f7c8a | |||
| 1cd616336f | |||
| 253790fe2e | |||
| 1c6f896b73 | |||
| 8810e12551 | |||
| 285007a59a | |||
| 6edd17646a | |||
| 154ee25dde | |||
| 30cf3cfd06 | |||
| 0d98d80919 | |||
| 54992e55cb | |||
| cc8135b733 | |||
| 5c6fa5151a | |||
| 2918bc9f81 | |||
| 6e2fbc23e9 | |||
| 1ec8ea53b4 | |||
| 4256d67789 | |||
| 0246ff4b22 | |||
| 956febc757 | |||
| beafdd0afd | |||
| b172e2cdc3 | |||
| baaf0e25f3 | |||
| 09c40978a1 | |||
| 0f4f48555e | |||
| d30515c1a2 | |||
| 5a6fc1d8ca |
@@ -0,0 +1,31 @@
|
|||||||
|
name: mypy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install mypy
|
||||||
|
pip install .
|
||||||
|
|
||||||
|
- name: Run type checker
|
||||||
|
run: mypy -p argenta
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: ruff
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install ruff
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
run: ruff check ./src
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install uv
|
||||||
|
uv sync --group dev
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: uv run python -m unittest discover
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
.venv
|
*venv
|
||||||
.idea
|
.idea
|
||||||
|
.vscode
|
||||||
dist
|
dist
|
||||||
poetry.lock
|
uv.lock
|
||||||
*__pycache__/
|
*__pycache__/
|
||||||
|
*.hist*
|
||||||
|
build
|
||||||
|
source
|
||||||
|
*cache
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Argenta
|
||||||
|
|
||||||
|
### Bibliothek zum Erstellen modularer CLI-Anwendungen
|
||||||
|
|
||||||
|
Mit Argenta können Sie die CLI-Funktionalität in isolierte, abstrahierte Umgebungen einkapseln. Zum Beispiel: Sie erstellen ein Dienstprogramm ähnlich dem Metasploit Framework, bei dem der Benutzer zuerst in einen bestimmten Scoop eintritt (z. B. ein Modul zum Scannen auswählt) und dann auf eine Reihe von Befehlen zugreift, die nur für diesen Kontext spezifisch sind. Argenta bietet eine einfache und prägnante Möglichkeit, eine solche Architektur zu konstruieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
# Argenta
|
# Argenta
|
||||||
|
|
||||||
---
|
### Library for creating modular CLI applications
|
||||||
|
|
||||||
## Описание
|
#### RU - [README.ru.md](https://github.com/koloideal/Argenta/blob/main/README.ru.md) • DE - [README.de.md](https://github.com/koloideal/Argenta/blob/main/README.de.md)
|
||||||
**Argenta** — Python library for creating custom shells
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Установка
|
Argenta allows you to encapsulate CLI functionality in isolated, abstracted environments. Eg: you are creating a utility similar to the Metasploit Framework, where the user first logs into a specific scope (for example, selects a module to scan), and then gets access to a set of commands specific only to that context. Argenta provides a simple and concise way to build such an architecture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Installing
|
||||||
```bash
|
```bash
|
||||||
pip install argenta
|
pip install argenta
|
||||||
```
|
```
|
||||||
@@ -18,427 +25,49 @@ poetry add argenta
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Быстрый старт
|
# Quick start
|
||||||
|
|
||||||
Пример простейшей оболочки с командой без зарегистрированных флагов
|
An example of a simple application
|
||||||
```python
|
```python
|
||||||
# routers.py
|
# routers.py
|
||||||
from argenta.router import Router
|
from argenta.router import Router
|
||||||
from argenta.command import Command
|
from argenta.command import Command
|
||||||
|
from argenta.response import Response
|
||||||
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@router.command(Command("hello"))
|
@router.command(Command("hello"))
|
||||||
def handler():
|
def handler(response: Response):
|
||||||
print("Hello, world!")
|
print("Hello, world!")
|
||||||
```
|
```
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# main.py
|
# main.py
|
||||||
from argenta.app import App
|
from argenta.app import App
|
||||||
|
from argenta.orchestrator import Orchestrator
|
||||||
from routers import router
|
from routers import router
|
||||||
|
|
||||||
app: App = App()
|
app: App = App()
|
||||||
|
orchestrator: Orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
app.start_polling()
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
```
|
```
|
||||||
Пример оболочки с командой, у которой зарегистрированы флаги
|
|
||||||
|
|
||||||
```python
|
|
||||||
# routers.py
|
|
||||||
import re
|
|
||||||
from argenta.router import Router
|
|
||||||
from argenta.command import Command
|
|
||||||
from argenta.command.params.flag import FlagsGroup, Flag
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
registered_flags = FlagsGroup([
|
|
||||||
Flag(flag_name='host',
|
|
||||||
flag_prefix='--',
|
|
||||||
possible_flag_values=re.compile(r'^192.168.\d{1,3}.\d{1,3}$')),
|
|
||||||
Flag('port', '---', re.compile(r'^[0-9]{1,4}$'))
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
@router.command(Command("hello"))
|
|
||||||
def handler():
|
|
||||||
print("Hello, world!")
|
|
||||||
|
|
||||||
|
|
||||||
@router.command(Command(trigger="ssh",
|
|
||||||
description='connect via ssh',
|
|
||||||
flags=registered_flags))
|
|
||||||
def handler_with_flags(flags: dict):
|
|
||||||
for flag in flags:
|
|
||||||
print(f'Flag name: {flag['name']}\n'
|
|
||||||
f'Flag value: {flag['value']}')
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# *classes* :
|
# Features in development
|
||||||
|
|
||||||
---
|
- Full support for autocompleter on Linux
|
||||||
|
|
||||||
## *class* : : `App`
|
## Full [docs](https://argenta-docs.vercel.app) | MIT 2025 kolo | made by [kolo](https://t.me/kolo_id)
|
||||||
Класс, определяющий поведение и состояние оболочки
|
|
||||||
|
|
||||||
### Конструктор
|
|
||||||
```python
|
|
||||||
App(prompt: str = 'Enter a command',
|
|
||||||
initial_greeting: str = '\nHello, I am Argenta\n',
|
|
||||||
farewell_message: str = '\nGoodBye\n',
|
|
||||||
exit_command: str = 'Q',
|
|
||||||
exit_command_description: str = 'Exit command',
|
|
||||||
system_points_title: str = 'System points:',
|
|
||||||
ignore_exit_command_register: bool = True,
|
|
||||||
ignore_command_register: bool = False,
|
|
||||||
line_separate: str = '',
|
|
||||||
command_group_description_separate: str = '',
|
|
||||||
repeat_command_groups: bool = True,
|
|
||||||
print_func: Callable[[str], None] = print)
|
|
||||||
```
|
|
||||||
**Аргументы:**
|
|
||||||
- **name : mean**
|
|
||||||
- `prompt` (`str`): Сообщение перед вводом команды.
|
|
||||||
- `initial_greeting` (`str`): Приветственное сообщение при запуске.
|
|
||||||
- `farewell_message` (`str`): Сообщение при выходе.
|
|
||||||
- `exit_command` (`str`): Команда выхода (по умолчанию `'Q'`).
|
|
||||||
- `exit_command_description` (`str`): Описание команды выхода.
|
|
||||||
- `system_points_title` (`str`): Заголовок перед списком системных команд.
|
|
||||||
- `ignore_exit_command_register` (`bool`): Игнорировать регистр команды выхода.
|
|
||||||
- `ignore_command_register` (`bool`): Игнорировать регистр всех команд.
|
|
||||||
- `line_separate` (`str`): Разделительная строка между командами.
|
|
||||||
- `command_group_description_separate` (`str`): Разделитель между группами команд.
|
|
||||||
- `repeat_command_groups` (`bool`): Повторять описание команд перед вводом.
|
|
||||||
- `print_func` (`Callable[[str], None]`): Функция вывода текста в терминал (по умолчанию `print`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ***methods***
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.start_polling() -> `None`**
|
|
||||||
|
|
||||||
*method mean* **::** запускает цикл обработки ввода
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.include_router(router: Router) -> `None`**
|
|
||||||
|
|
||||||
*param* `router: Router` **::** регистрируемый роутер
|
|
||||||
*required* **::** True
|
|
||||||
|
|
||||||
*method mean* **::** регистрирует роутер в оболочке
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.set_initial_message(message: str) -> `None`**
|
|
||||||
|
|
||||||
*param* `message: str` **::** устанавливаемое приветственное сообщение
|
|
||||||
*required* **::** True
|
|
||||||
*example* **::** `"Hello, I'm a example app"`
|
|
||||||
|
|
||||||
*method mean* **::** устанавливает сообщение, которое будет отображено при запуске программы
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.set_farewell_message(message: str) -> `None`**
|
|
||||||
|
|
||||||
*param* `message: str` **::** устанавливаемое сообщение при выходе
|
|
||||||
*required* **::** True
|
|
||||||
*example* **::** `"GoodBye !"`
|
|
||||||
|
|
||||||
*method mean* **::** устанавливает сообщение, которое будет отображено при выходе
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.set_description_message_pattern(pattern: str) -> `None`**
|
|
||||||
|
|
||||||
*param* `pattern: str` **::** паттерн описания команды при её выводе в консоль
|
|
||||||
*required* **::** True
|
|
||||||
*example* **::** `"[{command}] *=*=* {description}"`
|
|
||||||
|
|
||||||
*method mean* **::** устанавливает паттерн описания команд, который будет использован
|
|
||||||
при выводе в консоль
|
|
||||||
|
|
||||||
---
|
|
||||||
<a name="custom_handler"></a>
|
|
||||||
#### **.set_repeated_input_flags_handler(handler: Callable[[str], None]) -> `None`**
|
|
||||||
|
|
||||||
*param* `handler: Callable[[str], None]` **::** функция или лямбда функция, которой будет передано управление при
|
|
||||||
вводе юзером повторяющихся флагов
|
|
||||||
*required* **::** True
|
|
||||||
*example* **::** `lambda raw_command: print_func(f'Repeated input flags: "{raw_command}"')`
|
|
||||||
|
|
||||||
*method mean* **::** устанавливает функцию, которой будет передано управление при
|
|
||||||
вводе юзером повторяющихся флагов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.set_invalid_input_flags_handler(self, handler: Callable[[str], None]) -> `None`**
|
|
||||||
|
|
||||||
*param* `handler: Callable[[str], None]` **::** функция или лямбда функция, которой будет передано управление при
|
|
||||||
вводе юзером команды с некорректным синтаксисом флагов
|
|
||||||
*required* **::** True
|
|
||||||
*example* **::** `lambda raw_command: print_func(f'Incorrect flag syntax: "{raw_command}"')`
|
|
||||||
|
|
||||||
*method mean* **::** устанавливает функцию, которой будет передано управление при
|
|
||||||
вводе юзером команды с некорректным синтаксисом флагов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.set_unknown_command_handler(self, handler: Callable[[str], None]) -> `None`**
|
|
||||||
|
|
||||||
*param* `handler: Callable[[str], None]` **::** функция или лямбда функция, которой будет передано управление при
|
|
||||||
вводе юзером неизвестной команды
|
|
||||||
*required* **::** True
|
|
||||||
*example* **::** `lambda command: print_func(f"Unknown command: {command.get_string_entity()}")`
|
|
||||||
|
|
||||||
*method mean* **::** устанавливает функцию, которой будет передано управление при
|
|
||||||
вводе юзером неизвестной команды
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.set_empty_command_handler(self, handler: Callable[[str], None]) -> `None`**
|
|
||||||
|
|
||||||
*param* `handler: Callable[[str], None]` **::** функция или лямбда функция, которой будет передано управление при
|
|
||||||
вводе юзером пустой команды
|
|
||||||
*required* **::** True
|
|
||||||
*example* **::** `lambda: print_func(f'Empty input command')`
|
|
||||||
|
|
||||||
*method mean* **::** устанавливает функцию, которой будет передано управление при
|
|
||||||
вводе юзером пустой команды
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Примечания
|
|
||||||
|
|
||||||
- В устанавливаемом паттерне сообщения описания команды необходимы быть два ключевых слова:
|
|
||||||
`command` и `description`, каждое из которых должно быть заключено в фигурные скобки, после обработки
|
|
||||||
паттерна на места этих ключевых слов будут подставлены соответствующие атрибуты команды, при отсутствии
|
|
||||||
этих двух ключевых слов будет вызвано исключение `InvalidDescriptionMessagePatternException`
|
|
||||||
|
|
||||||
- Команды оболочки не должны повторяться, при значении атрибута `ignore_command_register` равным `True`
|
|
||||||
допускается создание обработчиков для разных регистров одинаковых символов в команде, для примера `u` и `U`,
|
|
||||||
при значении атрибута `ignore_command_register` класса `App` равным `False` тот же пример вызывает исключение
|
|
||||||
`RepeatedCommandInDifferentRoutersException`. Исключение вызывается только при наличии пересекающихся команд
|
|
||||||
у __<u>разных</u>__ роутеров
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Исключения
|
|
||||||
|
|
||||||
- `InvalidRouterInstanceException` — Переданный объект в метод `App().include_router()` не является экземпляром класса `Router`.
|
|
||||||
- `InvalidDescriptionMessagePatternException` — Неправильный формат паттерна описания команд.
|
|
||||||
- `IncorrectNumberOfHandlerArgsException` — У обработчика нестандартного поведения зарегистрировано неверное количество аргументов(в большинстве случаев у него должен быть один аргумент).
|
|
||||||
- `NoRegisteredRoutersException` — Отсутствуют зарегистрированные роутеры.
|
|
||||||
- `NoRegisteredHandlersException` — У роутера нет ни одного обработчика команд.
|
|
||||||
- `RepeatedCommandInDifferentRoutersException` — Одна и та же команда зарегистрирована в разных роутерах.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## *class* :: `Router`
|
|
||||||
Класс, который определяет и конфигурирует обработчики команд
|
|
||||||
|
|
||||||
### Конструктор
|
|
||||||
```python
|
|
||||||
Router(title: str = 'Commands group title:',
|
|
||||||
name: str = 'Default')
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Аргументы:**
|
|
||||||
- **name : mean**
|
|
||||||
- `title` (`str`): Заголовок группы команд.
|
|
||||||
- `name` (`str`): Персональное название роутера
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ***methods***
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **command(command: Command)**
|
|
||||||
|
|
||||||
*param* `command: Command` **::** экземпляр класса `Command`, который определяет строковый триггер команды,
|
|
||||||
допустимые флаги команды и другое
|
|
||||||
*required* **::** True
|
|
||||||
*example* **::** `Command(command='ssh', description='connect via ssh')`
|
|
||||||
|
|
||||||
*method mean* **::** декоратор, который регистрирует функцию как обработчик команды
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.get_name() -> `str`**
|
|
||||||
|
|
||||||
*method mean* **::** возвращает установленное название роутера
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.get_title() -> `str`**
|
|
||||||
|
|
||||||
*method mean* **::** возвращает установленный заголовок группы команд данного роутера
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.get_all_commands() -> `list[str]`**
|
|
||||||
|
|
||||||
*method mean* **::** возвращает все зарегистрированные команды для данного роутера
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Исключения
|
|
||||||
- `RepeatedCommandException` - Одна и та же команда зарегистрирована в одном роутере
|
|
||||||
- `RepeatedFlagNameException` - Повторяющиеся зарегистрированные флаги в команде
|
|
||||||
- `TooManyTransferredArgsException` - Слишком много зарегистрированных аргументов у обработчика команды
|
|
||||||
- `RequiredArgumentNotPassedException` - Не зарегистрирован обязательный аргумент у обработчика команды(аргумент, через который будут переданы флаги введённой команды)
|
|
||||||
- `IncorrectNumberOfHandlerArgsException` - У обработчика нестандартного поведения зарегистрировано неверное количество аргументов(в большинстве случаев у него должен быть один аргумент)
|
|
||||||
- `TriggerCannotContainSpacesException` - У регистрируемой команды в триггере содержатся пробелы
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## *class* :: `Command`
|
|
||||||
Класс, экземпляр которого определяет строковый триггер хэндлера и конфигурирует его атрибуты
|
|
||||||
|
|
||||||
### Конструктор
|
|
||||||
```python
|
|
||||||
Command(trigger: str,
|
|
||||||
description: str = None,
|
|
||||||
flags: Flag | FlagsGroup = None)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Аргументы:**
|
|
||||||
- **name : mean**
|
|
||||||
- `trigger` (`str`): Строковый триггер
|
|
||||||
- `description` (`str`): Описание команды, которое будет выведено в консоль при запуске оболочки
|
|
||||||
- `flags` (`Flag | FlagsGroup`): Флаги, которые будут обработаны при их наличии во вводе юзера
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.get_trigger() -> `str`**
|
|
||||||
|
|
||||||
*method mean* **::** возвращает строковый триггер экземпляра
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.get_description() -> `str`**
|
|
||||||
|
|
||||||
*method mean* **::** возвращает описание команды
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.get_registered_flags() -> `FlagsGroup | None`**
|
|
||||||
|
|
||||||
*method mean* **::** возвращает зарегистрированные флаги экземпляра
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Исключения
|
|
||||||
- `UnprocessedInputFlagException` - Некорректный синтаксис ввода команды
|
|
||||||
- `RepeatedInputFlagsException` - Повторяющиеся флаги во введённой команде
|
|
||||||
- `EmptyInputCommandException` - Введённая команда является пустой(не содержит символов)
|
|
||||||
|
|
||||||
**Примечание**
|
|
||||||
Все вышеуказанные исключения класса `Command` вызываются в рантайме запущенным экземпляром класса
|
|
||||||
`App`, а также по дефолту обрабатываются, при желании можно задать пользовательские
|
|
||||||
обработчики для этих исключений ([подробнее см.](#custom_handler))
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## *class* :: `Flag`
|
|
||||||
Класс, экземпляры которого в большинстве случаев передаются при создании
|
|
||||||
экземпляра класса `Command` для регистрации допустимого флага при вводе юзером команды
|
|
||||||
|
|
||||||
### Конструктор
|
|
||||||
```python
|
|
||||||
Flag(flag_name: str,
|
|
||||||
flag_prefix: typing.Literal['-', '--', '---'] = '-',
|
|
||||||
ignore_flag_value_register: bool = False,
|
|
||||||
possible_flag_values: list[str] | typing.Pattern[str] = False)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Аргументы:**
|
|
||||||
- **name : mean**
|
|
||||||
- `flag_name` (`str`): Имя флага
|
|
||||||
- `flag_prefix` (`Literal['-', '--', '---']`): Префикс команды, допустимым значением является от одного до трёх минусов
|
|
||||||
- `ignore_flag_value_register` (`bool`): Будет ли игнорироваться регистр значения введённого флага
|
|
||||||
- `possible_flag_values` (`list[str] | Pattern[str]`): Множество допустимых значений флага, может быть задано
|
|
||||||
списком с допустимыми значениями или регулярным выражением (рекомендуется `re.compile(r'example exspression')`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ***methods***
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.get_string_entity() -> `str`**
|
|
||||||
|
|
||||||
*method mean* **::** возвращает строковое представление флага(префикс + имя)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.get_flag_name() -> `str`**
|
|
||||||
|
|
||||||
*method mean* **::** возвращает имя флага
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.get_flag_prefix() -> `str`**
|
|
||||||
|
|
||||||
*method mean* **::** возвращает префикс флага
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## *class* :: `FlagsGroup`
|
|
||||||
Класс, объединяющий список флагов в один объект, используется в качестве
|
|
||||||
передаваемого аргумента `flags` экземпляру класса `Command`, при регистрации
|
|
||||||
хэндлера
|
|
||||||
|
|
||||||
### Конструктор
|
|
||||||
```python
|
|
||||||
FlagsGroup(flags: list[Flag] = None)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Аргументы:**
|
|
||||||
- **name : mean**
|
|
||||||
- `flags` (`list[Flag]`): Список флагов, которые будут объединены в одну группу
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ***methods***
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **.get_flags() -> `list[Flag]`**
|
|
||||||
|
|
||||||
*method mean* **::** возвращает зарегистрированные флаги
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Тесты
|
|
||||||
|
|
||||||
Запуск тестов:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m unittest discover
|
|
||||||
```
|
|
||||||
or
|
|
||||||
```bash
|
|
||||||
python -m unittest discover -v
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Argenta
|
||||||
|
|
||||||
|
### Библиотека для создания модульных CLI приложeний
|
||||||
|
|
||||||
|
Argenta позволяет инкапсулировать CLI фукциональность в изолированные, абстрагированные **среды**. К примеру: вы создаете утилиту, подобную Metasploit Framework, где пользователь сначала входит в определенный скоуп (например, выбирает модуль для сканирования), а затем получает доступ к набору команд, специфичных только для этого контекста. Argenta предоставляет простой и лаконичный способ для построения такой архитектуры.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Установка
|
||||||
|
```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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .entity import App
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
from typing import Callable
|
|
||||||
from inspect import getfullargspec
|
|
||||||
import re
|
|
||||||
|
|
||||||
from ..command.entity import Command
|
|
||||||
from ..router.entity import Router
|
|
||||||
from ..command.exceptions import (UnprocessedInputFlagException,
|
|
||||||
RepeatedInputFlagsException,
|
|
||||||
EmptyInputCommandException)
|
|
||||||
from .exceptions import (InvalidRouterInstanceException,
|
|
||||||
InvalidDescriptionMessagePatternException,
|
|
||||||
NoRegisteredRoutersException,
|
|
||||||
NoRegisteredHandlersException,
|
|
||||||
RepeatedCommandInDifferentRoutersException,
|
|
||||||
IncorrectNumberOfHandlerArgsException)
|
|
||||||
|
|
||||||
|
|
||||||
class App:
|
|
||||||
def __init__(self,
|
|
||||||
prompt: str = 'Enter a command',
|
|
||||||
initial_message: str = '\nHello, I am Argenta\n',
|
|
||||||
farewell_message: str = '\nGoodBye\n',
|
|
||||||
invalid_input_flags_message: str = 'Invalid input flags',
|
|
||||||
exit_command: str = 'Q',
|
|
||||||
exit_command_description: str = 'Exit command',
|
|
||||||
system_points_title: str = 'System points:',
|
|
||||||
ignore_exit_command_register: bool = True,
|
|
||||||
ignore_command_register: bool = False,
|
|
||||||
line_separate: str = '',
|
|
||||||
command_group_description_separate: str = '',
|
|
||||||
repeat_command_groups: bool = True,
|
|
||||||
print_func: Callable[[str], None] = print) -> None:
|
|
||||||
self.prompt = prompt
|
|
||||||
self.print_func = print_func
|
|
||||||
self.exit_command = exit_command
|
|
||||||
self.exit_command_description = exit_command_description
|
|
||||||
self.system_points_title = system_points_title
|
|
||||||
self.ignore_exit_command_register = ignore_exit_command_register
|
|
||||||
self.farewell_message = farewell_message
|
|
||||||
self.initial_message = initial_message
|
|
||||||
self.invalid_input_flags_message = invalid_input_flags_message
|
|
||||||
self.line_separate = line_separate
|
|
||||||
self.command_group_description_separate = command_group_description_separate
|
|
||||||
self.ignore_command_register = ignore_command_register
|
|
||||||
self.repeat_command_groups = repeat_command_groups
|
|
||||||
|
|
||||||
self._routers: list[Router] = []
|
|
||||||
self._description_message_pattern: str = '[{command}] *=*=* {description}'
|
|
||||||
self._registered_router_entities: list[dict[str, str | list[dict[str, Callable[[], None] | Command]] | Router]] = []
|
|
||||||
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(f'Empty input command')
|
|
||||||
self._unknown_command_handler: Callable[[Command], None] = lambda command: print_func(f"Unknown command: {command.get_string_entity()}")
|
|
||||||
|
|
||||||
|
|
||||||
def start_polling(self) -> None:
|
|
||||||
self._validate_number_of_routers()
|
|
||||||
self._validate_included_routers()
|
|
||||||
self._validate_all_router_commands()
|
|
||||||
|
|
||||||
self.print_func(self.initial_message)
|
|
||||||
|
|
||||||
if not self.repeat_command_groups:
|
|
||||||
self._print_command_group_description()
|
|
||||||
self.print_func(self.prompt)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if self.repeat_command_groups:
|
|
||||||
self._print_command_group_description()
|
|
||||||
self.print_func(self.prompt)
|
|
||||||
|
|
||||||
raw_command: str = input()
|
|
||||||
|
|
||||||
try:
|
|
||||||
input_command: Command = Command.parse_input_command(raw_command=raw_command)
|
|
||||||
except UnprocessedInputFlagException:
|
|
||||||
self.print_func(self.line_separate)
|
|
||||||
self._invalid_input_flags_handler(raw_command)
|
|
||||||
self.print_func(self.line_separate)
|
|
||||||
|
|
||||||
if not self.repeat_command_groups:
|
|
||||||
self.print_func(self.prompt)
|
|
||||||
continue
|
|
||||||
|
|
||||||
except RepeatedInputFlagsException:
|
|
||||||
self.print_func(self.line_separate)
|
|
||||||
self._repeated_input_flags_handler(raw_command)
|
|
||||||
self.print_func(self.line_separate)
|
|
||||||
|
|
||||||
if not self.repeat_command_groups:
|
|
||||||
self.print_func(self.prompt)
|
|
||||||
continue
|
|
||||||
|
|
||||||
except EmptyInputCommandException:
|
|
||||||
self.print_func(self.line_separate)
|
|
||||||
self._empty_input_command_handler()
|
|
||||||
self.print_func(self.line_separate)
|
|
||||||
|
|
||||||
if not self.repeat_command_groups:
|
|
||||||
self.print_func(self.prompt)
|
|
||||||
continue
|
|
||||||
|
|
||||||
self._check_command_for_exit_command(input_command.get_trigger())
|
|
||||||
|
|
||||||
self.print_func(self.line_separate)
|
|
||||||
is_unknown_command: bool = self._check_is_command_unknown(input_command)
|
|
||||||
|
|
||||||
if is_unknown_command:
|
|
||||||
self.print_func(self.line_separate)
|
|
||||||
self.print_func(self.command_group_description_separate)
|
|
||||||
if not self.repeat_command_groups:
|
|
||||||
self.print_func(self.prompt)
|
|
||||||
continue
|
|
||||||
|
|
||||||
for router in self._routers:
|
|
||||||
router.input_command_handler(input_command)
|
|
||||||
|
|
||||||
self.print_func(self.line_separate)
|
|
||||||
self.print_func(self.command_group_description_separate)
|
|
||||||
if not self.repeat_command_groups:
|
|
||||||
self.print_func(self.prompt)
|
|
||||||
|
|
||||||
|
|
||||||
def set_initial_message(self, message: str) -> None:
|
|
||||||
self.initial_message: str = message
|
|
||||||
|
|
||||||
|
|
||||||
def set_farewell_message(self, message: str) -> None:
|
|
||||||
self.farewell_message: str = message
|
|
||||||
|
|
||||||
|
|
||||||
def set_description_message_pattern(self, pattern: str) -> None:
|
|
||||||
first_check = re.match(r'.*{command}.*', pattern)
|
|
||||||
second_check = re.match(r'.*{description}.*', pattern)
|
|
||||||
|
|
||||||
if bool(first_check) and bool(second_check):
|
|
||||||
self._description_message_pattern: str = pattern
|
|
||||||
else:
|
|
||||||
raise InvalidDescriptionMessagePatternException(pattern)
|
|
||||||
|
|
||||||
|
|
||||||
def set_invalid_input_flags_handler(self, handler: Callable[[str], None]) -> None:
|
|
||||||
args = getfullargspec(handler).args
|
|
||||||
if len(args) != 1:
|
|
||||||
raise IncorrectNumberOfHandlerArgsException()
|
|
||||||
else:
|
|
||||||
self._invalid_input_flags_handler = handler
|
|
||||||
|
|
||||||
|
|
||||||
def set_repeated_input_flags_handler(self, handler: Callable[[str], None]) -> None:
|
|
||||||
args = getfullargspec(handler).args
|
|
||||||
if len(args) != 1:
|
|
||||||
raise IncorrectNumberOfHandlerArgsException()
|
|
||||||
else:
|
|
||||||
self._repeated_input_flags_handler = handler
|
|
||||||
|
|
||||||
|
|
||||||
def set_unknown_command_handler(self, handler: Callable[[str], None]) -> None:
|
|
||||||
args = getfullargspec(handler).args
|
|
||||||
if len(args) != 1:
|
|
||||||
raise IncorrectNumberOfHandlerArgsException()
|
|
||||||
else:
|
|
||||||
self._unknown_command_handler = handler
|
|
||||||
|
|
||||||
|
|
||||||
def set_empty_command_handler(self, handler: Callable[[str], None]) -> None:
|
|
||||||
args = getfullargspec(handler).args
|
|
||||||
if len(args) != 1:
|
|
||||||
raise IncorrectNumberOfHandlerArgsException()
|
|
||||||
else:
|
|
||||||
self._empty_input_command_handler = handler
|
|
||||||
|
|
||||||
|
|
||||||
def include_router(self, router: Router) -> None:
|
|
||||||
if not isinstance(router, Router):
|
|
||||||
raise InvalidRouterInstanceException()
|
|
||||||
|
|
||||||
router.set_ignore_command_register(self.ignore_command_register)
|
|
||||||
self._routers.append(router)
|
|
||||||
|
|
||||||
command_entities: list[dict[str, Callable[[], None] | Command]] = router.get_command_entities()
|
|
||||||
self._registered_router_entities.append({'name': router.get_name(),
|
|
||||||
'title': router.get_title(),
|
|
||||||
'entity': router,
|
|
||||||
'commands': command_entities})
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_number_of_routers(self) -> None:
|
|
||||||
if not self._routers:
|
|
||||||
raise NoRegisteredRoutersException()
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_included_routers(self) -> None:
|
|
||||||
for router in self._routers:
|
|
||||||
if not router.get_command_entities():
|
|
||||||
raise NoRegisteredHandlersException(router.get_name())
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_all_router_commands(self) -> None:
|
|
||||||
for idx in range(len(self._registered_router_entities)):
|
|
||||||
current_router: Router = self._registered_router_entities[idx]['entity']
|
|
||||||
routers_without_current_router = self._registered_router_entities.copy()
|
|
||||||
routers_without_current_router.pop(idx)
|
|
||||||
|
|
||||||
current_router_all_commands: list[str] = current_router.get_all_commands()
|
|
||||||
|
|
||||||
for router_entity in routers_without_current_router:
|
|
||||||
if len(set(current_router_all_commands).intersection(set(router_entity['entity'].get_all_commands()))) > 0:
|
|
||||||
raise RepeatedCommandInDifferentRoutersException()
|
|
||||||
if self.ignore_command_register:
|
|
||||||
if len(set([x.lower() for x in current_router_all_commands]).intersection(set([x.lower() for x in router_entity['entity'].get_all_commands()]))) > 0:
|
|
||||||
raise RepeatedCommandInDifferentRoutersException()
|
|
||||||
|
|
||||||
|
|
||||||
def _check_command_for_exit_command(self, command: str):
|
|
||||||
if command.lower() == self.exit_command.lower():
|
|
||||||
if self.ignore_exit_command_register:
|
|
||||||
self.print_func(self.farewell_message)
|
|
||||||
exit(0)
|
|
||||||
else:
|
|
||||||
if command == self.exit_command:
|
|
||||||
self.print_func(self.farewell_message)
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_is_command_unknown(self, command: Command):
|
|
||||||
registered_router_entities: list[dict[str, str | list[dict[str, Callable[[], None] | Command]] | Router]] = self._registered_router_entities
|
|
||||||
for router_entity in registered_router_entities:
|
|
||||||
for command_entity in router_entity['commands']:
|
|
||||||
if command_entity['command'].get_trigger().lower() == command.get_trigger().lower():
|
|
||||||
if self.ignore_command_register:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
if command_entity['command'].get_trigger() == command.get_trigger():
|
|
||||||
return False
|
|
||||||
self._unknown_command_handler(command)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _print_command_group_description(self):
|
|
||||||
for router_entity in self._registered_router_entities:
|
|
||||||
self.print_func(router_entity['title'])
|
|
||||||
for command_entity in router_entity['commands']:
|
|
||||||
self.print_func(self._description_message_pattern.format(
|
|
||||||
command=command_entity['command'].get_trigger(),
|
|
||||||
description=command_entity['command'].get_description()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.print_func(self.command_group_description_separate)
|
|
||||||
|
|
||||||
self.print_func(self.system_points_title)
|
|
||||||
self.print_func(self._description_message_pattern.format(
|
|
||||||
command=self.exit_command,
|
|
||||||
description=self.exit_command_description
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.print_func(self.command_group_description_separate)
|
|
||||||
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
class InvalidRouterInstanceException(Exception):
|
|
||||||
def __str__(self):
|
|
||||||
return "Invalid Router Instance"
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDescriptionMessagePatternException(Exception):
|
|
||||||
def __init__(self, pattern: str):
|
|
||||||
self.pattern = pattern
|
|
||||||
def __str__(self):
|
|
||||||
return ("Invalid Description Message Pattern\n"
|
|
||||||
"Correct pattern example: [{command}] *=*=* {description}\n"
|
|
||||||
"The pattern must contain two variables: `command` and `description` - description of the command\n"
|
|
||||||
f"Your pattern: {self.pattern}")
|
|
||||||
|
|
||||||
|
|
||||||
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}'"
|
|
||||||
|
|
||||||
|
|
||||||
class RepeatedCommandInDifferentRoutersException(Exception):
|
|
||||||
def __str__(self):
|
|
||||||
return "Commands in different handlers cannot be repeated"
|
|
||||||
|
|
||||||
|
|
||||||
class IncorrectNumberOfHandlerArgsException(Exception):
|
|
||||||
def __str__(self):
|
|
||||||
return "Incorrect Input Flags Handler has incorrect number of arguments"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .entity import Command
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
from .params.flag.entity import Flag
|
|
||||||
from .params.flag.flags_group.entity import FlagsGroup
|
|
||||||
from .exceptions import (UnprocessedInputFlagException,
|
|
||||||
RepeatedInputFlagsException,
|
|
||||||
EmptyInputCommandException)
|
|
||||||
|
|
||||||
from typing import Generic, TypeVar
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
|
||||||
|
|
||||||
|
|
||||||
class Command(Generic[T]):
|
|
||||||
def __init__(self, trigger: str,
|
|
||||||
description: str = None,
|
|
||||||
flags: Flag | FlagsGroup = None):
|
|
||||||
self._trigger = trigger
|
|
||||||
self._description = f'description for "{self._trigger}" command' if not description else description
|
|
||||||
self._registered_flags: FlagsGroup | None = flags if isinstance(flags, FlagsGroup) else FlagsGroup([flags]) if isinstance(flags, Flag) else flags
|
|
||||||
|
|
||||||
self._input_flags: FlagsGroup | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_trigger(self) -> str:
|
|
||||||
return self._trigger
|
|
||||||
|
|
||||||
|
|
||||||
def get_description(self) -> str:
|
|
||||||
return self._description
|
|
||||||
|
|
||||||
|
|
||||||
def get_registered_flags(self) -> FlagsGroup | None:
|
|
||||||
return self._registered_flags
|
|
||||||
|
|
||||||
|
|
||||||
def validate_input_flag(self, flag: Flag):
|
|
||||||
registered_flags: FlagsGroup | None = self.get_registered_flags()
|
|
||||||
if registered_flags:
|
|
||||||
if isinstance(registered_flags, Flag):
|
|
||||||
if registered_flags.get_string_entity() == flag.get_string_entity():
|
|
||||||
is_valid = registered_flags.validate_input_flag_value(flag.get_value())
|
|
||||||
if is_valid:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
for registered_flag in registered_flags:
|
|
||||||
if registered_flag.get_string_entity() == flag.get_string_entity():
|
|
||||||
is_valid = registered_flag.validate_input_flag_value(flag.get_value())
|
|
||||||
if is_valid:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _set_input_flags(self, input_flags: FlagsGroup):
|
|
||||||
self._input_flags = input_flags
|
|
||||||
|
|
||||||
def get_input_flags(self) -> FlagsGroup:
|
|
||||||
return self._input_flags
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_input_command(raw_command: str) -> 'Command[T]':
|
|
||||||
if not raw_command:
|
|
||||||
raise EmptyInputCommandException()
|
|
||||||
list_of_tokens = raw_command.split()
|
|
||||||
command = list_of_tokens[0]
|
|
||||||
list_of_tokens.pop(0)
|
|
||||||
|
|
||||||
flags: FlagsGroup = FlagsGroup()
|
|
||||||
current_flag_name = None
|
|
||||||
current_flag_value = None
|
|
||||||
for _ in list_of_tokens:
|
|
||||||
if _.startswith('-'):
|
|
||||||
flag_prefix_last_symbol_index = _.rfind('-')
|
|
||||||
if current_flag_name or len(_) < 2 or len(_[:flag_prefix_last_symbol_index]) > 3:
|
|
||||||
raise UnprocessedInputFlagException()
|
|
||||||
else:
|
|
||||||
current_flag_name = _
|
|
||||||
else:
|
|
||||||
if not current_flag_name:
|
|
||||||
raise UnprocessedInputFlagException()
|
|
||||||
else:
|
|
||||||
current_flag_value = _
|
|
||||||
if current_flag_name and current_flag_value:
|
|
||||||
flag_prefix_last_symbol_index = current_flag_name.rfind('-')
|
|
||||||
flag_prefix = current_flag_name[:flag_prefix_last_symbol_index+1]
|
|
||||||
flag_name = current_flag_name[flag_prefix_last_symbol_index+1:]
|
|
||||||
input_flag = Flag(flag_name=flag_name,
|
|
||||||
flag_prefix=flag_prefix)
|
|
||||||
input_flag.set_value(current_flag_value)
|
|
||||||
|
|
||||||
all_flags = [x.get_string_entity() for x in flags.get_flags()]
|
|
||||||
if input_flag.get_string_entity() not in all_flags:
|
|
||||||
flags.add_flag(input_flag)
|
|
||||||
else:
|
|
||||||
raise RepeatedInputFlagsException(input_flag)
|
|
||||||
|
|
||||||
current_flag_name = None
|
|
||||||
current_flag_value = None
|
|
||||||
if any([current_flag_name, current_flag_value]):
|
|
||||||
raise UnprocessedInputFlagException()
|
|
||||||
if len(flags.get_flags()) == 0:
|
|
||||||
return Command(trigger=command)
|
|
||||||
else:
|
|
||||||
input_command = Command(trigger=command)
|
|
||||||
input_command._set_input_flags(flags)
|
|
||||||
return input_command
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from .params.flag.entity import Flag
|
|
||||||
|
|
||||||
|
|
||||||
class UnprocessedInputFlagException(Exception):
|
|
||||||
def __str__(self):
|
|
||||||
return "Unprocessed Input Flags"
|
|
||||||
|
|
||||||
|
|
||||||
class RepeatedInputFlagsException(Exception):
|
|
||||||
def __init__(self, flag: Flag):
|
|
||||||
self.flag = flag
|
|
||||||
def __str__(self):
|
|
||||||
return ("Repeated Input Flags\n"
|
|
||||||
f"Duplicate flag was detected in the input: '{self.flag.get_string_entity()}'")
|
|
||||||
|
|
||||||
|
|
||||||
class EmptyInputCommandException(Exception):
|
|
||||||
def __str__(self):
|
|
||||||
return "Input Command is empty"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from .entity import Flag
|
|
||||||
from .flags_group.entity import FlagsGroup
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
from typing import Literal, Pattern
|
|
||||||
|
|
||||||
|
|
||||||
class Flag:
|
|
||||||
def __init__(self, flag_name: str,
|
|
||||||
flag_prefix: Literal['-', '--', '---'] = '--',
|
|
||||||
ignore_flag_value_register: bool = False,
|
|
||||||
possible_flag_values: list[str] | Pattern[str] = False):
|
|
||||||
self._flag_name = flag_name
|
|
||||||
self._flag_prefix = flag_prefix
|
|
||||||
self.possible_flag_values = possible_flag_values
|
|
||||||
self.ignore_flag_value_register = ignore_flag_value_register
|
|
||||||
|
|
||||||
self._flag_value = None
|
|
||||||
|
|
||||||
def get_string_entity(self):
|
|
||||||
string_entity: str = self._flag_prefix + self._flag_name
|
|
||||||
return string_entity
|
|
||||||
|
|
||||||
def get_flag_name(self):
|
|
||||||
return self._flag_name
|
|
||||||
|
|
||||||
def get_flag_prefix(self):
|
|
||||||
return self._flag_prefix
|
|
||||||
|
|
||||||
def get_value(self):
|
|
||||||
return self._flag_value
|
|
||||||
|
|
||||||
def set_value(self, value):
|
|
||||||
self._flag_value = value
|
|
||||||
|
|
||||||
def validate_input_flag_value(self, input_flag_value: str):
|
|
||||||
if isinstance(self.possible_flag_values, Pattern):
|
|
||||||
is_valid = bool(self.possible_flag_values.match(input_flag_value))
|
|
||||||
if bool(is_valid):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if isinstance(self.possible_flag_values, list):
|
|
||||||
if self.ignore_flag_value_register:
|
|
||||||
if input_flag_value.lower() in [x.lower() for x in self.possible_flag_values]:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
if input_flag_value in self.possible_flag_values:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
from argenta.command.params.flag.entity import Flag
|
|
||||||
|
|
||||||
|
|
||||||
class FlagsGroup:
|
|
||||||
def __init__(self, flags: list[Flag] = None):
|
|
||||||
self._flags: list[Flag] = [] if not flags else flags
|
|
||||||
|
|
||||||
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 unparse_to_dict(self):
|
|
||||||
result_dict: dict[str, dict] = {}
|
|
||||||
for flag in self._flags:
|
|
||||||
result_dict[flag.get_flag_name()] = {
|
|
||||||
'name': flag.get_flag_name(),
|
|
||||||
'string_entity': flag.get_string_entity(),
|
|
||||||
'prefix': flag.get_flag_prefix(),
|
|
||||||
'value': flag.get_value()
|
|
||||||
}
|
|
||||||
return result_dict
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(self._flags)
|
|
||||||
|
|
||||||
def __next__(self):
|
|
||||||
return next(iter(self))
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
return self._flags[item]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .entity import Router
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
from typing import Callable, Any
|
|
||||||
from inspect import getfullargspec
|
|
||||||
|
|
||||||
from ..command.entity import Command
|
|
||||||
from ..command.params.flag.entity import Flag
|
|
||||||
from ..command.params.flag.flags_group.entity import FlagsGroup
|
|
||||||
from ..router.exceptions import (RepeatedCommandException,
|
|
||||||
RepeatedFlagNameException,
|
|
||||||
TooManyTransferredArgsException,
|
|
||||||
RequiredArgumentNotPassedException,
|
|
||||||
IncorrectNumberOfHandlerArgsException,
|
|
||||||
TriggerCannotContainSpacesException)
|
|
||||||
|
|
||||||
|
|
||||||
class Router:
|
|
||||||
def __init__(self,
|
|
||||||
title: str = 'Commands group title:',
|
|
||||||
name: str = 'Default'):
|
|
||||||
|
|
||||||
self._title = title
|
|
||||||
self._name = name
|
|
||||||
|
|
||||||
self._command_entities: list[dict[str, Callable[[], None] | Command]] = []
|
|
||||||
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()}'")
|
|
||||||
|
|
||||||
|
|
||||||
def command(self, command: Command) -> Callable[[Any], Any]:
|
|
||||||
self._validate_command(command)
|
|
||||||
|
|
||||||
def command_decorator(func):
|
|
||||||
Router._validate_func_args(command, func)
|
|
||||||
self._command_entities.append({'handler_func': func,
|
|
||||||
'command': 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: Command):
|
|
||||||
input_command_name: str = input_command.get_trigger()
|
|
||||||
input_command_flags: FlagsGroup = input_command.get_input_flags()
|
|
||||||
for command_entity in self._command_entities:
|
|
||||||
if input_command_name.lower() == command_entity['command'].get_trigger().lower():
|
|
||||||
if command_entity['command'].get_registered_flags():
|
|
||||||
if input_command_flags:
|
|
||||||
for flag in input_command_flags:
|
|
||||||
is_valid = command_entity['command'].validate_input_flag(flag)
|
|
||||||
if not is_valid:
|
|
||||||
self._not_valid_flag_handler(flag)
|
|
||||||
return
|
|
||||||
return command_entity['handler_func'](input_command_flags.unparse_to_dict())
|
|
||||||
else:
|
|
||||||
return command_entity['handler_func']({})
|
|
||||||
else:
|
|
||||||
if input_command_flags:
|
|
||||||
self._not_valid_flag_handler(input_command_flags[0])
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
return command_entity['handler_func']()
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_command(self, command: Command):
|
|
||||||
command_name: str = command.get_trigger()
|
|
||||||
if command_name.find(' ') != -1:
|
|
||||||
raise TriggerCannotContainSpacesException()
|
|
||||||
if command_name in self.get_all_commands():
|
|
||||||
raise RepeatedCommandException()
|
|
||||||
if self._ignore_command_register:
|
|
||||||
if command_name.lower() in [x.lower() for x in self.get_all_commands()]:
|
|
||||||
raise RepeatedCommandException()
|
|
||||||
|
|
||||||
flags: FlagsGroup = 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 and transferred_args:
|
|
||||||
if len(transferred_args) != 1:
|
|
||||||
raise TooManyTransferredArgsException()
|
|
||||||
elif registered_args and not transferred_args:
|
|
||||||
raise RequiredArgumentNotPassedException()
|
|
||||||
elif not registered_args and transferred_args:
|
|
||||||
raise TooManyTransferredArgsException()
|
|
||||||
|
|
||||||
|
|
||||||
def set_ignore_command_register(self, ignore_command_register: bool):
|
|
||||||
self._ignore_command_register = ignore_command_register
|
|
||||||
|
|
||||||
|
|
||||||
def get_command_entities(self) -> list[dict[str, Callable[[], None] | Command]]:
|
|
||||||
return self._command_entities
|
|
||||||
|
|
||||||
|
|
||||||
def get_name(self) -> str:
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
|
|
||||||
def get_title(self) -> str:
|
|
||||||
return self._title
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_commands(self) -> list[str]:
|
|
||||||
all_commands: list[str] = []
|
|
||||||
for command_entity in self._command_entities:
|
|
||||||
all_commands.append(command_entity['command'].get_trigger())
|
|
||||||
|
|
||||||
return all_commands
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
class RepeatedCommandException(Exception):
|
|
||||||
def __str__(self):
|
|
||||||
return "Commands in handler cannot be repeated"
|
|
||||||
|
|
||||||
|
|
||||||
class RepeatedFlagNameException(Exception):
|
|
||||||
def __str__(self):
|
|
||||||
return "Repeated 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"
|
|
||||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 928 KiB |
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="809.000000pt" height="809.000000pt" viewBox="0 0 809.000000 809.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
|
||||||
|
<g transform="translate(0.000000,809.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M3845 7600 c-683 -38 -1333 -259 -1875 -639 -702 -492 -1223 -1251
|
||||||
|
-1434 -2089 -83 -330 -111 -602 -103 -980 6 -273 19 -393 67 -632 167 -829
|
||||||
|
642 -1593 1321 -2122 512 -399 1133 -656 1794 -745 165 -22 666 -25 825 -5
|
||||||
|
779 99 1451 397 2020 898 687 603 1095 1401 1201 2344 17 147 17 571 0 715
|
||||||
|
-59 518 -204 977 -442 1402 -180 322 -355 552 -629 823 -518 515 -1164 850
|
||||||
|
-1887 979 -254 45 -594 66 -858 51z m655 -234 c545 -76 1005 -248 1445 -541
|
||||||
|
195 -130 336 -245 506 -415 457 -456 758 -987 909 -1605 134 -547 132 -1119
|
||||||
|
-5 -1665 -149 -593 -445 -1112 -885 -1550 -624 -623 -1414 -966 -2315 -1006
|
||||||
|
-412 -18 -935 73 -1339 232 -656 259 -1228 720 -1610 1299 -298 450 -461 890
|
||||||
|
-538 1451 -28 210 -31 620 -4 819 103 786 437 1477 975 2016 149 149 266 249
|
||||||
|
417 357 315 225 692 405 1059 506 211 58 342 81 675 120 87 10 601 -3 710 -18z"/>
|
||||||
|
<path d="M3691 6759 c-231 -17 -522 -67 -660 -114 -227 -77 -354 -211 -381
|
||||||
|
-400 -14 -105 -13 -628 2 -643 9 -9 158 -12 611 -12 598 0 628 -2 649 -34 14
|
||||||
|
-21 8 -66 -12 -86 -20 -20 -33 -20 -908 -20 -857 0 -892 -1 -967 -20 -294 -75
|
||||||
|
-500 -321 -599 -715 -49 -195 -61 -309 -60 -585 0 -221 3 -272 23 -385 44
|
||||||
|
-251 116 -418 232 -540 78 -82 143 -123 254 -162 78 -27 85 -27 351 -31 l272
|
||||||
|
-4 16 23 c14 20 16 58 16 259 0 334 20 446 107 613 80 153 268 310 447 374
|
||||||
|
128 45 138 46 881 52 683 7 713 8 786 28 201 56 353 177 437 348 80 162 86
|
||||||
|
240 74 905 -11 608 -10 600 -84 743 -80 153 -240 271 -453 332 -252 73 -653
|
||||||
|
101 -1034 74z m-383 -466 c126 -78 147 -245 44 -355 -144 -154 -396 -54 -395
|
||||||
|
157 0 97 62 187 154 222 54 20 143 10 197 -24z"/>
|
||||||
|
<path d="M5451 5491 c-20 -20 -21 -30 -21 -283 0 -276 -7 -351 -42 -458 -81
|
||||||
|
-251 -281 -454 -523 -534 -149 -48 -151 -48 -890 -56 -678 -6 -703 -7 -780
|
||||||
|
-28 -139 -38 -219 -84 -315 -181 -61 -61 -95 -105 -117 -151 -65 -134 -64
|
||||||
|
-129 -75 -780 -12 -665 -10 -694 47 -817 122 -265 419 -429 895 -494 127 -18
|
||||||
|
592 -18 730 -1 377 48 646 169 785 355 57 75 67 94 96 182 20 64 23 94 27 325
|
||||||
|
4 238 3 257 -15 278 l-18 23 -551 -1 c-399 0 -559 3 -577 11 -51 23 -56 102
|
||||||
|
-8 126 9 4 439 10 956 13 1044 7 981 2 1125 75 178 89 305 255 386 502 84 257
|
||||||
|
111 618 73 973 -39 364 -167 655 -351 801 -63 50 -121 80 -213 110 -64 20 -93
|
||||||
|
23 -337 27 -260 4 -267 3 -287 -17z m-2104 -1698 c78 -52 531 -413 561 -447
|
||||||
|
17 -19 33 -48 37 -65 15 -71 3 -84 -323 -356 -275 -229 -310 -255 -342 -255
|
||||||
|
-78 1 -112 75 -61 134 9 10 86 75 171 144 247 201 355 294 358 311 1 9 -15 28
|
||||||
|
-38 44 -127 89 -503 393 -512 413 -23 50 20 103 82 104 15 0 45 -12 67 -27z
|
||||||
|
m1388 -1244 c59 -15 129 -77 151 -134 36 -96 0 -204 -88 -262 -36 -24 -51 -28
|
||||||
|
-118 -28 -67 0 -82 4 -118 28 -153 101 -124 338 48 392 57 17 71 18 125 4z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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): # pyright: ignore[reportUnusedFunction, reportUnusedParameter]
|
||||||
|
pass
|
||||||
|
|
||||||
|
app = App()
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
return get_time_of_pre_cycle_setup(app)
|
||||||
|
|
||||||
|
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): # pyright: ignore[reportUnusedFunction, reportUnusedParameter]
|
||||||
|
pass
|
||||||
|
|
||||||
|
app = App()
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
return get_time_of_pre_cycle_setup(app)
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
import re
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(prog='myprogram')
|
||||||
def set_description_message_pattern(pattern: str) -> None:
|
_ = parser.add_argument('--foo', help='foo of the %(prog)s program')
|
||||||
first_check = re.match(r'.*command.*', pattern)
|
parser.print_help()
|
||||||
second_check = re.match(r'.*{description}.*', pattern)
|
|
||||||
if bool(first_check) and bool(second_check):
|
|
||||||
print('Success')
|
|
||||||
|
|
||||||
|
|
||||||
set_description_message_pattern('Invalid des{ommand}cription pattern')
|
|
||||||
@@ -1,10 +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,47 +0,0 @@
|
|||||||
import re
|
|
||||||
from pprint import pprint
|
|
||||||
from rich.console import Console
|
|
||||||
|
|
||||||
from argenta.command.entity import Command
|
|
||||||
from argenta.command.params.flag.entity import Flag
|
|
||||||
from argenta.command.params.flag.flags_group.entity import FlagsGroup
|
|
||||||
from argenta.router import Router
|
|
||||||
from .handlers_implementation.help_command import help_command
|
|
||||||
|
|
||||||
|
|
||||||
work_router: Router = Router(title='Work nts:')
|
|
||||||
work_router.set_invalid_input_flag_handler(lambda flag: print(f'Invalid input flag: "{flag.get_string_entity()} {flag.get_value()}"'))
|
|
||||||
|
|
||||||
settings_router: Router = Router(title='Settings points:')
|
|
||||||
|
|
||||||
|
|
||||||
console = Console()
|
|
||||||
|
|
||||||
|
|
||||||
flags = FlagsGroup(flags=[
|
|
||||||
Flag(flag_name='host',
|
|
||||||
flag_prefix='--',
|
|
||||||
possible_flag_values=re.compile(r'^192.168.\d{1,3}.\d{1,3}$')),
|
|
||||||
Flag(flag_name='port',
|
|
||||||
flag_prefix='--', )
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
@work_router.command(Command(trigger='0', description='Get Help'))
|
|
||||||
def command_help():
|
|
||||||
help_command()
|
|
||||||
|
|
||||||
|
|
||||||
@work_router.command(Command(trigger='--gbP', description='Start Solving', flags=flags))
|
|
||||||
def command_start_solving(args: dict):
|
|
||||||
print('Solving...')
|
|
||||||
pprint(args)
|
|
||||||
#start_solving_command()
|
|
||||||
|
|
||||||
|
|
||||||
@settings_router.command(Command(trigger='G', description='Update WordMath'))
|
|
||||||
def command_update():
|
|
||||||
print('Command update')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,40 +1,29 @@
|
|||||||
from mock.mock_app.handlers.routers import work_router, settings_router
|
from mock.mock_app.routers import work_router
|
||||||
from art import text2art
|
|
||||||
from rich.console import Console
|
|
||||||
|
|
||||||
from argenta.app import App
|
from argenta import App, Orchestrator
|
||||||
from argenta.router import Router
|
from argenta.app import PredefinedMessages, DynamicDividingLine, AutoCompleter
|
||||||
from argenta.command import Command
|
from argenta.orchestrator import ArgParser
|
||||||
from argenta.command.params.flag import Flag, FlagsGroup
|
from argenta.orchestrator.argparser import BooleanArgument
|
||||||
|
|
||||||
|
|
||||||
app: App = App(prompt='[italic white bold]What do you want to do(enter number of action)?',
|
arg_parser = ArgParser(processed_args=[BooleanArgument("repeat")])
|
||||||
line_separate=f'\n{"[bold green]-[/bold green][bold red]-[/bold red]"*25}\n',
|
app: App = App(
|
||||||
print_func=Console().print,
|
dividing_line=DynamicDividingLine(),
|
||||||
command_group_description_separate='',
|
autocompleter=AutoCompleter(),
|
||||||
repeat_command_groups=True)
|
)
|
||||||
|
orchestrator: Orchestrator = Orchestrator(arg_parser)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
ascii_name: str = text2art('WordMath', font='nancyj')
|
|
||||||
initial_greeting: str = f'[bold red]\n\n{ascii_name}'
|
|
||||||
|
|
||||||
ascii_goodbye_message: str = text2art('GoodBye', font='small')
|
|
||||||
goodbye_message: str = f'[bold red]\n{ascii_goodbye_message}{' '*12}made by kolo\n'
|
|
||||||
|
|
||||||
app.include_router(work_router)
|
app.include_router(work_router)
|
||||||
app.include_router(settings_router)
|
print(f"\n\n{orchestrator.get_input_args()}")
|
||||||
|
|
||||||
app.set_initial_message(initial_greeting)
|
app.add_message_on_startup(PredefinedMessages.USAGE)
|
||||||
app.set_farewell_message(goodbye_message)
|
app.add_message_on_startup(PredefinedMessages.AUTOCOMPLETE)
|
||||||
|
app.add_message_on_startup(PredefinedMessages.HELP)
|
||||||
|
|
||||||
app.set_invalid_input_flags_handler(lambda raw_command: print(f"Invalid input flags: {raw_command}"))
|
orchestrator.start_polling(app)
|
||||||
app.set_unknown_command_handler(lambda command: print(f"Unknown command: {command.get_string_entity()}"))
|
|
||||||
app.set_repeated_input_flags_handler(lambda raw_command: print(f"Repeated input flags: {raw_command}"))
|
|
||||||
|
|
||||||
app.set_description_message_pattern('[bold red][{command}][/bold red] [blue]*=*=*[/blue] [bold yellow italic]{description}')
|
|
||||||
|
|
||||||
app.start_polling()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from argenta.command import Command, PredefinedFlags, Flags, Flag, PossibleValues
|
||||||
|
from argenta.response import Response
|
||||||
|
from argenta import Router
|
||||||
|
|
||||||
|
|
||||||
|
work_router: Router = Router(title="Work points:")
|
||||||
|
|
||||||
|
flag = Flag('csdv', possible_values=PossibleValues.NEITHER)
|
||||||
|
|
||||||
|
|
||||||
|
@work_router.command(
|
||||||
|
Command("get",
|
||||||
|
description="Get Help",
|
||||||
|
aliases=["help", "Get_help"],
|
||||||
|
flags=Flags([PredefinedFlags.PORT,
|
||||||
|
PredefinedFlags.HOST])))
|
||||||
|
def command_help(response: Response):
|
||||||
|
print(response.status)
|
||||||
|
print(response.input_flags.flags)
|
||||||
|
|
||||||
|
|
||||||
|
@work_router.command("run")
|
||||||
|
def command_start_solving(response: Response):
|
||||||
|
print(response.status)
|
||||||
|
print(response.input_flags.flags)
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "argenta"
|
name = "argenta"
|
||||||
version = "0.3.8"
|
version = "1.1.1"
|
||||||
description = "python library for creating custom shells"
|
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"}
|
|
||||||
]
|
|
||||||
license = {text = "MIT"}
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [] # no dependencies
|
readme = "README.md"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
dependencies = [
|
||||||
|
"rich (>=14.0.0,<15.0.0)",
|
||||||
|
"art (>=6.4,<7.0)",
|
||||||
|
"pyreadline3>=3.5.4",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
exclude = [
|
exclude = [
|
||||||
@@ -21,16 +22,15 @@ 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]
|
[dependency-groups]
|
||||||
art = "^6.4"
|
dev = [
|
||||||
rich = "^13.9.4"
|
"mypy>=1.14.1",
|
||||||
numpy = "^2.2.2"
|
"pytest>=8.3.2",
|
||||||
word2number = "^1.1"
|
"ruff>=0.12.12",
|
||||||
numexpr = "^2.10.2"
|
"wemake-python-styleguide>=0.17.0",
|
||||||
requests = "^2.32.3"
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
__all__ = [
|
||||||
|
'App',
|
||||||
|
'Orchestrator',
|
||||||
|
'Router',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
from argenta.app import App
|
||||||
|
from argenta.orchestrator import Orchestrator
|
||||||
|
from argenta.router import Router
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
__all__ = [
|
||||||
|
"App",
|
||||||
|
"PredefinedMessages",
|
||||||
|
"DynamicDividingLine",
|
||||||
|
"StaticDividingLine",
|
||||||
|
"AutoCompleter"
|
||||||
|
]
|
||||||
|
|
||||||
|
from argenta.app.models import App
|
||||||
|
from argenta.app.defaults import PredefinedMessages
|
||||||
|
from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine
|
||||||
|
from argenta.app.autocompleter.entity import AutoCompleter
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
__all__ = ["AutoCompleter"]
|
||||||
|
|
||||||
|
|
||||||
|
from argenta.app.autocompleter.entity import AutoCompleter
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import os
|
||||||
|
import readline
|
||||||
|
from typing import Never
|
||||||
|
|
||||||
|
|
||||||
|
class AutoCompleter:
|
||||||
|
def __init__(
|
||||||
|
self, history_filename: str | None = None, 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: str | None = history_filename
|
||||||
|
self.autocomplete_button: str = autocomplete_button
|
||||||
|
|
||||||
|
def _complete(self, text: str, state: int) -> str | None:
|
||||||
|
"""
|
||||||
|
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 _get_history_items() if cmd.startswith(text)
|
||||||
|
)
|
||||||
|
if len(matches) > 1:
|
||||||
|
common_prefix = matches[0]
|
||||||
|
for match in matches[1:]:
|
||||||
|
i = 0
|
||||||
|
while (
|
||||||
|
i < len(common_prefix)
|
||||||
|
and i < len(match)
|
||||||
|
and common_prefix[i] == match[i]
|
||||||
|
):
|
||||||
|
i += 1
|
||||||
|
common_prefix = common_prefix[:i]
|
||||||
|
if state == 0:
|
||||||
|
readline.insert_text(common_prefix[len(text) :])
|
||||||
|
readline.redisplay()
|
||||||
|
return None
|
||||||
|
elif len(matches) == 1:
|
||||||
|
return matches[0] if state == 0 else None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
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 os.path.exists(self.history_filename):
|
||||||
|
readline.read_history_file(self.history_filename)
|
||||||
|
else:
|
||||||
|
for line in all_commands:
|
||||||
|
readline.add_history(line)
|
||||||
|
|
||||||
|
readline.set_completer(self._complete)
|
||||||
|
readline.set_completer_delims(readline.get_completer_delims().replace(" ", ""))
|
||||||
|
readline.parse_and_bind(f"{self.autocomplete_button}: complete")
|
||||||
|
|
||||||
|
def exit_setup(self, all_commands: list[str]) -> None:
|
||||||
|
"""
|
||||||
|
Private. Exit setup function
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if self.history_filename:
|
||||||
|
readline.write_history_file(self.history_filename)
|
||||||
|
with open(self.history_filename, "r") as history_file:
|
||||||
|
raw_history = history_file.read()
|
||||||
|
pretty_history: list[str] = []
|
||||||
|
for line in set(raw_history.strip().split("\n")):
|
||||||
|
if line.split()[0] in all_commands:
|
||||||
|
pretty_history.append(line)
|
||||||
|
with open(self.history_filename, "w") as history_file:
|
||||||
|
_ = history_file.write("\n".join(pretty_history))
|
||||||
|
|
||||||
|
def _get_history_items() -> list[str] | list[Never]:
|
||||||
|
"""
|
||||||
|
Private. Returns a list of all commands entered by the user
|
||||||
|
:return: all commands entered by the user as list[str] | list[Never]
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
readline.get_history_item(i)
|
||||||
|
for i in range(1, readline.get_current_history_length() + 1)
|
||||||
|
]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
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>"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
__all__ = ["StaticDividingLine", "DynamicDividingLine"]
|
||||||
|
|
||||||
|
|
||||||
|
from argenta.app.dividing_line.models import StaticDividingLine, DynamicDividingLine
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
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: str = 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: int = 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"
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
import io
|
||||||
|
import re
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from typing import Never, TypeAlias
|
||||||
|
|
||||||
|
from art import text2art # pyright: ignore[reportMissingTypeStubs, reportUnknownVariableType]
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.markup import escape
|
||||||
|
|
||||||
|
from argenta.app.autocompleter import AutoCompleter
|
||||||
|
from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine
|
||||||
|
from argenta.app.protocols import (
|
||||||
|
DescriptionMessageGenerator,
|
||||||
|
EmptyCommandHandler,
|
||||||
|
NonStandardBehaviorHandler,
|
||||||
|
Printer,
|
||||||
|
)
|
||||||
|
from argenta.app.registered_routers.entity import RegisteredRouters
|
||||||
|
from argenta.command.exceptions import (
|
||||||
|
EmptyInputCommandException,
|
||||||
|
InputCommandException,
|
||||||
|
RepeatedInputFlagsException,
|
||||||
|
UnprocessedInputFlagException,
|
||||||
|
)
|
||||||
|
from argenta.command.models import Command, InputCommand
|
||||||
|
from argenta.response import Response
|
||||||
|
from argenta.router import Router
|
||||||
|
from argenta.router.defaults import system_router
|
||||||
|
|
||||||
|
Matches: TypeAlias = list[str] | list[Never]
|
||||||
|
|
||||||
|
|
||||||
|
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: Printer) -> None:
|
||||||
|
self._prompt: str = prompt
|
||||||
|
self._print_func: Printer = print_func
|
||||||
|
self._exit_command: Command = exit_command
|
||||||
|
self._system_router_title: str | None = system_router_title
|
||||||
|
self._dividing_line: StaticDividingLine | DynamicDividingLine = dividing_line
|
||||||
|
self._ignore_command_register: bool = ignore_command_register
|
||||||
|
self._repeat_command_groups_description: bool = repeat_command_groups
|
||||||
|
self._override_system_messages: bool = override_system_messages
|
||||||
|
self._autocompleter: AutoCompleter = autocompleter
|
||||||
|
|
||||||
|
self._farewell_message: str = farewell_message
|
||||||
|
self._initial_message: str = initial_message
|
||||||
|
|
||||||
|
self._description_message_gen: DescriptionMessageGenerator = lambda command, description: f"{command} *=*=* {description}"
|
||||||
|
self._registered_routers: RegisteredRouters = RegisteredRouters()
|
||||||
|
self._messages_on_startup: list[str] = []
|
||||||
|
|
||||||
|
self._matching_lower_triggers_with_routers: dict[str, Router] = {}
|
||||||
|
self._matching_default_triggers_with_routers: dict[str, Router] = {}
|
||||||
|
|
||||||
|
self._current_matching_triggers_with_routers: dict[str, Router] = self._matching_lower_triggers_with_routers if self._ignore_command_register else self._matching_default_triggers_with_routers
|
||||||
|
|
||||||
|
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = lambda _: print_func(f"Incorrect flag syntax: {_}")
|
||||||
|
self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = lambda _: print_func(f"Repeated input flags: {_}")
|
||||||
|
self._empty_input_command_handler: EmptyCommandHandler = lambda: print_func("Empty input command")
|
||||||
|
self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = lambda _: print_func(f"Unknown command: {_.trigger}")
|
||||||
|
self._exit_command_handler: NonStandardBehaviorHandler[Response] = lambda _: print_func(self._farewell_message)
|
||||||
|
|
||||||
|
def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None:
|
||||||
|
"""
|
||||||
|
Public. Sets the output pattern of the available commands
|
||||||
|
:param _: output pattern of the available commands
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self._description_message_gen = _
|
||||||
|
|
||||||
|
def set_incorrect_input_syntax_handler(self, _: NonStandardBehaviorHandler[str], /) -> None:
|
||||||
|
"""
|
||||||
|
Public. Sets the handler for incorrect flags when entering a command
|
||||||
|
:param _: handler for incorrect flags when entering a command
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self._incorrect_input_syntax_handler = _
|
||||||
|
|
||||||
|
def set_repeated_input_flags_handler(self, _: NonStandardBehaviorHandler[str], /) -> 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, _: NonStandardBehaviorHandler[InputCommand], /) -> 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, _: EmptyCommandHandler, /) -> 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, _: NonStandardBehaviorHandler[Response], /) -> 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.title:
|
||||||
|
self._print_func(registered_router.title)
|
||||||
|
for command_handler in registered_router.command_handlers:
|
||||||
|
handled_command = command_handler.handled_command
|
||||||
|
self._print_func(
|
||||||
|
self._description_message_gen(
|
||||||
|
handled_command.trigger,
|
||||||
|
handled_command.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, 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(
|
||||||
|
length=max_length_line, is_override=self._override_system_messages
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(text.strip("\n"))
|
||||||
|
self._print_func(
|
||||||
|
self._dividing_line.get_full_dynamic_line(
|
||||||
|
length=max_length_line, is_override=self._override_system_messages
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif isinstance(self._dividing_line, StaticDividingLine): # pyright: ignore[reportUnnecessaryIsInstance]
|
||||||
|
self._print_func(
|
||||||
|
self._dividing_line.get_full_static_line(is_override=self._override_system_messages)
|
||||||
|
)
|
||||||
|
print(text.strip("\n"))
|
||||||
|
self._print_func(
|
||||||
|
self._dividing_line.get_full_static_line(is_override=self._override_system_messages)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
trigger = command.trigger
|
||||||
|
exit_trigger = self._exit_command.trigger
|
||||||
|
if self._ignore_command_register:
|
||||||
|
if (
|
||||||
|
trigger.lower() == exit_trigger.lower()
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
elif trigger.lower() in [
|
||||||
|
x.lower() for x in self._exit_command.aliases
|
||||||
|
]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if trigger == exit_trigger:
|
||||||
|
return True
|
||||||
|
elif trigger in self._exit_command.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.trigger
|
||||||
|
if self._ignore_command_register:
|
||||||
|
if input_command_trigger.lower() in list(self._current_matching_triggers_with_routers.keys()):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if input_command_trigger in list(self._current_matching_triggers_with_routers.keys()):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _error_handler(
|
||||||
|
self, error: InputCommandException, 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
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
Private. Sets up system router
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
system_router.title = self._system_router_title
|
||||||
|
|
||||||
|
@system_router.command(self._exit_command)
|
||||||
|
def _(response: Response) -> None:
|
||||||
|
self._exit_command_handler(response)
|
||||||
|
|
||||||
|
if system_router not in self._registered_routers.registered_routers:
|
||||||
|
system_router.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 = list(self._current_matching_triggers_with_routers.keys())
|
||||||
|
|
||||||
|
matches_startswith_unknown_command: Matches = sorted(
|
||||||
|
cmd for cmd in all_commands if cmd.startswith(unknown_command)
|
||||||
|
)
|
||||||
|
matches_startswith_cmd: Matches = sorted(
|
||||||
|
cmd for cmd in all_commands if unknown_command.startswith(cmd)
|
||||||
|
)
|
||||||
|
|
||||||
|
matches: Matches = matches_startswith_unknown_command or matches_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 = f"[italic dim bold]{self._prompt}"
|
||||||
|
self._initial_message = ("\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n")
|
||||||
|
self._farewell_message = (
|
||||||
|
"[bold red]\n\n" +
|
||||||
|
str(text2art(self._farewell_message, font="chanky")) + # pyright: ignore[reportUnknownArgumentType]
|
||||||
|
"\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")
|
||||||
|
|
||||||
|
def unknown_command_handler(command: InputCommand) -> None:
|
||||||
|
cmd_trg: str = command.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:
|
||||||
|
router_triggers = router_entity.triggers
|
||||||
|
router_aliases = router_entity.aliases
|
||||||
|
combined = router_triggers + router_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(list(self._current_matching_triggers_with_routers.keys()))
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine
|
||||||
|
DEFAULT_DIVIDING_LINE: StaticDividingLine = StaticDividingLine()
|
||||||
|
|
||||||
|
DEFAULT_PRINT_FUNC: Printer = Console().print
|
||||||
|
DEFAULT_AUTOCOMPLETER: AutoCompleter = AutoCompleter()
|
||||||
|
DEFAULT_EXIT_COMMAND: Command = Command("Q", description="Exit command")
|
||||||
|
|
||||||
|
|
||||||
|
class App(BaseApp):
|
||||||
|
def __init__(
|
||||||
|
self, *,
|
||||||
|
prompt: str = "What do you want to do?\n\n",
|
||||||
|
initial_message: str = "Argenta\n",
|
||||||
|
farewell_message: str = "\nSee you\n",
|
||||||
|
exit_command: Command = DEFAULT_EXIT_COMMAND,
|
||||||
|
system_router_title: str | None = "System points:",
|
||||||
|
ignore_command_register: bool = True,
|
||||||
|
dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE,
|
||||||
|
repeat_command_groups: bool = True,
|
||||||
|
override_system_messages: bool = False,
|
||||||
|
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
|
||||||
|
print_func: Printer = DEFAULT_PRINT_FUNC,
|
||||||
|
) -> 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 InputCommandException as error:
|
||||||
|
with redirect_stdout(io.StringIO()) as stderr:
|
||||||
|
self._error_handler(error, raw_command)
|
||||||
|
stderr_result: str = stderr.getvalue()
|
||||||
|
self._print_framed_text(stderr_result)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self._is_exit_command(input_command):
|
||||||
|
system_router.finds_appropriate_handler(input_command)
|
||||||
|
self._autocompleter.exit_setup(list(self._current_matching_triggers_with_routers.keys()))
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._is_unknown_command(input_command):
|
||||||
|
with redirect_stdout(io.StringIO()) as stdout:
|
||||||
|
self._unknown_command_handler(input_command)
|
||||||
|
stdout_res: str = stdout.getvalue()
|
||||||
|
self._print_framed_text(stdout_res)
|
||||||
|
continue
|
||||||
|
|
||||||
|
processing_router = self._current_matching_triggers_with_routers[input_command.trigger.lower()]
|
||||||
|
|
||||||
|
if processing_router.disable_redirect_stdout:
|
||||||
|
if isinstance(self._dividing_line, StaticDividingLine):
|
||||||
|
self._print_func(self._dividing_line.get_full_static_line(is_override=self._override_system_messages))
|
||||||
|
processing_router.finds_appropriate_handler(input_command)
|
||||||
|
self._print_func(self._dividing_line.get_full_static_line(is_override=self._override_system_messages))
|
||||||
|
else:
|
||||||
|
dividing_line_unit_part: str = self._dividing_line.get_unit_part()
|
||||||
|
self._print_func(StaticDividingLine(dividing_line_unit_part).get_full_static_line(is_override=self._override_system_messages))
|
||||||
|
processing_router.finds_appropriate_handler(input_command)
|
||||||
|
self._print_func(StaticDividingLine(dividing_line_unit_part).get_full_static_line(is_override=self._override_system_messages))
|
||||||
|
else:
|
||||||
|
with redirect_stdout(io.StringIO()) as stdout:
|
||||||
|
processing_router.finds_appropriate_handler(input_command)
|
||||||
|
stdout_result: str = stdout.getvalue()
|
||||||
|
if stdout_result:
|
||||||
|
self._print_framed_text(stdout_result)
|
||||||
|
|
||||||
|
def include_router(self, router: Router) -> None:
|
||||||
|
"""
|
||||||
|
Public. Registers the router in the application
|
||||||
|
:param router: registered router
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
router.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,22 @@
|
|||||||
|
from typing import Protocol, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar('T', contravariant=True) # noqa: WPS111
|
||||||
|
|
||||||
|
|
||||||
|
class NonStandardBehaviorHandler(Protocol[T]):
|
||||||
|
def __call__(self, __param: T) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
class EmptyCommandHandler(Protocol):
|
||||||
|
def __call__(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class Printer(Protocol):
|
||||||
|
def __call__(self, __text: str) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptionMessageGenerator(Protocol):
|
||||||
|
def __call__(self, __first_param: str, __second_param: str) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from typing import Iterator, Optional
|
||||||
|
|
||||||
|
from argenta.router import Router
|
||||||
|
|
||||||
|
|
||||||
|
class RegisteredRouters:
|
||||||
|
def __init__(self, registered_routers: Optional[list[Router]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Private. Combines registered routers
|
||||||
|
:param registered_routers: list of the registered routers
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.registered_routers: list[Router] = registered_routers if registered_routers else []
|
||||||
|
|
||||||
|
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))
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
__all__ = [
|
||||||
|
"Command",
|
||||||
|
"PossibleValues",
|
||||||
|
"PredefinedFlags",
|
||||||
|
"InputCommand",
|
||||||
|
"Flags",
|
||||||
|
"Flag"
|
||||||
|
]
|
||||||
|
|
||||||
|
from argenta.command.models import Command, InputCommand
|
||||||
|
from argenta.command.flag import defaults as PredefinedFlags
|
||||||
|
from argenta.command.flag import (Flag, Flags, PossibleValues)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from argenta.command.flag.models import Flag, InputFlag
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
|
||||||
|
class InputCommandException(ABC, Exception):
|
||||||
|
"""
|
||||||
|
Private. Base exception class for all exceptions raised when parse input command
|
||||||
|
"""
|
||||||
|
@override
|
||||||
|
@abstractmethod
|
||||||
|
def __str__(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class UnprocessedInputFlagException(InputCommandException):
|
||||||
|
"""
|
||||||
|
Private. Raised when an unprocessed input flag is detected
|
||||||
|
"""
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "Unprocessed Input Flags"
|
||||||
|
|
||||||
|
|
||||||
|
class RepeatedInputFlagsException(InputCommandException):
|
||||||
|
"""
|
||||||
|
Private. Raised when repeated input flags are detected
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, flag: Flag | InputFlag):
|
||||||
|
self.flag: Flag | InputFlag = flag
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
string_entity: str = self.flag.string_entity
|
||||||
|
return (
|
||||||
|
"Repeated Input Flags\n"
|
||||||
|
f"Duplicate flag was detected in the input: '{string_entity}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyInputCommandException(InputCommandException):
|
||||||
|
"""
|
||||||
|
Private. Raised when an empty input command is detected
|
||||||
|
"""
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "Input Command is empty"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
__all__ = [
|
||||||
|
"Flag",
|
||||||
|
"InputFlag",
|
||||||
|
"Flags",
|
||||||
|
"PossibleValues",
|
||||||
|
"ValidationStatus"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
from argenta.command.flag.models import Flag, InputFlag, PossibleValues, ValidationStatus
|
||||||
|
from argenta.command.flag.flags.models import Flags
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from typing import Literal
|
||||||
|
from argenta.command.flag.models import Flag, PossibleValues
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_PREFIX: Literal["-", "--", "---"] = "-"
|
||||||
|
|
||||||
|
HELP = Flag(name="help", possible_values=PossibleValues.NEITHER)
|
||||||
|
SHORT_HELP = Flag(name="H", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
|
||||||
|
|
||||||
|
INFO = Flag(name="info", possible_values=PossibleValues.NEITHER) # noqa: WPS110
|
||||||
|
SHORT_INFO = Flag(name="I", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
|
||||||
|
|
||||||
|
ALL = Flag(name="all", possible_values=PossibleValues.NEITHER)
|
||||||
|
SHORT_ALL = Flag(name="A", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
|
||||||
|
|
||||||
|
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=DEFAULT_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}$"))
|
||||||
|
SHORT_PORT = Flag(name="P", prefix=DEFAULT_PREFIX, possible_values=re.compile(r"^\d{1,5}$"))
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
__all__ = [
|
||||||
|
"Flags",
|
||||||
|
"InputFlags"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
from argenta.command.flag.flags.models import (
|
||||||
|
Flags,
|
||||||
|
InputFlags
|
||||||
|
)
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
from argenta.command.flag.models import InputFlag, Flag
|
||||||
|
from typing import Generic, TypeVar, override
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
|
||||||
|
FlagType = TypeVar("FlagType")
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFlags(Generic[FlagType]):
|
||||||
|
def __init__(self, flags: list[FlagType] | None = None) -> None:
|
||||||
|
"""
|
||||||
|
Public. A model that combines the registered flags
|
||||||
|
:param flags: the flags that will be registered
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.flags: list[FlagType] = flags if flags else []
|
||||||
|
|
||||||
|
def add_flag(self, flag: FlagType) -> None:
|
||||||
|
"""
|
||||||
|
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]) -> None:
|
||||||
|
"""
|
||||||
|
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 __iter__(self) -> Iterator[FlagType]:
|
||||||
|
return iter(self.flags)
|
||||||
|
|
||||||
|
def __next__(self) -> FlagType:
|
||||||
|
return next(iter(self))
|
||||||
|
|
||||||
|
def __getitem__(self, flag_index: int) -> FlagType:
|
||||||
|
return self.flags[flag_index]
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return bool(self.flags)
|
||||||
|
|
||||||
|
|
||||||
|
class Flags(BaseFlags[Flag]):
|
||||||
|
def get_flag_by_name(self, name: str) -> Flag | 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
|
||||||
|
"""
|
||||||
|
return next((flag for flag in self.flags if flag.name == name), None)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, Flags):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
if len(self.flags) != len(other.flags):
|
||||||
|
return False
|
||||||
|
|
||||||
|
flag_pairs: zip[tuple[Flag, Flag]] = zip(self.flags, other.flags)
|
||||||
|
return all(s_flag == o_flag for s_flag, o_flag in flag_pairs)
|
||||||
|
|
||||||
|
def __contains__(self, flag_to_check: object) -> bool:
|
||||||
|
if isinstance(flag_to_check, Flag):
|
||||||
|
for flag in self.flags:
|
||||||
|
if flag == flag_to_check:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
|
|
||||||
|
class InputFlags(BaseFlags[InputFlag]):
|
||||||
|
def get_flag_by_name(self, name: str) -> InputFlag | 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
|
||||||
|
"""
|
||||||
|
return next((flag for flag in self.flags if flag.name == name), None)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, InputFlags):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
if len(self.flags) != len(other.flags):
|
||||||
|
return False
|
||||||
|
|
||||||
|
paired_flags: zip[tuple[InputFlag, InputFlag]] = zip(self.flags, other.flags)
|
||||||
|
|
||||||
|
return all(my_flag == other_flag for my_flag, other_flag in paired_flags)
|
||||||
|
|
||||||
|
def __contains__(self, ingressable_item: object) -> bool:
|
||||||
|
if isinstance(ingressable_item, InputFlag):
|
||||||
|
for flag in self.flags:
|
||||||
|
if flag == ingressable_item:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from re import Pattern
|
||||||
|
from typing import Literal, override
|
||||||
|
|
||||||
|
|
||||||
|
class PossibleValues(Enum):
|
||||||
|
NEITHER = 'NEITHER'
|
||||||
|
ALL = 'ALL'
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationStatus(Enum):
|
||||||
|
VALID = 'VALID'
|
||||||
|
INVALID = 'INVALID'
|
||||||
|
UNDEFINED = 'UNDEFINED'
|
||||||
|
|
||||||
|
|
||||||
|
class Flag:
|
||||||
|
def __init__(
|
||||||
|
self, name: str, *,
|
||||||
|
prefix: Literal["-", "--", "---"] = "--",
|
||||||
|
possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
|
||||||
|
) -> 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
|
||||||
|
"""
|
||||||
|
self.name: str = name
|
||||||
|
self.prefix: Literal["-", "--", "---"] = prefix
|
||||||
|
self.possible_values: list[str] | Pattern[str] | PossibleValues = possible_values
|
||||||
|
|
||||||
|
def validate_input_flag_value(self, input_flag_value: str | None) -> bool:
|
||||||
|
"""
|
||||||
|
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 == PossibleValues.NEITHER:
|
||||||
|
return input_flag_value is None
|
||||||
|
|
||||||
|
if isinstance(self.possible_values, Pattern):
|
||||||
|
return isinstance(input_flag_value, str) and bool(self.possible_values.match(input_flag_value))
|
||||||
|
|
||||||
|
if isinstance(self.possible_values, list):
|
||||||
|
return input_flag_value in self.possible_values
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def 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
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.string_entity
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'Flag<name={self.name}, prefix={self.prefix}>'
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if isinstance(other, Flag):
|
||||||
|
return self.string_entity == other.string_entity
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class InputFlag:
|
||||||
|
def __init__(
|
||||||
|
self, name: str, *,
|
||||||
|
prefix: Literal['-', '--', '---'] = '--',
|
||||||
|
input_value: str | None,
|
||||||
|
status: ValidationStatus | 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
|
||||||
|
"""
|
||||||
|
self.name: str = name
|
||||||
|
self.prefix: Literal['-', '--', '---'] = prefix
|
||||||
|
self.input_value: str | None = input_value
|
||||||
|
self.status: ValidationStatus | None = status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def 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
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f'{self.string_entity} {self.input_value}'
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'InputFlag<name={self.name}, prefix={self.prefix}, value={self.input_value}, status={self.status}>'
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if isinstance(other, InputFlag):
|
||||||
|
return (
|
||||||
|
self.name == other.name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
from argenta.command.flag.models import Flag, InputFlag, ValidationStatus
|
||||||
|
from argenta.command.flag.flags.models import InputFlags, Flags
|
||||||
|
from argenta.command.exceptions import (
|
||||||
|
UnprocessedInputFlagException,
|
||||||
|
RepeatedInputFlagsException,
|
||||||
|
EmptyInputCommandException,
|
||||||
|
)
|
||||||
|
from typing import Never, Self, cast, Literal
|
||||||
|
|
||||||
|
|
||||||
|
ParseFlagsResult = tuple[InputFlags, str | None, str | None]
|
||||||
|
ParseResult = tuple[str, InputFlags]
|
||||||
|
|
||||||
|
MIN_FLAG_PREFIX: str = "-"
|
||||||
|
DEFAULT_WITHOUT_FLAGS: Flags = Flags()
|
||||||
|
|
||||||
|
DEFAULT_WITHOUT_INPUT_FLAGS: InputFlags = InputFlags()
|
||||||
|
|
||||||
|
|
||||||
|
class Command:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
trigger: str, *,
|
||||||
|
description: str | None = None,
|
||||||
|
flags: Flag | Flags = DEFAULT_WITHOUT_FLAGS,
|
||||||
|
aliases: list[str] | None = 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
|
||||||
|
"""
|
||||||
|
self.registered_flags: Flags = flags if isinstance(flags, Flags) else Flags([flags])
|
||||||
|
self.trigger: str = trigger
|
||||||
|
self.description: str = description if description else "Command without description"
|
||||||
|
self.aliases: list[str] = aliases if aliases else []
|
||||||
|
|
||||||
|
def validate_input_flag(
|
||||||
|
self, flag: InputFlag
|
||||||
|
) -> ValidationStatus:
|
||||||
|
"""
|
||||||
|
Private. Validates the input flag
|
||||||
|
:param flag: input flag for validation
|
||||||
|
:return: is input flag valid as bool
|
||||||
|
"""
|
||||||
|
registered_flags: Flags = self.registered_flags
|
||||||
|
for registered_flag in registered_flags:
|
||||||
|
if registered_flag.string_entity == flag.string_entity:
|
||||||
|
is_valid = registered_flag.validate_input_flag_value(flag.input_value)
|
||||||
|
if is_valid:
|
||||||
|
return ValidationStatus.VALID
|
||||||
|
else:
|
||||||
|
return ValidationStatus.INVALID
|
||||||
|
return ValidationStatus.UNDEFINED
|
||||||
|
|
||||||
|
|
||||||
|
class InputCommand:
|
||||||
|
def __init__(self, trigger: str, *,
|
||||||
|
input_flags: InputFlag | InputFlags = DEFAULT_WITHOUT_INPUT_FLAGS):
|
||||||
|
"""
|
||||||
|
Private. The model of the input command, after parsing
|
||||||
|
:param trigger:the trigger of the command
|
||||||
|
:param input_flags: the input flags
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.trigger: str = trigger
|
||||||
|
self.input_flags: InputFlags = input_flags if isinstance(input_flags, InputFlags) else InputFlags([input_flags])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, raw_command: str) -> Self:
|
||||||
|
"""
|
||||||
|
Private. Parse the raw input command
|
||||||
|
:param raw_command: raw input command
|
||||||
|
:return: model of the input command, after parsing as InputCommand
|
||||||
|
"""
|
||||||
|
trigger, input_flags = CommandParser(raw_command).parse_raw_command()
|
||||||
|
|
||||||
|
return cls(trigger=trigger, input_flags=input_flags)
|
||||||
|
|
||||||
|
|
||||||
|
class CommandParser:
|
||||||
|
def __init__(self, raw_command: str) -> None:
|
||||||
|
self.raw_command: str = raw_command
|
||||||
|
self._parsed_input_flags: InputFlags = InputFlags()
|
||||||
|
|
||||||
|
def parse_raw_command(self) -> ParseResult:
|
||||||
|
if not self.raw_command:
|
||||||
|
raise EmptyInputCommandException()
|
||||||
|
|
||||||
|
input_flags, crnt_flag_name, crnt_flag_val = self._parse_flags(self.raw_command.split()[1:])
|
||||||
|
|
||||||
|
if any([crnt_flag_name, crnt_flag_val]):
|
||||||
|
raise UnprocessedInputFlagException()
|
||||||
|
else:
|
||||||
|
return (self.raw_command.split()[0], input_flags)
|
||||||
|
|
||||||
|
def _parse_flags(self, _tokens: list[str] | list[Never]) -> ParseFlagsResult:
|
||||||
|
crnt_flg_name, crnt_flg_val = None, None
|
||||||
|
for index, token in enumerate(_tokens):
|
||||||
|
crnt_flg_name, crnt_flg_val = _parse_single_token(token, crnt_flg_name, crnt_flg_val)
|
||||||
|
|
||||||
|
if not crnt_flg_name or self._is_next_token_value(index, _tokens):
|
||||||
|
continue
|
||||||
|
|
||||||
|
input_flag = InputFlag(
|
||||||
|
name=crnt_flg_name[crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1:],
|
||||||
|
prefix=cast(
|
||||||
|
Literal["-", "--", "---"],
|
||||||
|
crnt_flg_name[:crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1],
|
||||||
|
),
|
||||||
|
input_value=crnt_flg_val,
|
||||||
|
status=None
|
||||||
|
)
|
||||||
|
|
||||||
|
if input_flag in self._parsed_input_flags:
|
||||||
|
raise RepeatedInputFlagsException(input_flag)
|
||||||
|
|
||||||
|
self._parsed_input_flags.add_flag(input_flag)
|
||||||
|
crnt_flg_name, crnt_flg_val = None, None
|
||||||
|
|
||||||
|
return (self._parsed_input_flags, crnt_flg_name, crnt_flg_val)
|
||||||
|
|
||||||
|
def _is_next_token_value(self, current_index: int,
|
||||||
|
_tokens: list[str] | list[Never]) -> bool:
|
||||||
|
next_index = current_index + 1
|
||||||
|
if next_index >= len(_tokens):
|
||||||
|
return False
|
||||||
|
|
||||||
|
next_token = _tokens[next_index]
|
||||||
|
return not next_token.startswith(MIN_FLAG_PREFIX)
|
||||||
|
|
||||||
|
def _parse_single_token(
|
||||||
|
token: str,
|
||||||
|
crnt_flag_name: str | None,
|
||||||
|
crnt_flag_val: str | None
|
||||||
|
) -> tuple[str | None, str | None]:
|
||||||
|
if not token.startswith(MIN_FLAG_PREFIX):
|
||||||
|
if not crnt_flag_name or crnt_flag_val:
|
||||||
|
raise UnprocessedInputFlagException
|
||||||
|
return crnt_flag_name, token
|
||||||
|
|
||||||
|
prefix = token[:token.rfind(MIN_FLAG_PREFIX)]
|
||||||
|
if len(token) < 2 or len(prefix) > 2:
|
||||||
|
raise UnprocessedInputFlagException
|
||||||
|
|
||||||
|
new_flag_name = token
|
||||||
|
new_flag_value = None
|
||||||
|
|
||||||
|
return new_flag_name, new_flag_value
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
__all__ = ["get_time_of_pre_cycle_setup"]
|
||||||
|
|
||||||
|
|
||||||
|
from argenta.metrics.main import get_time_of_pre_cycle_setup
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import io
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from argenta 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() # pyright: ignore[reportPrivateUsage]
|
||||||
|
end = time()
|
||||||
|
return end - start
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
__all__ = [
|
||||||
|
"Orchestrator",
|
||||||
|
"ArgParser"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
from argenta.orchestrator.entity import Orchestrator
|
||||||
|
from argenta.orchestrator.argparser.entity import ArgParser
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
__all__ = [
|
||||||
|
"ArgParser",
|
||||||
|
"PositionalArgument",
|
||||||
|
"OptionalArgument",
|
||||||
|
"BooleanArgument"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
from argenta.orchestrator.argparser.entity import ArgParser
|
||||||
|
from argenta.orchestrator.argparser.arguments import (BooleanArgument,
|
||||||
|
PositionalArgument,
|
||||||
|
OptionalArgument)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
__all__ = ["BooleanArgument", "PositionalArgument", "OptionalArgument"]
|
||||||
|
|
||||||
|
|
||||||
|
from argenta.orchestrator.argparser.arguments.models import (
|
||||||
|
BooleanArgument,
|
||||||
|
PositionalArgument,
|
||||||
|
OptionalArgument,
|
||||||
|
)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Literal, override
|
||||||
|
|
||||||
|
|
||||||
|
class BaseArgument(ABC):
|
||||||
|
"""
|
||||||
|
Private. Base class for all arguments
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def string_entity(self) -> str:
|
||||||
|
"""
|
||||||
|
Public. Returns the string representation of the argument
|
||||||
|
:return: the string representation as a str
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
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: str = name
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def string_entity(self) -> str:
|
||||||
|
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: str = name
|
||||||
|
self.prefix: Literal["-", "--", "---"] = prefix
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def string_entity(self) -> str:
|
||||||
|
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: str = name
|
||||||
|
self.prefix: Literal["-", "--", "---"] = prefix
|
||||||
|
|
||||||
|
@property
|
||||||
|
@override
|
||||||
|
def string_entity(self) -> str:
|
||||||
|
return self.prefix + self.name
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from argparse import ArgumentParser, Namespace
|
||||||
|
|
||||||
|
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: str = name
|
||||||
|
self._description: str = description
|
||||||
|
self._epilog: str = epilog
|
||||||
|
|
||||||
|
self._entity: ArgumentParser = ArgumentParser(prog=name, description=description, epilog=epilog)
|
||||||
|
self._processed_args: list[PositionalArgument | OptionalArgument | BooleanArgument] = processed_args
|
||||||
|
|
||||||
|
for arg in processed_args:
|
||||||
|
if isinstance(arg, PositionalArgument) or isinstance(arg, OptionalArgument):
|
||||||
|
_ = self._entity.add_argument(arg.string_entity)
|
||||||
|
else:
|
||||||
|
_ = self._entity.add_argument(arg.string_entity, action="store_true")
|
||||||
|
|
||||||
|
def parse_args(self) -> Namespace:
|
||||||
|
return self._entity.parse_args()
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
|
from argenta.app import App
|
||||||
|
from argenta.orchestrator.argparser import ArgParser
|
||||||
|
|
||||||
|
|
||||||
|
class Orchestrator:
|
||||||
|
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 | None = arg_parser
|
||||||
|
|
||||||
|
def start_polling(self, 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.parse_args()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
__all__ = ["Response", "ResponseStatus"]
|
||||||
|
|
||||||
|
|
||||||
|
from argenta.response.entity import Response
|
||||||
|
from argenta.response.status import ResponseStatus
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from typing import Literal
|
||||||
|
from argenta.command.flag.flags.models import InputFlags
|
||||||
|
from argenta.response.status import ResponseStatus
|
||||||
|
|
||||||
|
|
||||||
|
EMPTY_INPUT_FLAGS: InputFlags = InputFlags()
|
||||||
|
|
||||||
|
|
||||||
|
class Response:
|
||||||
|
__slots__: tuple[Literal['status', 'input_flags'], ...] = ("status", "input_flags")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
status: ResponseStatus,
|
||||||
|
input_flags: InputFlags = EMPTY_INPUT_FLAGS,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Public. The entity of the user input sent to the handler
|
||||||
|
:param status: the status of the response
|
||||||
|
:param input_flags: all input flags
|
||||||
|
"""
|
||||||
|
self.status: ResponseStatus = status
|
||||||
|
self.input_flags: InputFlags = input_flags
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseStatus(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"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_flags(cls, *, has_invalid_value_flags: bool, has_undefined_flags: bool) -> 'ResponseStatus':
|
||||||
|
key = (has_invalid_value_flags, has_undefined_flags)
|
||||||
|
status_map: dict[tuple[bool, bool], ResponseStatus] = {
|
||||||
|
(True, True): cls.UNDEFINED_AND_INVALID_FLAGS,
|
||||||
|
(True, False): cls.INVALID_VALUE_FLAGS,
|
||||||
|
(False, True): cls.UNDEFINED_FLAGS,
|
||||||
|
(False, False): cls.ALL_FLAGS_VALID,
|
||||||
|
}
|
||||||
|
return status_map[key]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from argenta.router.entity import Router
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Router"]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from collections.abc import Iterator
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from argenta.command import Command
|
||||||
|
from argenta.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
class CommandHandler:
|
||||||
|
def __init__(self, handler_as_func: 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_as_func: Callable[[Response], None] = handler_as_func
|
||||||
|
self.handled_command: 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_as_func(response)
|
||||||
|
|
||||||
|
|
||||||
|
class CommandHandlers:
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
self.command_handlers: list[CommandHandler] = command_handlers if command_handlers else []
|
||||||
|
|
||||||
|
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))
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from argenta.router import Router
|
||||||
|
|
||||||
|
|
||||||
|
system_router = Router(title="System points:")
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
from typing import Callable, TypeAlias
|
||||||
|
from inspect import getfullargspec, get_annotations, getsourcefile, getsourcelines
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from argenta.command import Command, InputCommand
|
||||||
|
from argenta.command.flag import ValidationStatus
|
||||||
|
from argenta.response import Response, ResponseStatus
|
||||||
|
from argenta.router.command_handler.entity import CommandHandlers, CommandHandler
|
||||||
|
from argenta.command.flag.flags import (
|
||||||
|
Flags,
|
||||||
|
InputFlags
|
||||||
|
)
|
||||||
|
from argenta.router.exceptions import (
|
||||||
|
RepeatedFlagNameException,
|
||||||
|
TooManyTransferredArgsException,
|
||||||
|
RequiredArgumentNotPassedException,
|
||||||
|
TriggerContainSpacesException,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
HandlerFunc: TypeAlias = Callable[[Response], None]
|
||||||
|
|
||||||
|
|
||||||
|
class Router:
|
||||||
|
def __init__(
|
||||||
|
self, *, title: str | None = "Default 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: str | None = title
|
||||||
|
self.disable_redirect_stdout: bool = disable_redirect_stdout
|
||||||
|
|
||||||
|
self.command_handlers: CommandHandlers = CommandHandlers()
|
||||||
|
self.command_register_ignore: bool = False
|
||||||
|
|
||||||
|
def command(self, command: Command | str) -> Callable[[HandlerFunc], HandlerFunc]:
|
||||||
|
"""
|
||||||
|
Public. Registers handler
|
||||||
|
:param command: Registered command
|
||||||
|
:return: decorated handler as Callable
|
||||||
|
"""
|
||||||
|
if isinstance(command, str):
|
||||||
|
redefined_command = Command(command)
|
||||||
|
else:
|
||||||
|
redefined_command = command
|
||||||
|
|
||||||
|
_validate_command(redefined_command)
|
||||||
|
|
||||||
|
def decorator(func: HandlerFunc) -> HandlerFunc:
|
||||||
|
_validate_func_args(func)
|
||||||
|
self.command_handlers.add_handler(CommandHandler(func, redefined_command))
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return 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.trigger
|
||||||
|
input_command_flags: InputFlags = input_command.input_flags
|
||||||
|
|
||||||
|
for command_handler in self.command_handlers:
|
||||||
|
handle_command = command_handler.handled_command
|
||||||
|
if input_command_name.lower() == handle_command.trigger.lower():
|
||||||
|
self.process_input_command(input_command_flags, command_handler)
|
||||||
|
if input_command_name.lower() in handle_command.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.handled_command
|
||||||
|
if handle_command.registered_flags.flags:
|
||||||
|
if input_command_flags.flags:
|
||||||
|
response: Response = _structuring_input_flags(handle_command, input_command_flags)
|
||||||
|
command_handler.handling(response)
|
||||||
|
else:
|
||||||
|
response = Response(ResponseStatus.ALL_FLAGS_VALID)
|
||||||
|
command_handler.handling(response)
|
||||||
|
else:
|
||||||
|
if input_command_flags.flags:
|
||||||
|
undefined_flags = InputFlags()
|
||||||
|
for input_flag in input_command_flags:
|
||||||
|
input_flag.status = ValidationStatus.UNDEFINED
|
||||||
|
undefined_flags.add_flag(input_flag)
|
||||||
|
response = Response(ResponseStatus.UNDEFINED_FLAGS, input_flags=undefined_flags)
|
||||||
|
command_handler.handling(response)
|
||||||
|
else:
|
||||||
|
response = Response(ResponseStatus.ALL_FLAGS_VALID)
|
||||||
|
command_handler.handling(response)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def 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.handled_command.trigger)
|
||||||
|
return all_triggers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def 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.handled_command.aliases:
|
||||||
|
all_aliases.extend(command_handler.handled_command.aliases)
|
||||||
|
return all_aliases
|
||||||
|
|
||||||
|
|
||||||
|
class CommandDecorator:
|
||||||
|
def __init__(self, router_instance: Router, command: Command):
|
||||||
|
self.router: Router = router_instance
|
||||||
|
self.command: Command = command
|
||||||
|
|
||||||
|
def __call__(self, handler_func: Callable[[Response], None]) -> Callable[[Response], None]:
|
||||||
|
_validate_func_args(handler_func)
|
||||||
|
self.router.command_handlers.add_handler(CommandHandler(handler_func, self.command))
|
||||||
|
return handler_func
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
invalid_value_flags, undefined_flags = False, False
|
||||||
|
|
||||||
|
for flag in input_flags:
|
||||||
|
flag_status: ValidationStatus = (handled_command.validate_input_flag(flag))
|
||||||
|
flag.status = flag_status
|
||||||
|
if flag_status == ValidationStatus.INVALID:
|
||||||
|
invalid_value_flags = True
|
||||||
|
elif flag_status == ValidationStatus.UNDEFINED:
|
||||||
|
undefined_flags = True
|
||||||
|
|
||||||
|
status = ResponseStatus.from_flags(has_invalid_value_flags=invalid_value_flags,
|
||||||
|
has_undefined_flags=undefined_flags)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status=status,
|
||||||
|
input_flags=input_flags
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_func_args(func: Callable[[Response], None]) -> 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, None] = get_annotations(func)
|
||||||
|
|
||||||
|
arg_annotation = func_annotations.get(transferred_arg)
|
||||||
|
|
||||||
|
if arg_annotation is not None:
|
||||||
|
if arg_annotation is not Response:
|
||||||
|
source_line: int = getsourcelines(func)[1]
|
||||||
|
Console().print(
|
||||||
|
f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint ' +
|
||||||
|
f"of argument([green]{transferred_arg}[/green]) passed to the handler must be [/i][bold blue]{Response}[/bold blue]," +
|
||||||
|
f" [i]but[/i] [bold blue]{arg_annotation}[/bold blue] [i]is specified[/i]",
|
||||||
|
highlight=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
command_name: str = command.trigger
|
||||||
|
if command_name.find(" ") != -1:
|
||||||
|
raise TriggerContainSpacesException()
|
||||||
|
flags: Flags = command.registered_flags
|
||||||
|
flags_name: list[str] = [flag.string_entity.lower() for flag in flags]
|
||||||
|
if len(set(flags_name)) < len(flags_name):
|
||||||
|
raise RepeatedFlagNameException()
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from typing import override
|
||||||
|
|
||||||
|
|
||||||
|
class RepeatedFlagNameException(Exception):
|
||||||
|
"""
|
||||||
|
Private. Raised when a repeated flag name is registered
|
||||||
|
"""
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "Repeated registered flag names in register command"
|
||||||
|
|
||||||
|
|
||||||
|
class TooManyTransferredArgsException(Exception):
|
||||||
|
"""
|
||||||
|
Private. Raised when too many arguments are passed
|
||||||
|
"""
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "Too many transferred arguments"
|
||||||
|
|
||||||
|
|
||||||
|
class RequiredArgumentNotPassedException(Exception):
|
||||||
|
"""
|
||||||
|
Private. Raised when a required argument is not passed
|
||||||
|
"""
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "Required argument not passed"
|
||||||
|
|
||||||
|
|
||||||
|
class TriggerContainSpacesException(Exception):
|
||||||
|
"""
|
||||||
|
Private. Raised when there is a space in the trigger being registered
|
||||||
|
"""
|
||||||
|
@override
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return "Command trigger cannot contain spaces"
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import _io
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from unittest import TestCase
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
|
||||||
|
from argenta.app import App
|
||||||
|
from argenta.command import Command, PredefinedFlags
|
||||||
|
from argenta.command.flag.models import ValidationStatus
|
||||||
|
from argenta.router import Router
|
||||||
|
from argenta.command.flag.flags.models import Flags
|
||||||
|
from argenta.orchestrator import Orchestrator
|
||||||
|
from argenta.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestSystemHandlerNormalWork(TestCase):
|
||||||
|
@patch("builtins.input", side_effect=["help", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_incorrect_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print('test command')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn("\nUnknown command: help\n", output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["TeSt", "Q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_incorrect_command2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print('test command')
|
||||||
|
|
||||||
|
app = App(ignore_command_register=False,
|
||||||
|
override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\nUnknown command: TeSt\n', output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test --help", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_unregistered_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
undefined_flag = response.input_flags.get_flag_by_name('help')
|
||||||
|
if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED:
|
||||||
|
print(f'test command with undefined flag: {undefined_flag.string_entity}')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\ntest command with undefined flag: --help\n', output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test --port 22", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_unregistered_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
undefined_flag = response.input_flags.get_flag_by_name("port")
|
||||||
|
if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED:
|
||||||
|
print(f'test command with undefined flag with value: {undefined_flag.string_entity} {undefined_flag.input_value}')
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
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("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):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
flags = Flags([PredefinedFlags.HOST])
|
||||||
|
|
||||||
|
@router.command(Command('test', flags=flags))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
undefined_flag = response.input_flags.get_flag_by_name("port")
|
||||||
|
if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED:
|
||||||
|
print(f'connecting to host with flag: {undefined_flag.string_entity} {undefined_flag.input_value}')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\nconnecting to host with flag: --port 132\n', output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test", "some", "q"])
|
||||||
|
@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):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print(f'test command')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertRegex(output, re.compile(r'\ntest command\n(.|\n)*\nUnknown command: some'))
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test", "some", "more", "q"])
|
||||||
|
@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):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print(f'test command')
|
||||||
|
|
||||||
|
@router.command(Command('more'))
|
||||||
|
def test1(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print(f'more command')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertRegex(output, re.compile(r'\ntest command\n(.|\n)*\nUnknown command: some\n(.|\n)*\nmore command'))
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test 535 --port", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_incorrect_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print(f'test command')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"'))
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn("\nIncorrect flag syntax: \"test 535 --port\"\n", output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_empty_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print(f'test command')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
app.set_empty_command_handler(lambda: print('Empty input command'))
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn("\nEmpty input command\n", output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test --port 22 --port 33", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_repeated_flags(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test', flags=PredefinedFlags.PORT))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print('test command')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"'))
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('Repeated input flags: "test --port 22 --port 33"', output)
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test --help", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_unregistered_flag3(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
undefined_flag = response.input_flags.get_flag_by_name('help')
|
||||||
|
if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED:
|
||||||
|
print(f'test command with undefined flag: {undefined_flag.string_entity}')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\ntest command with undefined flag: --help\n', output)
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import _io
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from unittest import TestCase
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
|
||||||
|
from argenta.app import App
|
||||||
|
from argenta.command import Command, PredefinedFlags
|
||||||
|
from argenta.command.flag.models import PossibleValues, ValidationStatus
|
||||||
|
from argenta.response import Response
|
||||||
|
from argenta.router import Router
|
||||||
|
from argenta.orchestrator import Orchestrator
|
||||||
|
from argenta.command.flag import Flag
|
||||||
|
from argenta.command.flag.flags import Flags
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestSystemHandlerNormalWork(TestCase):
|
||||||
|
@patch("builtins.input", side_effect=["test", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print('test command')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\ntest command\n', output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["TeSt", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print('test command')
|
||||||
|
|
||||||
|
app = App(ignore_command_register=True,
|
||||||
|
override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\ntest command\n', output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test --help", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_custom_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
flag = Flag('help', prefix='--', possible_values=PossibleValues.NEITHER)
|
||||||
|
|
||||||
|
@router.command(Command('test', flags=flag))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
valid_flag = response.input_flags.get_flag_by_name('help')
|
||||||
|
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||||
|
print(f'\nhelp for {valid_flag.name} flag\n')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\nhelp for help flag\n', output)
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test --port 22", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_custom_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
flag = Flag('port', prefix='--', possible_values=re.compile(r'^\d{1,5}$'))
|
||||||
|
|
||||||
|
@router.command(Command('test', flags=flag))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
valid_flag = response.input_flags.get_flag_by_name('port')
|
||||||
|
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||||
|
print(f'flag value for {valid_flag.name} flag : {valid_flag.input_value}')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\nflag value for port flag : 22\n', output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test -H", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_default_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
flag = PredefinedFlags.SHORT_HELP
|
||||||
|
|
||||||
|
@router.command(Command('test', flags=flag))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
valid_flag = response.input_flags.get_flag_by_name('H')
|
||||||
|
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||||
|
print(f'help for {valid_flag.name} flag')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\nhelp for H flag\n', output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test --info", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_default_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
flag = PredefinedFlags.INFO
|
||||||
|
|
||||||
|
@router.command(Command('test', flags=flag))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
valid_flag = response.input_flags.get_flag_by_name('info')
|
||||||
|
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||||
|
print('info about test command')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\ninfo about test command\n', output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test --host 192.168.0.1", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_default_flag3(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
flag = PredefinedFlags.HOST
|
||||||
|
|
||||||
|
@router.command(Command('test', flags=flag))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
valid_flag = response.input_flags.get_flag_by_name('host')
|
||||||
|
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||||
|
print(f'connecting to host {valid_flag.input_value}')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\nconnecting to host 192.168.0.1\n', output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test --host 192.168.32.1 --port 132", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_correct_command_with_two_flags(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
flags = Flags([PredefinedFlags.HOST, PredefinedFlags.PORT])
|
||||||
|
|
||||||
|
@router.command(Command('test', flags=flags))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
host_flag = response.input_flags.get_flag_by_name('host')
|
||||||
|
port_flag = response.input_flags.get_flag_by_name('port')
|
||||||
|
if (host_flag and host_flag.status == ValidationStatus.VALID) and (port_flag and port_flag.status == ValidationStatus.VALID):
|
||||||
|
print(f'connecting to host {host_flag.input_value} and port {port_flag.input_value}')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertIn('\nconnecting to host 192.168.32.1 and port 132\n', output)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test", "some", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_two_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print(f'test command')
|
||||||
|
|
||||||
|
@router.command(Command('some'))
|
||||||
|
def test2(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print(f'some command')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertRegex(output, re.compile(r'\ntest command\n(.|\n)*\nsome command\n'))
|
||||||
|
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["test", "some", "more", "q"])
|
||||||
|
@patch("sys.stdout", new_callable=io.StringIO)
|
||||||
|
def test_input_three_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock):
|
||||||
|
router = Router()
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
@router.command(Command('test'))
|
||||||
|
def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print(f'test command')
|
||||||
|
|
||||||
|
@router.command(Command('some'))
|
||||||
|
def test1(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print(f'some command')
|
||||||
|
|
||||||
|
@router.command(Command('more'))
|
||||||
|
def test2(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
print(f'more command')
|
||||||
|
|
||||||
|
app = App(override_system_messages=True,
|
||||||
|
print_func=print)
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
output = mock_stdout.getvalue()
|
||||||
|
|
||||||
|
self.assertRegex(output, re.compile(r'\ntest command\n(.|\n)*\nsome command\n(.|\n)*\nmore command'))
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from argenta.app import App
|
|
||||||
from argenta.app.exceptions import (InvalidDescriptionMessagePatternException,
|
|
||||||
NoRegisteredRoutersException)
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class TestApp(unittest.TestCase):
|
|
||||||
def test_set_invalid_description_message_pattern(self):
|
|
||||||
with self.assertRaises(InvalidDescriptionMessagePatternException):
|
|
||||||
App().set_description_message_pattern('Invalid description pattern')
|
|
||||||
|
|
||||||
def test_set_invalid_description_message_pattern2(self):
|
|
||||||
with self.assertRaises(InvalidDescriptionMessagePatternException):
|
|
||||||
App().set_description_message_pattern('Invalid {desription} description {comand} pattern')
|
|
||||||
|
|
||||||
def test_no_registered_router(self):
|
|
||||||
with self.assertRaises(NoRegisteredRoutersException):
|
|
||||||
App()._validate_number_of_routers()
|
|
||||||
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
from argenta.command import Command
|
|
||||||
from argenta.command.exceptions import (UnprocessedInputFlagException,
|
|
||||||
RepeatedInputFlagsException,
|
|
||||||
EmptyInputCommandException)
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class TestCommand(unittest.TestCase):
|
|
||||||
def test_parse_correct_raw_command(self):
|
|
||||||
self.assertEqual(Command.parse_input_command('ssh --host 192.168.0.3').get_trigger(), 'ssh')
|
|
||||||
|
|
||||||
def test_parse_raw_command_with_flag_name_without_value(self):
|
|
||||||
with self.assertRaises(UnprocessedInputFlagException):
|
|
||||||
Command.parse_input_command('ssh --host')
|
|
||||||
|
|
||||||
def test_parse_raw_command_without_flag_name_with_value(self):
|
|
||||||
with self.assertRaises(UnprocessedInputFlagException):
|
|
||||||
Command.parse_input_command('ssh 192.168.0.3')
|
|
||||||
|
|
||||||
def test_parse_raw_command_with_repeated_flag_name(self):
|
|
||||||
with self.assertRaises(RepeatedInputFlagsException):
|
|
||||||
Command.parse_input_command('ssh --host 192.168.0.3 --host 172.198.0.43')
|
|
||||||
|
|
||||||
def test_parse_empty_raw_command(self):
|
|
||||||
with self.assertRaises(EmptyInputCommandException):
|
|
||||||
Command.parse_input_command('')
|
|
||||||
|
|
||||||
def test_get_command_description(self):
|
|
||||||
self.assertEqual(Command(trigger='test', description='test description').get_description(), 'test description')
|
|
||||||
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
from argenta.command.params.flag import Flag
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class TestFlag(unittest.TestCase):
|
|
||||||
def test_get_string_entity(self):
|
|
||||||
self.assertEqual(Flag(flag_name='test').get_string_entity(),
|
|
||||||
'--test')
|
|
||||||
|
|
||||||
def test_get_string_entity2(self):
|
|
||||||
self.assertEqual(Flag(flag_name='test',
|
|
||||||
flag_prefix='---').get_string_entity(),
|
|
||||||
'---test')
|
|
||||||
|
|
||||||
def test_get_flag_name(self):
|
|
||||||
self.assertEqual(Flag(flag_name='test').get_flag_name(),
|
|
||||||
'test')
|
|
||||||
|
|
||||||
def test_get_flag_prefix(self):
|
|
||||||
self.assertEqual(Flag(flag_name='test').get_flag_prefix(),
|
|
||||||
'--')
|
|
||||||
|
|
||||||
def test_get_flag_prefix2(self):
|
|
||||||
self.assertEqual(Flag(flag_name='test',
|
|
||||||
flag_prefix='--').get_flag_prefix(),
|
|
||||||
'--')
|
|
||||||
|
|
||||||
def test_get_flag_value_without_set(self):
|
|
||||||
self.assertEqual(Flag(flag_name='test').get_value(),
|
|
||||||
None)
|
|
||||||
|
|
||||||
def test_get_flag_value_with_set(self):
|
|
||||||
flag = Flag(flag_name='test')
|
|
||||||
flag.set_value('example')
|
|
||||||
self.assertEqual(flag.get_value(), 'example')
|
|
||||||
|
|
||||||
def test_validate_incorrect_flag_value_with_list_of_possible_flag_values(self):
|
|
||||||
flag = Flag(flag_name='test', possible_flag_values=['1', '2', '3'])
|
|
||||||
self.assertEqual(flag.validate_input_flag_value('bad value'), False)
|
|
||||||
|
|
||||||
def test_validate_correct_flag_value_with_list_of_possible_flag_values(self):
|
|
||||||
flag = Flag(flag_name='test', possible_flag_values=['1', '2', '3'])
|
|
||||||
self.assertEqual(flag.validate_input_flag_value('1'), True)
|
|
||||||
|
|
||||||
def test_validate_incorrect_flag_value_with_pattern_of_possible_flag_values(self):
|
|
||||||
flag = Flag(flag_name='test', possible_flag_values=re.compile(r'192.168.\d+.\d+'))
|
|
||||||
self.assertEqual(flag.validate_input_flag_value('152.123.9.8'), False)
|
|
||||||
|
|
||||||
def test_validate_correct_flag_value_with_pattern_of_possible_flag_values(self):
|
|
||||||
flag = Flag(flag_name='test', possible_flag_values=re.compile(r'192.168.\d+.\d+'))
|
|
||||||
self.assertEqual(flag.validate_input_flag_value('192.168.9.8'), True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
from argenta.command.params.flag import Flag, FlagsGroup
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class TestFlagsGroup(unittest.TestCase):
|
|
||||||
def test_get_flags(self):
|
|
||||||
flags = FlagsGroup()
|
|
||||||
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 = FlagsGroup()
|
|
||||||
flags.add_flag(Flag('test'))
|
|
||||||
self.assertEqual(len(flags.get_flags()), 1)
|
|
||||||
|
|
||||||
def test_add_flags(self):
|
|
||||||
flags = FlagsGroup()
|
|
||||||
flags.add_flags([Flag('test'), Flag('test2')])
|
|
||||||
self.assertEqual(len(flags.get_flags()), 2)
|
|
||||||
|
|
||||||
def test_unparse_flags_to_dict(self):
|
|
||||||
list_of_flags = [
|
|
||||||
Flag('test1'),
|
|
||||||
Flag('test2'),
|
|
||||||
Flag('test3'),
|
|
||||||
]
|
|
||||||
flags = FlagsGroup(list_of_flags)
|
|
||||||
serialized_flags = flags.unparse_to_dict()
|
|
||||||
needed_result = {'test1': {'name': 'test1',
|
|
||||||
'prefix': '--',
|
|
||||||
'string_entity': '--test1',
|
|
||||||
'value': None},
|
|
||||||
'test2': {'name': 'test2',
|
|
||||||
'prefix': '--',
|
|
||||||
'string_entity': '--test2',
|
|
||||||
'value': None},
|
|
||||||
'test3': {'name': 'test3',
|
|
||||||
'prefix': '--',
|
|
||||||
'string_entity': '--test3',
|
|
||||||
'value': None}}
|
|
||||||
|
|
||||||
self.assertDictEqual(serialized_flags, needed_result)
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
from argenta.command.params.flag import FlagsGroup, Flag
|
|
||||||
from argenta.router import Router
|
|
||||||
from argenta.command import Command
|
|
||||||
from argenta.router.exceptions import RepeatedCommandException, TriggerCannotContainSpacesException
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
self.assertEqual(Router(title='test title').get_title(), 'test title')
|
|
||||||
|
|
||||||
def test_input_correct_command(self):
|
|
||||||
router = Router()
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
self.assertEqual(router.input_command_handler(Command(trigger='test')), 'correct result')
|
|
||||||
|
|
||||||
def test_input_command_with_invalid_flag(self):
|
|
||||||
router = Router()
|
|
||||||
router.set_invalid_input_flag_handler(lambda x: x)
|
|
||||||
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
input_command = Command(trigger='test')
|
|
||||||
input_command._set_input_flags(FlagsGroup([Flag('host')]))
|
|
||||||
|
|
||||||
self.assertEqual(router.input_command_handler(input_command), None)
|
|
||||||
|
|
||||||
def test_input_correct_command_with_one_register_and_ignore_command_register(self):
|
|
||||||
router = Router()
|
|
||||||
router.set_ignore_command_register(True)
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
self.assertEqual(router.input_command_handler(Command(trigger='test')), 'correct result')
|
|
||||||
|
|
||||||
def test_input_correct_command_with_different_register_and_ignore_command_register(self):
|
|
||||||
router = Router()
|
|
||||||
router.set_ignore_command_register(True)
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
self.assertEqual(router.input_command_handler(Command(trigger='TeSt')), 'correct result')
|
|
||||||
|
|
||||||
def test_input_incorrect_command_with_ignore_command_register(self):
|
|
||||||
router = Router()
|
|
||||||
router.set_ignore_command_register(True)
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
self.assertEqual(router.input_command_handler(Command(trigger='Test2')), None)
|
|
||||||
|
|
||||||
def test_register_repeated_commands_with_one_register(self):
|
|
||||||
router = Router()
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
with self.assertRaises(RepeatedCommandException):
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
def test_register_commands_with_different_register(self):
|
|
||||||
router = Router()
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
try:
|
|
||||||
@router.command(Command(trigger='Test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
except RepeatedCommandException:
|
|
||||||
self.fail('RepeatedCommandException should not have been thrown')
|
|
||||||
|
|
||||||
def test_register_repeated_commands_with_one_register_and_set_ignore_command_register(self):
|
|
||||||
router = Router()
|
|
||||||
router.set_ignore_command_register(True)
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
with self.assertRaises(RepeatedCommandException):
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
def test_register_repeated_commands_with_different_register_and_set_ignore_command_register(self):
|
|
||||||
router = Router()
|
|
||||||
router.set_ignore_command_register(True)
|
|
||||||
@router.command(Command(trigger='test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
with self.assertRaises(RepeatedCommandException):
|
|
||||||
@router.command(Command(trigger='Test'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
def test_register_command_with_spaces_in_trigger(self):
|
|
||||||
router = Router()
|
|
||||||
with self.assertRaises(TriggerCannotContainSpacesException):
|
|
||||||
@router.command(Command(trigger='command with spaces'))
|
|
||||||
def test():
|
|
||||||
return 'correct result'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from argenta.command.models import InputCommand, Command
|
||||||
|
from argenta.app import App
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from argenta.router import Router
|
||||||
|
|
||||||
|
|
||||||
|
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._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._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._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._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()}
|
||||||
|
self.assertEqual(app._is_unknown_command(InputCommand('tW')), False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
from argenta.command.flag import Flag, InputFlag
|
||||||
|
from argenta.command.flag.flags import Flags
|
||||||
|
from argenta.command.flag.models import PossibleValues
|
||||||
|
from argenta.command.models import InputCommand, Command, ValidationStatus
|
||||||
|
from argenta.command.exceptions import (UnprocessedInputFlagException,
|
||||||
|
RepeatedInputFlagsException,
|
||||||
|
EmptyInputCommandException)
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class TestInputCommand(unittest.TestCase):
|
||||||
|
def test_parse_correct_raw_command(self):
|
||||||
|
self.assertEqual(InputCommand.parse('ssh --host 192.168.0.3').trigger, 'ssh')
|
||||||
|
|
||||||
|
def test_parse_raw_command_without_flag_name_with_value(self):
|
||||||
|
with self.assertRaises(UnprocessedInputFlagException):
|
||||||
|
InputCommand.parse('ssh 192.168.0.3')
|
||||||
|
|
||||||
|
def test_parse_raw_command_with_repeated_flag_name(self):
|
||||||
|
with self.assertRaises(RepeatedInputFlagsException):
|
||||||
|
InputCommand.parse('ssh --host 192.168.0.3 --host 172.198.0.43')
|
||||||
|
|
||||||
|
def test_parse_empty_raw_command(self):
|
||||||
|
with self.assertRaises(EmptyInputCommandException):
|
||||||
|
InputCommand.parse('')
|
||||||
|
|
||||||
|
def test_validate_valid_input_flag1(self):
|
||||||
|
command = Command('some', flags=Flag('test'))
|
||||||
|
self.assertEqual(command.validate_input_flag(InputFlag('test', input_value=None, status=None)), ValidationStatus.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', input_value=None, status=None)), ValidationStatus.VALID)
|
||||||
|
|
||||||
|
def test_validate_undefined_input_flag1(self):
|
||||||
|
command = Command('some', flags=Flag('test'))
|
||||||
|
self.assertEqual(command.validate_input_flag(InputFlag('more', input_value=None, status=None)), ValidationStatus.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', input_value=None, status=None)), ValidationStatus.UNDEFINED)
|
||||||
|
|
||||||
|
def test_validate_undefined_input_flag3(self):
|
||||||
|
command = Command('some')
|
||||||
|
self.assertEqual(command.validate_input_flag(InputFlag('case', input_value=None, status=None)), ValidationStatus.UNDEFINED)
|
||||||
|
|
||||||
|
def test_invalid_input_flag1(self):
|
||||||
|
command = Command('some', flags=Flag('test', possible_values=PossibleValues.NEITHER))
|
||||||
|
self.assertEqual(command.validate_input_flag(InputFlag('test', input_value='example', status=None)), ValidationStatus.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', input_value='slay', status=None)), ValidationStatus.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', input_value='example', status=None)), ValidationStatus.INVALID)
|
||||||
|
|
||||||
|
def test_isinstance_parse_correct_raw_command(self):
|
||||||
|
cmd = InputCommand.parse('ssh --host 192.168.0.3')
|
||||||
|
self.assertIsInstance(cmd, InputCommand)
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from argenta.app.dividing_line import DynamicDividingLine, StaticDividingLine
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class TestDividingLine(unittest.TestCase):
|
||||||
|
def test_get_static_dividing_line_full_line(self):
|
||||||
|
line = StaticDividingLine('-')
|
||||||
|
self.assertEqual(line.get_full_static_line(is_override=True).count('-'), 25)
|
||||||
|
|
||||||
|
def test_get_dynamic_dividing_line_full_line(self):
|
||||||
|
line = DynamicDividingLine()
|
||||||
|
self.assertEqual(line.get_full_dynamic_line(length=20, is_override=True).count('-'), 20)
|
||||||
|
|
||||||
|
def test_get_dividing_line_unit_part(self):
|
||||||
|
line = StaticDividingLine('')
|
||||||
|
self.assertEqual(line.get_unit_part(), ' ')
|
||||||
|
|
||||||
|
def test_get_dividing_line2_unit_part(self):
|
||||||
|
line = StaticDividingLine('+-0987654321!@#$%^&*()_')
|
||||||
|
self.assertEqual(line.get_unit_part(), '+')
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
from argenta.command.flag import Flag, InputFlag, PossibleValues
|
||||||
|
from argenta.command.flag.flags import InputFlags, Flags
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class TestFlag(unittest.TestCase):
|
||||||
|
def test_get_string_entity(self):
|
||||||
|
self.assertEqual(Flag(name='test').string_entity,
|
||||||
|
'--test')
|
||||||
|
|
||||||
|
def test_get_string_entity2(self):
|
||||||
|
self.assertEqual(Flag(name='test',
|
||||||
|
prefix='---').string_entity,
|
||||||
|
'---test')
|
||||||
|
|
||||||
|
def test_get_flag_name(self):
|
||||||
|
self.assertEqual(Flag(name='test').name,
|
||||||
|
'test')
|
||||||
|
|
||||||
|
def test_get_flag_prefix(self):
|
||||||
|
self.assertEqual(Flag(name='test').prefix,
|
||||||
|
'--')
|
||||||
|
|
||||||
|
def test_get_flag_prefix2(self):
|
||||||
|
self.assertEqual(Flag(name='test',
|
||||||
|
prefix='--').prefix,
|
||||||
|
'--')
|
||||||
|
|
||||||
|
def test_get_flag_value_without_set(self):
|
||||||
|
self.assertEqual(InputFlag(name='test', input_value=None, status=None).input_value,
|
||||||
|
None)
|
||||||
|
|
||||||
|
def test_get_flag_value_with_set(self):
|
||||||
|
flag = InputFlag(name='test', input_value='example', status=None)
|
||||||
|
self.assertEqual(flag.input_value, 'example')
|
||||||
|
|
||||||
|
def test_validate_incorrect_flag_value_with_list_of_possible_flag_values(self):
|
||||||
|
flag = Flag(name='test', possible_values=['1', '2', '3'])
|
||||||
|
self.assertEqual(flag.validate_input_flag_value('bad value'), False)
|
||||||
|
|
||||||
|
def test_validate_correct_flag_value_with_list_of_possible_flag_values(self):
|
||||||
|
flag = Flag(name='test', possible_values=['1', '2', '3'])
|
||||||
|
self.assertEqual(flag.validate_input_flag_value('1'), True)
|
||||||
|
|
||||||
|
def test_validate_incorrect_flag_value_with_pattern_of_possible_flag_values(self):
|
||||||
|
flag = Flag(name='test', possible_values=re.compile(r'192.168.\d+.\d+'))
|
||||||
|
self.assertEqual(flag.validate_input_flag_value('152.123.9.8'), False)
|
||||||
|
|
||||||
|
def test_validate_correct_flag_value_with_pattern_of_possible_flag_values(self):
|
||||||
|
flag = Flag(name='test', possible_values=re.compile(r'192.168.\d+.\d+'))
|
||||||
|
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=PossibleValues.NEITHER)
|
||||||
|
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=PossibleValues.NEITHER)
|
||||||
|
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=PossibleValues.NEITHER)
|
||||||
|
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=PossibleValues.ALL)
|
||||||
|
self.assertEqual(flag.validate_input_flag_value('random value'), True)
|
||||||
|
|
||||||
|
def test_get_input_flag1(self):
|
||||||
|
flag = InputFlag(name='test', input_value=None, status=None)
|
||||||
|
input_flags = InputFlags([flag])
|
||||||
|
self.assertEqual(input_flags.get_flag_by_name('test'), flag)
|
||||||
|
|
||||||
|
def test_get_input_flag2(self):
|
||||||
|
flag = InputFlag(name='test', input_value=None, status=None)
|
||||||
|
flag2 = InputFlag(name='some', input_value=None, status=None)
|
||||||
|
input_flags = InputFlags([flag, flag2])
|
||||||
|
self.assertEqual(input_flags.get_flag_by_name('some'), flag2)
|
||||||
|
|
||||||
|
def test_get_undefined_input_flag(self):
|
||||||
|
flag = InputFlag(name='test', input_value=None, status=None)
|
||||||
|
flag2 = InputFlag(name='some', input_value=None, status=None)
|
||||||
|
input_flags = InputFlags([flag, flag2])
|
||||||
|
self.assertEqual(input_flags.get_flag_by_name('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.flags,
|
||||||
|
list_of_flags)
|
||||||
|
|
||||||
|
def test_add_flag(self):
|
||||||
|
flags = Flags()
|
||||||
|
flags.add_flag(Flag('test'))
|
||||||
|
self.assertEqual(len(flags.flags), 1)
|
||||||
|
|
||||||
|
def test_add_flags(self):
|
||||||
|
flags = Flags()
|
||||||
|
flags.add_flags([Flag('test'), Flag('test2')])
|
||||||
|
self.assertEqual(len(flags.flags), 2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
from argenta.command.flag import InputFlag, Flag
|
||||||
|
from argenta.command.flag.flags import Flags, InputFlags
|
||||||
|
from argenta.command.flag.models import PossibleValues, ValidationStatus
|
||||||
|
from argenta.response.entity import Response
|
||||||
|
from argenta.router import Router
|
||||||
|
from argenta.command import Command
|
||||||
|
from argenta.router.entity import _structuring_input_flags, _validate_command, _validate_func_args # pyright: ignore[reportPrivateUsage]
|
||||||
|
from argenta.router.exceptions import (TriggerContainSpacesException,
|
||||||
|
RepeatedFlagNameException,
|
||||||
|
TooManyTransferredArgsException,
|
||||||
|
RequiredArgumentNotPassedException)
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class TestRouter(unittest.TestCase):
|
||||||
|
def test_register_command_with_spaces_in_trigger(self):
|
||||||
|
with self.assertRaises(TriggerContainSpacesException):
|
||||||
|
_validate_command(Command(trigger='command with spaces'))
|
||||||
|
|
||||||
|
def test_register_command_with_repeated_flags(self):
|
||||||
|
with self.assertRaises(RepeatedFlagNameException):
|
||||||
|
_validate_command(Command(trigger='command', flags=Flags([Flag('test'), Flag('test')])))
|
||||||
|
|
||||||
|
def test_structuring_input_flags1(self):
|
||||||
|
cmd = Command('cmd')
|
||||||
|
input_flags = InputFlags([InputFlag('ssh', input_value=None, status=None)])
|
||||||
|
self.assertEqual(_structuring_input_flags(cmd, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value=None, status=ValidationStatus.UNDEFINED)]))
|
||||||
|
|
||||||
|
def test_structuring_input_flags2(self):
|
||||||
|
cmd = Command('cmd')
|
||||||
|
input_flags = InputFlags([InputFlag('ssh', input_value='some', status=None)])
|
||||||
|
self.assertEqual(_structuring_input_flags(cmd, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='some', status=ValidationStatus.UNDEFINED)]))
|
||||||
|
|
||||||
|
def test_structuring_input_flags3(self):
|
||||||
|
cmd = Command('cmd', flags=Flag('port'))
|
||||||
|
input_flags = InputFlags([InputFlag('ssh', input_value='some2', status=None)])
|
||||||
|
self.assertEqual(_structuring_input_flags(cmd, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='some2', status=ValidationStatus.UNDEFINED)]))
|
||||||
|
|
||||||
|
def test_structuring_input_flags4(self):
|
||||||
|
command = Command('cmd', flags=Flag('ssh', possible_values=PossibleValues.NEITHER))
|
||||||
|
input_flags = InputFlags([InputFlag('ssh', input_value='some3', status=None)])
|
||||||
|
self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='some3', status=ValidationStatus.INVALID)]))
|
||||||
|
|
||||||
|
def test_structuring_input_flags5(self):
|
||||||
|
command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'some[1-5]$')))
|
||||||
|
input_flags = InputFlags([InputFlag('ssh', input_value='some40', status=None)])
|
||||||
|
self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='some40', status=ValidationStatus.INVALID)]))
|
||||||
|
|
||||||
|
def test_structuring_input_flags6(self):
|
||||||
|
command = Command('cmd', flags=Flag('ssh', possible_values=['example']))
|
||||||
|
input_flags = InputFlags([InputFlag('ssh', input_value='example2', status=None)])
|
||||||
|
self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='example2', status=ValidationStatus.INVALID)]))
|
||||||
|
|
||||||
|
def test_structuring_input_flags7(self):
|
||||||
|
command = Command('cmd', flags=Flag('port'))
|
||||||
|
input_flags = InputFlags([InputFlag('port', input_value='some2', status=None)])
|
||||||
|
self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)]))
|
||||||
|
|
||||||
|
def test_structuring_input_flags8(self):
|
||||||
|
command = Command('cmd', flags=Flag('port', possible_values=['some2', 'some3']))
|
||||||
|
input_flags = InputFlags([InputFlag('port', input_value='some2', status=None)])
|
||||||
|
self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)]))
|
||||||
|
|
||||||
|
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', input_value='more5', status=None)])
|
||||||
|
self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='more5', status=ValidationStatus.VALID)]))
|
||||||
|
|
||||||
|
def test_structuring_input_flags10(self):
|
||||||
|
command = Command('cmd', flags=Flag('ssh', possible_values=PossibleValues.NEITHER))
|
||||||
|
input_flags = InputFlags([InputFlag('ssh', input_value=None, status=None)])
|
||||||
|
self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value=None, status=ValidationStatus.VALID)]))
|
||||||
|
|
||||||
|
def test_validate_incorrect_func_args1(self):
|
||||||
|
def handler():
|
||||||
|
pass
|
||||||
|
with self.assertRaises(RequiredArgumentNotPassedException):
|
||||||
|
_validate_func_args(handler) # pyright: ignore[reportArgumentType]
|
||||||
|
|
||||||
|
def test_validate_incorrect_func_args2(self):
|
||||||
|
def handler(args, kwargs): # pyright: ignore[reportMissingParameterType, reportUnknownParameterType]
|
||||||
|
pass
|
||||||
|
with self.assertRaises(TooManyTransferredArgsException):
|
||||||
|
_validate_func_args(handler) # pyright: ignore[reportArgumentType]
|
||||||
|
|
||||||
|
def test_get_router_aliases(self):
|
||||||
|
router = Router()
|
||||||
|
@router.command(Command('some', aliases=['test', 'case']))
|
||||||
|
def handler(response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
|
pass
|
||||||
|
self.assertListEqual(router.aliases, ['test', 'case'])
|
||||||
|
|
||||||
|
def test_get_router_aliases2(self):
|
||||||
|
router = Router()
|
||||||
|
@router.command(Command('some', aliases=['test', 'case']))
|
||||||
|
def handler(response: Response): # pyright: ignore[reportUnusedFunction]
|
||||||
|
pass
|
||||||
|
@router.command(Command('ext', aliases=['more', 'foo']))
|
||||||
|
def handler2(response: Response): # pyright: ignore[reportUnusedFunction]
|
||||||
|
pass
|
||||||
|
self.assertListEqual(router.aliases, ['test', 'case', 'more', 'foo'])
|
||||||
|
|
||||||
|
def test_get_router_aliases3(self):
|
||||||
|
router = Router()
|
||||||
|
@router.command(Command('some'))
|
||||||
|
def handler(response: Response): # pyright: ignore[reportUnusedFunction]
|
||||||
|
pass
|
||||||
|
self.assertListEqual(router.aliases, [])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||