mirror of
https://github.com/koloideal/Argenta.git
synced 2026-06-10 18:15:28 +03:00
Update documentation and code snippets
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
from argenta import Router, Response
|
||||||
|
from argenta.command import Command, Flag, PossibleValues
|
||||||
|
from argenta.command.flag import ValidationStatus
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
@router.command(Command(
|
||||||
|
"deploy",
|
||||||
|
flags=Flag("verbose", possible_values=PossibleValues.NEITHER)
|
||||||
|
))
|
||||||
|
def deploy_handler(response: Response):
|
||||||
|
# Check for toggle flag presence
|
||||||
|
verbose_flag = response.input_flags.get_flag_by_name("verbose")
|
||||||
|
|
||||||
|
if verbose_flag and verbose_flag.status == ValidationStatus.VALID:
|
||||||
|
print("Deploying with verbose output...")
|
||||||
|
# Detailed logic
|
||||||
|
elif verbose_flag and verbose_flag.status == ValidationStatus.INVALID:
|
||||||
|
print("Incorrect flag value")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("Deploying...")
|
||||||
|
# Normal logic
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from argenta import Router, Response
|
||||||
|
from argenta.command import Command, Flag
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
@router.command(Command("greet", flags=Flag("name")))
|
||||||
|
def greet_handler(response: Response):
|
||||||
|
# Get flag by name
|
||||||
|
name_flag = response.input_flags.get_flag_by_name("name")
|
||||||
|
|
||||||
|
# Check if flag was passed
|
||||||
|
if name_flag:
|
||||||
|
print(f"Hello, {name_flag.input_value}!")
|
||||||
|
else:
|
||||||
|
print("Hello, stranger!")
|
||||||
@@ -5,9 +5,9 @@ flag_with_value = InputFlag(
|
|||||||
)
|
)
|
||||||
|
|
||||||
flag_without_value = InputFlag(
|
flag_without_value = InputFlag(
|
||||||
name="help", prefix="-", input_value=None, status=ValidationStatus.VALID
|
name="help", prefix="-", input_value='', status=ValidationStatus.VALID
|
||||||
)
|
)
|
||||||
|
|
||||||
# String representation includes value
|
# String representation includes value
|
||||||
print(str(flag_with_value)) # --output result.txt
|
print(str(flag_with_value)) # --output result.txt
|
||||||
print(str(flag_without_value)) # -help None
|
print(str(flag_without_value)) # -help
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import operator
|
||||||
|
import re
|
||||||
|
|
||||||
|
from argenta import App, Orchestrator, Response, Router
|
||||||
|
from argenta.app import DynamicDividingLine
|
||||||
|
from argenta.command import Command, Flag, Flags
|
||||||
|
from argenta.response.status import ResponseStatus
|
||||||
|
|
||||||
|
router = Router("Calculator")
|
||||||
|
|
||||||
|
operations = {
|
||||||
|
'mul': operator.mul,
|
||||||
|
'sub': operator.sub,
|
||||||
|
'add': operator.add
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.command(
|
||||||
|
Command(
|
||||||
|
"calc",
|
||||||
|
description="Calculator with two numbers",
|
||||||
|
flags=Flags(
|
||||||
|
[
|
||||||
|
Flag("a", possible_values=re.compile(r"^\d{,5}$")), # First number
|
||||||
|
Flag("b", possible_values=re.compile(r"^\d{,5}$")), # Second number
|
||||||
|
Flag("operation", possible_values=["add", "sub", "mul"]), # Operation: add, sub, mul
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
def calc_handler(response: Response):
|
||||||
|
# Get flag values
|
||||||
|
a_flag = response.input_flags.get_flag_by_name("a")
|
||||||
|
b_flag = response.input_flags.get_flag_by_name("b")
|
||||||
|
op_flag = response.input_flags.get_flag_by_name("op")
|
||||||
|
|
||||||
|
# Check that all flags are provided
|
||||||
|
if response.status != ResponseStatus.ALL_FLAGS_VALID or not all([a_flag, b_flag, op_flag]):
|
||||||
|
print("Error: must specify --a, --b and --op")
|
||||||
|
return
|
||||||
|
|
||||||
|
a = float(a_flag.input_value)
|
||||||
|
b = float(b_flag.input_value)
|
||||||
|
operation = op_flag.input_value
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = operations[operation](a, b)
|
||||||
|
except ZeroDivisionError:
|
||||||
|
print("Can't divide by zero")
|
||||||
|
else:
|
||||||
|
print(f"Result: {result}")
|
||||||
|
|
||||||
|
|
||||||
|
app = App(
|
||||||
|
initial_message="Calculator",
|
||||||
|
repeat_command_groups_printing=False,
|
||||||
|
prompt=">> ",
|
||||||
|
dividing_line=DynamicDividingLine("~"),
|
||||||
|
)
|
||||||
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app.include_router(router)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+2
-2
@@ -17,9 +17,9 @@ Argenta
|
|||||||
.. image:: https://github.com/koloideal/Argenta/blob/main/imgs/mock_app_preview4.png?raw=True
|
.. image:: https://github.com/koloideal/Argenta/blob/main/imgs/mock_app_preview4.png?raw=True
|
||||||
:alt: App example
|
:alt: App example
|
||||||
|
|
||||||
Argenta предназначена для создания приложений, работающих в собственном контексте (scope). Это означает, что при запуске пользователь входит в интерактивную сессию, где ему доступна вся реализованная вами функциональность.
|
Argenta предназначена для создания приложений, работающих в собственном контексте (scope). Это означает, что приложение запускается один раз и создаёт интерактивную сессию, похожую на Python REPL или MySQL консоль. При запуске пользователь входит в эту сессию, где ему доступна вся реализованная вами функциональность.
|
||||||
|
|
||||||
Один из ключевых принципов библиотеки — цикличность. После выполнения команды пользователь остаётся в интерактивной сессии, в отличие от таких библиотек, как ``argparse``, ``click`` и ``typer``. Выход из сессии контролируется пользователем.
|
Один из ключевых принципов библиотеки — цикличность. После выполнения команды пользователь остаётся в интерактивной сессии и имеет доступ к созданной вами функциональности, в отличие от таких библиотек, как ``argparse``, ``click`` и ``typer``. Выход из сессии контролируется пользователем.
|
||||||
|
|
||||||
**Ключевые особенности:**
|
**Ключевые особенности:**
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ msgid ""
|
|||||||
"к БД или API-клиентов) в DI-контейнер."
|
"к БД или API-клиентов) в DI-контейнер."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"``custom_providers``: List of custom ``dishka.Provider`` providers for adding your services "
|
"``custom_providers``: List of custom ``dishka.Provider`` providers for adding your services "
|
||||||
"(e.g., database connections or API clients) to the DI container."
|
"(e.g., database connections or API clients) to the``di``container."
|
||||||
|
|
||||||
#: ../../root/api/orchestrator/index.rst:32
|
#: ../../root/api/orchestrator/index.rst:32
|
||||||
msgid ""
|
msgid ""
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ msgid "Как это работает?"
|
|||||||
msgstr "How Does It Work?"
|
msgstr "How Does It Work?"
|
||||||
|
|
||||||
#: ../../root/dependency_injection.rst:48
|
#: ../../root/dependency_injection.rst:48
|
||||||
msgid "В основе DI в Argenta лежат **провайдеры** и **контейнер**."
|
msgid "В основе``di``в Argenta лежат **провайдеры** и **контейнер**."
|
||||||
msgstr "DI in Argenta is based on **providers** and a **container**."
|
msgstr "DI in Argenta is based on **providers** and a **container**."
|
||||||
|
|
||||||
#: ../../root/dependency_injection.rst:50
|
#: ../../root/dependency_injection.rst:50
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ InputFlag
|
|||||||
|
|
||||||
.. py:attribute:: input_value
|
.. py:attribute:: input_value
|
||||||
|
|
||||||
Значение, переданное с флагом. Может быть `None` для флагов без значений.
|
Значение, переданное с флагом. Может быть ``''`` (пустой строкой) для флагов без значений.
|
||||||
|
|
||||||
.. py:attribute:: status
|
.. py:attribute:: status
|
||||||
:no-index:
|
:no-index:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ ArgParser
|
|||||||
Лучшие практики
|
Лучшие практики
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
Использовать атрибут ``parsed_argspace`` рекомендуется только на этапе настройки приложения. В обработчиках лучшей практикой является получение ``ArgSpace`` через DI. Подробнее см. :ref:`здесь <root_dependency_injection>`.
|
Использовать атрибут ``parsed_argspace`` рекомендуется только на этапе настройки приложения. В обработчиках лучшей практикой является получение ``ArgSpace`` через ``di``. Подробнее см. :ref:`здесь <root_dependency_injection>`.
|
||||||
|
|
||||||
**Пример использования:**
|
**Пример использования:**
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Orchestrator
|
|||||||
Создаёт и конфигурирует экземпляр ``Orchestrator``.
|
Создаёт и конфигурирует экземпляр ``Orchestrator``.
|
||||||
|
|
||||||
* ``arg_parser``: Экземпляр ``ArgParser``, отвечающий за парсинг аргументов командной строки при запуске скрипта (не путать с командами в интерактивном режиме).
|
* ``arg_parser``: Экземпляр ``ArgParser``, отвечающий за парсинг аргументов командной строки при запуске скрипта (не путать с командами в интерактивном режиме).
|
||||||
* ``custom_providers``: Список пользовательских провайдеров ``dishka.Provider`` для добавления ваших сервисов (например, подключений к БД или API-клиентов) в DI-контейнер.
|
* ``custom_providers``: Список пользовательских провайдеров ``dishka.Provider`` для добавления ваших сервисов (например, подключений к БД или API-клиентов) в di-контейнер.
|
||||||
* ``auto_inject_handlers``: Если **True** (по умолчанию), ``dishka`` автоматически внедрит зависимости в обработчики команд, инспектируя их сигнатуры.
|
* ``auto_inject_handlers``: Если **True** (по умолчанию), ``dishka`` автоматически внедрит зависимости в обработчики команд, инспектируя их сигнатуры.
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
Внедрение зависимостей
|
Внедрение зависимостей
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
Внедрение зависимостей (Dependency Injection, DI) — это паттерн проектирования, который помогает писать слабосвязанный, легко тестируемый и расширяемый код. Вместо того чтобы обработчики сами создавали нужные им объекты (зависимости), они получают их извне.
|
Внедрение зависимостей (Dependency Injection, ``di`` ) — это паттерн проектирования, который помогает писать слабосвязанный, легко тестируемый и расширяемый код. Вместо того чтобы обработчики сами создавали нужные им объекты (зависимости), они получают их извне.
|
||||||
|
|
||||||
``Argenta`` использует библиотеку ``dishka`` для реализации DI, что позволяет декларативно объявлять зависимости прямо в сигнатурах ваших обработчиков.
|
``Argenta`` использует библиотеку ``dishka`` для реализации ``di``, что позволяет декларативно объявлять зависимости прямо в сигнатурах ваших обработчиков.
|
||||||
Подробнее о **DI**, **IoC** и API для создания провайдеров можно прочитать в `официальной документации dishka <https://dishka.readthedocs.io/en/stable/di_intro.html>`_.
|
Подробнее о **DI**, **IoC** и API для создания провайдеров можно прочитать в `официальной документации dishka <https://dishka.readthedocs.io/en/stable/di_intro.html>`_.
|
||||||
|
|
||||||
-----
|
-----
|
||||||
@@ -32,7 +32,10 @@
|
|||||||
:language: python
|
:language: python
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
После создания провайдера его необходимо зарегистрировать в оркестраторе:
|
После создания провайдера его необходимо зарегистрировать в оркестраторе.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Провайдеры регистрируются в ``Orchestrator``, а не в ``App``, так как оркестратор отвечает за настройку di-контейнера на уровне всего приложения. Вы можете передать список из нескольких провайдеров через параметр ``custom_providers``.
|
||||||
|
|
||||||
**Пример использования:**
|
**Пример использования:**
|
||||||
|
|
||||||
@@ -45,7 +48,7 @@
|
|||||||
Как это работает?
|
Как это работает?
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
В основе DI в Argenta лежат **провайдеры** и **контейнер**.
|
В основе ``di`` в Argenta лежат **провайдеры** и **контейнер**.
|
||||||
|
|
||||||
* **Провайдер (Provider)** — это "рецепт", который объясняет, как создавать и настраивать ту или иную зависимость (например, подключение к БД, API-клиент или любой другой сервис).
|
* **Провайдер (Provider)** — это "рецепт", который объясняет, как создавать и настраивать ту или иную зависимость (например, подключение к БД, API-клиент или любой другой сервис).
|
||||||
* **Контейнер (IoC Container)** — это "фабрика", которая хранит все рецепты (провайдеры) и по запросу создаёт и выдаёт готовые зависимости.
|
* **Контейнер (IoC Container)** — это "фабрика", которая хранит все рецепты (провайдеры) и по запросу создаёт и выдаёт готовые зависимости.
|
||||||
@@ -68,7 +71,7 @@
|
|||||||
Обмен данными между обработчиками
|
Обмен данными между обработчиками
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
Помимо DI, обработчики могут обмениваться данными в рамках сессии через **объект контекста**. В ``Argenta`` эту роль выполняет объект ``DataBridge``.
|
Помимо ``di``, обработчики могут обмениваться данными в рамках сессии через **объект контекста**. В ``Argenta`` эту роль выполняет объект ``DataBridge``.
|
||||||
|
|
||||||
Каждый обработчик может записывать в него данные, а также читать, обновлять и удалять их.
|
Каждый обработчик может записывать в него данные, а также читать, обновлять и удалять их.
|
||||||
|
|
||||||
|
|||||||
+32
-2
@@ -1,7 +1,7 @@
|
|||||||
.. _root_flags:
|
.. _root_flags:
|
||||||
|
|
||||||
Флаги вводимых команд
|
Флаги команд
|
||||||
=====================
|
============
|
||||||
|
|
||||||
Флаги — это специальные параметры, которые пользователь может добавлять к командам для управления их поведением.
|
Флаги — это специальные параметры, которые пользователь может добавлять к командам для управления их поведением.
|
||||||
|
|
||||||
@@ -56,6 +56,36 @@
|
|||||||
|
|
||||||
Флаг состоит из префикса (``-``, ``--`` или ``---``), имени и, опционально, значения, которое указывается через пробел.
|
Флаг состоит из префикса (``-``, ``--`` или ``---``), имени и, опционально, значения, которое указывается через пробел.
|
||||||
|
|
||||||
|
**Примеры:**
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
greet --name John # Flag with value
|
||||||
|
deploy --verbose # Flag without value (switch)
|
||||||
|
backup -f --compress # Several flags
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
Работа с флагами в обработчиках
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
Чтобы получить значение флага в обработчике, используйте объект ``response.input_flags`` типа :ref:`InputFlags <root_api_command_input_flags>`.
|
||||||
|
|
||||||
|
**Пример с флагом, имеющим значение:**
|
||||||
|
|
||||||
|
.. literalinclude:: ../code_snippets/flags/greet_handler.py
|
||||||
|
:language: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
**Пример с флагом-переключателем:**
|
||||||
|
|
||||||
|
.. literalinclude:: ../code_snippets/flags/deploy_handler.py
|
||||||
|
:language: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
Подробнее о работе с объектом ``InputFlags`` см. в разделе :ref:`InputFlags <root_api_command_input_flags>`.
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
Два типа флагов
|
Два типа флагов
|
||||||
|
|||||||
@@ -23,6 +23,14 @@
|
|||||||
:language: python
|
:language: python
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
|
**Запуск**
|
||||||
|
|
||||||
|
Сохраните код в файл (например, ``main.py``) и запустите:
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
python main.py
|
||||||
|
|
||||||
**Результат**
|
**Результат**
|
||||||
|
|
||||||
.. image:: https://i.ibb.co/35q24Bh8/image.png
|
.. image:: https://i.ibb.co/35q24Bh8/image.png
|
||||||
@@ -30,10 +38,30 @@
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
Более сложный пример: Менеджер задач
|
Промежуточный пример: Калькулятор с флагами
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
Прежде чем перейти к сложному примеру с ``di``, рассмотрим промежуточный вариант — калькулятор, который использует флаги для управления поведением.
|
||||||
|
|
||||||
|
.. literalinclude:: ../code_snippets/quickstart/calculator_app.py
|
||||||
|
:language: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
**Использование:**
|
||||||
|
|
||||||
|
.. code-block:: shell
|
||||||
|
|
||||||
|
calc --a 10 --b 5 --operation add # Result: 15.0
|
||||||
|
calc --a 10 --b 5 --operation mul # Result: 50.0
|
||||||
|
|
||||||
|
Этот пример показывает, как работать с флагами без использования ``di``. Теперь перейдём к более сложному примеру.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
Сложный пример: Менеджер задач с ``di``
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
В этом руководстве мы создадим простое, но полнофункциональное CLI-приложение «Менеджер задач», которое продемонстрирует ключевые возможности Argenta.
|
В этом руководстве мы создадим полнофункциональное CLI-приложение «Менеджер задач», которое продемонстрирует работу с внедрением зависимостей.
|
||||||
|
|
||||||
1. **Установка**
|
1. **Установка**
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,20 @@
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
Когда нужно отключать перехват stdout
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Отключайте перехват ``stdout`` (``disable_redirect_stdout=True`` в ``Router``), если ваши команды:
|
||||||
|
|
||||||
|
✓ Используют ``input()`` для интерактивного ввода данных от пользователя
|
||||||
|
✓ Используют прогресс-бары (``tqdm``, ``rich.progress``)
|
||||||
|
✓ Выводят данные в реальном времени (streaming, логи)
|
||||||
|
✓ Используют библиотеки, которые напрямую работают с ``stdout``
|
||||||
|
|
||||||
|
Для обычных команд с ``print()`` перехват можно оставить включённым — это не влияет на их работу.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
Механизм перехвата ``stdout``
|
Механизм перехвата ``stdout``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ from re import Pattern
|
|||||||
from typing import Literal, override
|
from typing import Literal, override
|
||||||
|
|
||||||
|
|
||||||
|
PREFIX_TYPE = Literal["-", "--", "---"]
|
||||||
|
|
||||||
|
|
||||||
class PossibleValues(Enum):
|
class PossibleValues(Enum):
|
||||||
NEITHER = "NEITHER"
|
NEITHER = "NEITHER"
|
||||||
ALL = "ALL"
|
ALL = "ALL"
|
||||||
@@ -21,7 +24,7 @@ class Flag:
|
|||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
*,
|
*,
|
||||||
prefix: Literal["-", "--", "---"] = "--",
|
prefix: PREFIX_TYPE = "--",
|
||||||
possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
|
possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -32,23 +35,23 @@ class Flag:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.prefix: Literal["-", "--", "---"] = prefix
|
self.prefix: PREFIX_TYPE = prefix
|
||||||
self.possible_values: list[str] | Pattern[str] | PossibleValues = possible_values
|
self.possible_values: list[str] | Pattern[str] | PossibleValues = possible_values
|
||||||
|
|
||||||
def validate_input_flag_value(self, input_flag_value: str | None) -> bool:
|
def validate_input_flag_value(self, input_flag_value: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Private. Validates the input flag value
|
Private. Validates the input flag value
|
||||||
:param input_flag_value: The input flag value to validate
|
:param input_flag_value: The input flag value to validate
|
||||||
:return: whether the entered flag is valid as bool
|
:return: whether the entered flag is valid as bool
|
||||||
"""
|
"""
|
||||||
if self.possible_values == PossibleValues.NEITHER:
|
if self.possible_values == PossibleValues.NEITHER:
|
||||||
return input_flag_value is None
|
return input_flag_value == ''
|
||||||
|
|
||||||
if self.possible_values == PossibleValues.ALL:
|
if self.possible_values == PossibleValues.ALL:
|
||||||
return input_flag_value is not None
|
return input_flag_value != ''
|
||||||
|
|
||||||
if isinstance(self.possible_values, Pattern):
|
if isinstance(self.possible_values, Pattern):
|
||||||
return isinstance(input_flag_value, str) and bool(self.possible_values.match(input_flag_value))
|
return bool(self.possible_values.match(input_flag_value))
|
||||||
|
|
||||||
if isinstance(self.possible_values, list):
|
if isinstance(self.possible_values, list):
|
||||||
return input_flag_value in self.possible_values
|
return input_flag_value in self.possible_values
|
||||||
@@ -85,8 +88,8 @@ class InputFlag:
|
|||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
*,
|
*,
|
||||||
prefix: Literal["-", "--", "---"] = "--",
|
prefix: PREFIX_TYPE = "--",
|
||||||
input_value: str | None,
|
input_value: str,
|
||||||
status: ValidationStatus | None,
|
status: ValidationStatus | None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -97,8 +100,8 @@ class InputFlag:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.prefix: Literal["-", "--", "---"] = prefix
|
self.prefix: PREFIX_TYPE = prefix
|
||||||
self.input_value: str | None = input_value
|
self.input_value: str = input_value
|
||||||
self.status: ValidationStatus | None = status
|
self.status: ValidationStatus | None = status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
__all__ = ["Command", "InputCommand"]
|
__all__ = ["Command", "InputCommand"]
|
||||||
|
|
||||||
from typing import Literal, Never, Self, cast
|
import shlex
|
||||||
|
from typing import Never, Self, cast
|
||||||
|
from typing_extensions import Literal
|
||||||
|
|
||||||
from argenta.command.exceptions import (
|
from argenta.command.exceptions import (
|
||||||
EmptyInputCommandException,
|
EmptyInputCommandException,
|
||||||
@@ -14,6 +16,7 @@ ParseFlagsResult = tuple[InputFlags, str | None, str | None]
|
|||||||
ParseResult = tuple[str, InputFlags]
|
ParseResult = tuple[str, InputFlags]
|
||||||
|
|
||||||
MIN_FLAG_PREFIX: str = "-"
|
MIN_FLAG_PREFIX: str = "-"
|
||||||
|
PREFIX_TYPE = Literal["-", "--", "---"]
|
||||||
DEFAULT_WITHOUT_FLAGS: Flags = Flags()
|
DEFAULT_WITHOUT_FLAGS: Flags = Flags()
|
||||||
DEFAULT_WITHOUT_ALIASES: list[Never] = []
|
DEFAULT_WITHOUT_ALIASES: list[Never] = []
|
||||||
|
|
||||||
@@ -78,75 +81,50 @@ class InputCommand:
|
|||||||
:param raw_command: raw input command
|
:param raw_command: raw input command
|
||||||
:return: model of the input command, after parsing as InputCommand
|
:return: model of the input command, after parsing as InputCommand
|
||||||
"""
|
"""
|
||||||
trigger, input_flags = CommandParser(raw_command).parse_raw_command()
|
tokens = shlex.split(raw_command)
|
||||||
|
|
||||||
return cls(trigger=trigger, input_flags=input_flags)
|
if not tokens:
|
||||||
|
raise EmptyInputCommandException
|
||||||
|
|
||||||
|
command = tokens[0]
|
||||||
|
flags: InputFlags = InputFlags()
|
||||||
|
|
||||||
class CommandParser:
|
i = 1
|
||||||
def __init__(self, raw_command: str) -> None:
|
while i < len(tokens):
|
||||||
self.raw_command: str = raw_command
|
token = tokens[i]
|
||||||
self._parsed_input_flags: InputFlags = InputFlags()
|
|
||||||
|
|
||||||
def parse_raw_command(self) -> ParseResult:
|
if token.startswith("---"):
|
||||||
if not self.raw_command:
|
prefix = "---"
|
||||||
raise EmptyInputCommandException()
|
name = token[3:]
|
||||||
|
elif token.startswith("--"):
|
||||||
input_flags, crnt_flag_name, crnt_flag_val = self._parse_flags(self.raw_command.split()[1:])
|
prefix = "--"
|
||||||
|
name = token[2:]
|
||||||
if any([crnt_flag_name, crnt_flag_val]):
|
elif token.startswith("-"):
|
||||||
raise UnprocessedInputFlagException()
|
prefix = "-"
|
||||||
|
name = token[1:]
|
||||||
else:
|
else:
|
||||||
return (self.raw_command.split()[0], input_flags)
|
raise UnprocessedInputFlagException
|
||||||
|
|
||||||
def _parse_flags(self, _tokens: list[str] | list[Never]) -> ParseFlagsResult:
|
if not name:
|
||||||
crnt_flg_name, crnt_flg_val = None, None
|
raise UnprocessedInputFlagException
|
||||||
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):
|
if i + 1 < len(tokens) and not tokens[i + 1].startswith("-"):
|
||||||
continue
|
input_value = tokens[i + 1]
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
input_value = ""
|
||||||
|
i += 1
|
||||||
|
|
||||||
input_flag = InputFlag(
|
input_flag = InputFlag(
|
||||||
name=crnt_flg_name[crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1 :],
|
name=name,
|
||||||
prefix=cast(
|
prefix=cast(PREFIX_TYPE, prefix), # pyright: ignore[reportUnnecessaryCast]
|
||||||
Literal["-", "--", "---"],
|
input_value=input_value,
|
||||||
crnt_flg_name[: crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1],
|
status=None
|
||||||
),
|
|
||||||
input_value=crnt_flg_val,
|
|
||||||
status=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if input_flag in self._parsed_input_flags:
|
if input_flag in flags:
|
||||||
raise RepeatedInputFlagsException(input_flag)
|
raise RepeatedInputFlagsException(input_flag)
|
||||||
|
|
||||||
self._parsed_input_flags.add_flag(input_flag)
|
flags.add_flag(input_flag)
|
||||||
crnt_flg_name, crnt_flg_val = None, None
|
|
||||||
|
|
||||||
return (self._parsed_input_flags, crnt_flg_name, crnt_flg_val)
|
return cls(command, input_flags=flags)
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ HandlerFunc: TypeAlias = Callable[..., None]
|
|||||||
class Router:
|
class Router:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
|
||||||
title: str | None = "Default title",
|
title: str | None = "Default title",
|
||||||
|
*,
|
||||||
disable_redirect_stdout: bool = False,
|
disable_redirect_stdout: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from argenta.command.exceptions import (EmptyInputCommandException,
|
|||||||
UnprocessedInputFlagException)
|
UnprocessedInputFlagException)
|
||||||
from argenta.command.flag import Flag, InputFlag
|
from argenta.command.flag import Flag, InputFlag
|
||||||
from argenta.command.flag.flags import Flags
|
from argenta.command.flag.flags import Flags
|
||||||
from argenta.command.flag.models import PossibleValues
|
from argenta.command.flag.models import PossibleValues, ValidationStatus
|
||||||
from argenta.command.models import Command, InputCommand, ValidationStatus
|
from argenta.command.models import Command, InputCommand
|
||||||
|
|
||||||
|
|
||||||
class TestInputCommand(unittest.TestCase):
|
class TestInputCommand(unittest.TestCase):
|
||||||
@@ -28,7 +28,7 @@ class TestInputCommand(unittest.TestCase):
|
|||||||
|
|
||||||
def test_validate_invalid_input_flag1(self):
|
def test_validate_invalid_input_flag1(self):
|
||||||
command = Command('some', flags=Flag('test'))
|
command = Command('some', flags=Flag('test'))
|
||||||
self.assertEqual(command.validate_input_flag(InputFlag('test', input_value=None, status=None)), ValidationStatus.INVALID)
|
self.assertEqual(command.validate_input_flag(InputFlag('test', input_value='', status=None)), ValidationStatus.INVALID)
|
||||||
|
|
||||||
def test_validate_valid_input_flag2(self):
|
def test_validate_valid_input_flag2(self):
|
||||||
command = Command('some', flags=Flags([Flag('test'), Flag('more')]))
|
command = Command('some', flags=Flags([Flag('test'), Flag('more')]))
|
||||||
@@ -36,15 +36,15 @@ class TestInputCommand(unittest.TestCase):
|
|||||||
|
|
||||||
def test_validate_undefined_input_flag1(self):
|
def test_validate_undefined_input_flag1(self):
|
||||||
command = Command('some', flags=Flag('test'))
|
command = Command('some', flags=Flag('test'))
|
||||||
self.assertEqual(command.validate_input_flag(InputFlag('more', input_value=None, status=None)), ValidationStatus.UNDEFINED)
|
self.assertEqual(command.validate_input_flag(InputFlag('more', input_value='', status=None)), ValidationStatus.UNDEFINED)
|
||||||
|
|
||||||
def test_validate_undefined_input_flag2(self):
|
def test_validate_undefined_input_flag2(self):
|
||||||
command = Command('some', flags=Flags([Flag('test'), Flag('more')]))
|
command = Command('some', flags=Flags([Flag('test'), Flag('more')]))
|
||||||
self.assertEqual(command.validate_input_flag(InputFlag('case', input_value=None, status=None)), ValidationStatus.UNDEFINED)
|
self.assertEqual(command.validate_input_flag(InputFlag('case', input_value='', status=None)), ValidationStatus.UNDEFINED)
|
||||||
|
|
||||||
def test_validate_undefined_input_flag3(self):
|
def test_validate_undefined_input_flag3(self):
|
||||||
command = Command('some')
|
command = Command('some')
|
||||||
self.assertEqual(command.validate_input_flag(InputFlag('case', input_value=None, status=None)), ValidationStatus.UNDEFINED)
|
self.assertEqual(command.validate_input_flag(InputFlag('case', input_value='', status=None)), ValidationStatus.UNDEFINED)
|
||||||
|
|
||||||
def test_invalid_input_flag1(self):
|
def test_invalid_input_flag1(self):
|
||||||
command = Command('some', flags=Flag('test', possible_values=PossibleValues.NEITHER))
|
command = Command('some', flags=Flag('test', possible_values=PossibleValues.NEITHER))
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ class TestFlag(unittest.TestCase):
|
|||||||
'--')
|
'--')
|
||||||
|
|
||||||
def test_get_flag_value_without_set(self):
|
def test_get_flag_value_without_set(self):
|
||||||
self.assertEqual(InputFlag(name='test', input_value=None, status=None).input_value,
|
self.assertEqual(InputFlag(name='test', input_value='', status=None).input_value,
|
||||||
None)
|
'')
|
||||||
|
|
||||||
def test_get_flag_value_with_set(self):
|
def test_get_flag_value_with_set(self):
|
||||||
flag = InputFlag(name='test', input_value='example', status=None)
|
flag = InputFlag(name='test', input_value='example', status=None)
|
||||||
@@ -54,11 +54,11 @@ class TestFlag(unittest.TestCase):
|
|||||||
|
|
||||||
def test_validate_correct_empty_flag_value_without_possible_flag_values(self):
|
def test_validate_correct_empty_flag_value_without_possible_flag_values(self):
|
||||||
flag = Flag(name='test', possible_values=PossibleValues.NEITHER)
|
flag = Flag(name='test', possible_values=PossibleValues.NEITHER)
|
||||||
self.assertEqual(flag.validate_input_flag_value(None), True)
|
self.assertEqual(flag.validate_input_flag_value(''), True)
|
||||||
|
|
||||||
def test_validate_correct_empty_flag_value_with_possible_flag_values(self):
|
def test_validate_correct_empty_flag_value_with_possible_flag_values(self):
|
||||||
flag = Flag(name='test', possible_values=PossibleValues.NEITHER)
|
flag = Flag(name='test', possible_values=PossibleValues.NEITHER)
|
||||||
self.assertEqual(flag.validate_input_flag_value(None), True)
|
self.assertEqual(flag.validate_input_flag_value(''), True)
|
||||||
|
|
||||||
def test_validate_incorrect_random_flag_value_without_possible_flag_values(self):
|
def test_validate_incorrect_random_flag_value_without_possible_flag_values(self):
|
||||||
flag = Flag(name='test', possible_values=PossibleValues.NEITHER)
|
flag = Flag(name='test', possible_values=PossibleValues.NEITHER)
|
||||||
@@ -69,19 +69,19 @@ class TestFlag(unittest.TestCase):
|
|||||||
self.assertEqual(flag.validate_input_flag_value('random value'), True)
|
self.assertEqual(flag.validate_input_flag_value('random value'), True)
|
||||||
|
|
||||||
def test_get_input_flag1(self):
|
def test_get_input_flag1(self):
|
||||||
flag = InputFlag(name='test', input_value=None, status=None)
|
flag = InputFlag(name='test', input_value='', status=None)
|
||||||
input_flags = InputFlags([flag])
|
input_flags = InputFlags([flag])
|
||||||
self.assertEqual(input_flags.get_flag_by_name('test'), flag)
|
self.assertEqual(input_flags.get_flag_by_name('test'), flag)
|
||||||
|
|
||||||
def test_get_input_flag2(self):
|
def test_get_input_flag2(self):
|
||||||
flag = InputFlag(name='test', input_value=None, status=None)
|
flag = InputFlag(name='test', input_value='', status=None)
|
||||||
flag2 = InputFlag(name='some', input_value=None, status=None)
|
flag2 = InputFlag(name='some', input_value='', status=None)
|
||||||
input_flags = InputFlags([flag, flag2])
|
input_flags = InputFlags([flag, flag2])
|
||||||
self.assertEqual(input_flags.get_flag_by_name('some'), flag2)
|
self.assertEqual(input_flags.get_flag_by_name('some'), flag2)
|
||||||
|
|
||||||
def test_get_undefined_input_flag(self):
|
def test_get_undefined_input_flag(self):
|
||||||
flag = InputFlag(name='test', input_value=None, status=None)
|
flag = InputFlag(name='test', input_value='', status=None)
|
||||||
flag2 = InputFlag(name='some', input_value=None, status=None)
|
flag2 = InputFlag(name='some', input_value='', status=None)
|
||||||
input_flags = InputFlags([flag, flag2])
|
input_flags = InputFlags([flag, flag2])
|
||||||
self.assertEqual(input_flags.get_flag_by_name('case'), None)
|
self.assertEqual(input_flags.get_flag_by_name('case'), None)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user