This commit is contained in:
2025-11-21 19:41:35 +03:00
parent 8edd59c1b8
commit 2e76f68d4a
37 changed files with 395 additions and 482 deletions
@@ -0,0 +1,32 @@
import sys
from unittest.mock import patch
import pytest
from pytest import CaptureFixture
from argenta import App, Orchestrator, Router, Command, Response
@pytest.fixture(autouse=True)
def patched_argv():
with patch.object(sys, 'argv', ['program.py']):
yield
def test_input_incorrect_command(capsys: CaptureFixture[str]):
router = Router()
orchestrator = Orchestrator()
@router.command(Command('test'))
def test(response: Response) -> None:
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}')
)
with patch("builtins.input", side_effect=["help", "q"]):
orchestrator.start_polling(app)
output = capsys.readouterr().out
assert "\nUnknown command: help\n" in output
@@ -1,25 +1,21 @@
import io
import unittest
from contextlib import redirect_stdout
from argenta import App, Router, Command, Response
from argenta.command import InputCommand
class TestAppIntegration(unittest.TestCase):
def setUp(self) -> None:
self.app = App(override_system_messages=True, repeat_command_groups=False)
self.router = Router(title="App")
def test_simple_app() -> None:
app = App(override_system_messages=True, repeat_command_groups_printing=False)
router = Router(title="App")
@self.router.command(Command("HELP", description="Show help"))
def help_cmd(response: Response):
print("Available commands: HELP")
@router.command(Command("HELP", description="Show help"))
def help_cmd(response: Response):
print("Available commands: HELP")
_ = help_cmd # appease linter: function is registered via decorator
app.include_router(router)
self.app.include_router(self.router)
with redirect_stdout(io.StringIO()) as stdout:
router.finds_appropriate_handler(InputCommand.parse("HELP"))
def test_help_command(self):
with redirect_stdout(io.StringIO()) as stdout:
self.router.finds_appropriate_handler(InputCommand.parse("HELP"))
self.assertIn("Available commands:", stdout.getvalue())
assert "Available commands:" in stdout.getvalue()
@@ -1,59 +1,42 @@
import io
import unittest
from contextlib import redirect_stdout
from unittest.mock import Mock
from dishka import Provider, provide, make_container, Scope # pyright: ignore[reportUnknownVariableType]
from argenta.command import InputCommand
from dishka import Provider, make_container, Scope
from argenta import Router, Command
from argenta.di.integration import inject, setup_dishka, FromDishka
from argenta.response import Response
from argenta.response.status import ResponseStatus
from argenta import Router, Response
from argenta.di.integration import setup_dishka, FromDishka
class Service:
def hello(self) -> str:
return "world"
def get_service() -> Service:
return Service()
router = Router(title="DI")
@router.command(Command("HELLO"))
@inject # Auto-inject dependencies from the Response container
@router.command("HELLO")
def hello(response: Response, service: FromDishka[Service]) -> None:
print(f"hello {service.hello()}")
class TestDIHandler(unittest.TestCase):
def test_hello_uses_service(self):
# Prepare DI container with a stub
fake = Mock(spec=Service)
fake.hello.return_value = "mocked"
class TestProvider(Provider):
scope = Scope.APP
@provide(scope=Scope.APP)
def service(self) -> Service:
return fake
container = make_container(TestProvider())
# Bind container to Response via integration
setup_dishka(app=_FakeApp(), container=container, auto_inject=False) # type: ignore[arg-type]
# Create Response bound to the container
r = Response(ResponseStatus.ALL_FLAGS_VALID)
r._dishka_container = container # direct assignment is acceptable in tests # pyright: ignore[reportPrivateUsage]
# Call handler directly
with redirect_stdout(io.StringIO()) as stdout:
hello(r)
self.assertIn("hello mocked", stdout.getvalue())
class _FakeApp:
# Minimal stub for setup_dishka; app object is not used in unit tests
registered_routers = []
registered_routers = [router]
def test_hello_uses_service():
provider = Provider(scope=Scope.APP)
provider.provide(get_service)
container = make_container(provider)
setup_dishka(app=_FakeApp(), container=container, auto_inject=True)
# Call handler
with redirect_stdout(io.StringIO()) as stdout:
router.finds_appropriate_handler(InputCommand.parse('HELLO'))
assert "hello world" in stdout.getvalue()
@@ -1,5 +1,4 @@
import io
import unittest
from contextlib import redirect_stdout
from argenta import Router, Command, Response
@@ -8,15 +7,13 @@ from argenta.command import InputCommand
router = Router(title="Demo")
@router.command(Command("PING", description="Ping command"))
def ping(response: Response):
print("PONG")
class TestSimpleHandler(unittest.TestCase):
def test_ping_prints_pong(self):
# Имитация запуска хендлера через роутер
with redirect_stdout(io.StringIO()) as stdout:
router.finds_appropriate_handler(InputCommand.parse("PING"))
self.assertIn("PONG", stdout.getvalue())
def test_ping_prints_pong():
# Call handler
with redirect_stdout(io.StringIO()) as stdout:
router.finds_appropriate_handler(InputCommand.parse("PING"))
assert "PONG" in stdout.getvalue()
+1 -5
View File
@@ -12,7 +12,7 @@ default:
@{{sphinxbuild}} -M help "{{sourcedir}}" "{{builddir}}" {{sphinxopts}}
# Build all language versions
build-all:
build:
{{sphinxbuild}} -b html -D language=ru {{sourcedir}} {{builddir}}/html/ru
{{sphinxbuild}} -b html -D language =en {{sourcedir}} {{builddir}}/html/en
@@ -28,7 +28,3 @@ live-en:
update-langs:
{{sphinxbuild}} -b gettext . _build/gettext
sphinx-intl update -p _build/gettext -l en
# Generic build target (html, latex, etc.)
build target:
{{sphinxbuild}} -M {{target}} "{{sourcedir}}" "{{builddir}}" {{sphinxopts}}
@@ -75,7 +75,7 @@ msgstr ""
#: ../../root/api/app/index.rst:47
msgid ""
"``repeat_command_groups``: Если ``True``, список доступных команд "
"``repeat_command_groups_printing``: Если ``True``, список доступных команд "
"выводится перед каждым вводом."
msgstr ""
@@ -296,7 +296,7 @@ msgstr ""
#~ msgstr ""
#~ msgid ""
#~ "``repeat_command_groups``: Если **True** (по "
#~ "``repeat_command_groups_printing``: Если **True** (по "
#~ "умолчанию), описание доступных команд будет"
#~ " выводиться перед каждым вводом."
#~ msgstr ""
+9 -9
View File
@@ -30,7 +30,7 @@ App
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,
repeat_command_groups_printing: bool = True,
override_system_messages: bool = False,
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
print_func: Printer = DEFAULT_PRINT_FUNC) -> None
@@ -40,14 +40,14 @@ App
* ``prompt``: Приглашение к вводу, отображаемое перед каждой командой.
* ``initial_message``: Сообщение, выводимое при запуске приложения.
* ``farewell_message``: Сообщение, выводимое при выходе из приложения.
* ``exit_command``: Команда, используемая для выхода из приложения.
* ``exit_command``: Команда, которая маркируется как триггер для выхода из приложения.
* ``system_router_title``: Заголовок для системного роутера (содержит команду выхода).
* ``ignore_command_register``: Если ``True``, регистр команд игнорируется при поиске обработчика.
* ``dividing_line``: Стиль разделительной линии (``StaticDividingLine`` или ``DynamicDividingLine``).
* ``repeat_command_groups``: Если ``True``, список доступных команд выводится перед каждым вводом.
* ``ignore_command_register``: Если ``True``, регистр вводимых команд игнорируется при поиске обработчика.
* ``dividing_line``: Тип разделительной линии (``StaticDividingLine`` или ``DynamicDividingLine``).
* ``repeat_command_groups_printing``: Если ``True``, список доступных команд выводится перед каждым вводом.
* ``override_system_messages``: Если ``True``, стандартное форматирование (цвета, ASCII-арт) отключается.
* ``autocompleter``: Объект, отвечающий за автодополнение команд.
* ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.print``).
* ``autocompleter``: Экземпляр класса :ref:`AutoCompleter <root_api_app_autocompleter>`, отвечающий за автодополнение команд.
* ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.Console().print``).
-----
@@ -68,7 +68,7 @@ App
- .. py:method:: add_message_on_startup(self, message: str) -> None
Добавляет текстовое сообщение, которое выводится при запуске приложения после `initial_message`.
Добавляет текстовое сообщение, которое выводится при запуске приложения после ``initial_message``.
:param message: Строка с сообщением.
@@ -89,7 +89,7 @@ App
.. py:method:: set_description_message_pattern(self, handler: Callable[[str, str], str]) -> None
Устанавливает шаблон для форматирования строки описания команды.
Устанавливает шаблон для форматирования описания команды.
Обработчик принимает триггер команды (``str``) и её описание (``str``), а возвращает отформатированную строку.
+12 -14
View File
@@ -27,14 +27,12 @@
.. code-block:: python
from argenta import (
App, Orchestrator, Router, Command, Response
)
from argenta import App, Orchestrator, Router, Command, Response
* :ref:`App <root_api_app_index>`Основной класс приложения.
* :ref:`Orchestrator <root_api_orchestrator_index>` — Класс для управления жизненным циклом.
* :ref:`App <root_api_app_index>`Объект приложения, который отвечает за логику роутинга, настройки, валидации и т.д.
* :ref:`Orchestrator <root_api_orchestrator_index>` — Класс для конфигурирования и запуска всего приложения.
* :ref:`Router <root_api_router>` — Класс для группировки и регистрации команд.
* :ref:`Command <root_api_command_index>` — Класс для создания команд.
* :ref:`Command <root_api_command_index>` — Класс для создания команд при инициализации хэндлеров.
* :ref:`Response <root_api_response>` — Объект ответа, передаваемый в обработчики.
.. rubric:: Команды и флаги
@@ -55,9 +53,9 @@
* :ref:`Flags <root_api_command_flags>` — Коллекция для регистрации флагов.
* :ref:`InputFlag <root_api_command_input_flag>` — Класс для введённого пользователем флага.
* :ref:`InputFlags <root_api_command_input_flags>` — Коллекция введённых флагов.
* :ref:`PossibleValues <root_api_command_possible_values>` — Правила валидации значений флагов.
* :ref:`PossibleValues <root_api_command_possible_values>` — Правила валидации значений флага.
* :ref:`ValidationStatus <root_api_command_validation_status>` — Статусы валидации флагов.
* ``PredefinedFlags`` — Готовые наборы флагов (например, ``--help``).
* :ref:`PredefinedFlags <root_api_command_flag_predefined_flags>` — Коллекция предопределённых флагов.
.. rubric:: Настройка приложения
@@ -70,10 +68,10 @@
PredefinedMessages
)
* :ref:`AutoCompleter <root_api_app_autocompleter>` — Базовый класс для автодополнения.
* :ref:`StaticDividingLine <root_api_app_dividing_lines>` — Статическая разделительная линия.
* :ref:`DynamicDividingLine <root_api_app_dividing_lines>` — Динамическая разделительная линия.
* ``PredefinedMessages`` — Готовые системные сообщения.
* :ref:`AutoCompleter <root_api_app_autocompleter>` - Класс для настройки автодополнения.
* :ref:`StaticDividingLine <root_api_app_dividing_lines>` — Статическая разделительная линия для оформления вывода.
* :ref:`DynamicDividingLine <root_api_app_dividing_lines>` — Динамическая разделительная линия для оформления вывода.
* :ref:`PredefinedMessages <root_api_predefined_messages>` — Готовые сообщения для вывода при старте приложения.
.. rubric:: Внедрение зависимостей
@@ -84,8 +82,8 @@
inject
)
* :ref:`FromDishka <root_dependency_injection>` — Маркер для внедрения зависимостей.
* :ref:`inject <root_dependency_injection>` — Декоратор для асинхронного внедрения.
* :ref:`FromDishka <root_dependency_injection>` — Маркер аргумента функции как зависимости, которая должна быть инжектирована.
* :ref:`inject <root_dependency_injection>` — Декоратор для инжектирования зависимостей, указанных в сигнатуре.
.. toctree::
+1 -1
View File
@@ -6,7 +6,7 @@
Наше обязательство
------------------
В целях создания открытой и гостеприимной атмосферы мы, как участники и мейнтейнеры, обязуемся сделать участие в нашем проекте и сообществе свободным от преследований для всех, независимо от возраста, телосложения, инвалидности, этнической принадлежности, гендерной идентичности и самовыражения, уровня опыта, образования, социально-экономического статуса, национальности, внешности, расы, религии или сексуальной идентичности и ориентации.
В целях создания открытой и гостеприимной атмосферы мы, как участники и мейнтейнеры, обязуемся сделать участие в нашем проекте и сообществе свободным от преследований для всех, независимо от возраста, телосложения, инвалидности, этнической принадлежности, уровня опыта, образования, социально-экономического статуса, национальности, внешности, расы или религии.
-----
+16 -13
View File
@@ -55,7 +55,7 @@
Поищите ответ в существующих `Issues <https://github.com/koloideal/Argenta/issues>`_. Если вы нашли похожий вопрос, но всё ещё нуждаетесь в разъяснениях, можете написать в нём. Также рекомендуем поискать ответ в интернете.
Если ответа не нашлось, создайте новый `Issue <https://github.com/koloideal/Argenta/issues/new>`_ и предоставьте как можно больше контекста, включая версии проекта и платформы (CPython, pip и т.д.).
Если ответа не нашлось, создайте новый `Issue <https://github.com/koloideal/Argenta/issues/new>`_ и предоставьте как можно больше контекста, включая версии проекта и платформы.
Мы займемся вашей задачей как можно скорее.
@@ -87,10 +87,10 @@
* Также поищите в интернете (включая `Stack Overflow`), чтобы узнать, обсуждалась ли проблема за пределами `GitHub`.
* Соберите информацию об ошибке:
* Трассировка стека.
* ОС, платформа и версия (Windows, Linux, macOS, x86, ARM).
* Версия интерпретатора, компилятора, SDK, среды выполнения, менеджера пакетов и т.д.
* Входные данные и полученный результат.
* Можете ли вы надёжно воспроизвести проблему? Воспроизводится ли она на старых версиях?
* ОС, платформа и версия (Windows, Linux, macOS, x86, ARM).
* Версия интерпретатора, компилятора, SDK, среды выполнения, менеджера пакетов и т.д.
* Входные данные и полученный результат.
* Можете ли вы надёжно воспроизвести проблему? Воспроизводится ли она на старых версиях?
.. rubric:: Как мне отправить хороший отчет об ошибке?
@@ -187,7 +187,7 @@
python -m pytest tests
#. Сделайте коммит, следуя нашему руководству по стилю, и отправьте изменения в ваш форк.
#. Сделайте коммит, следуя :ref:`нашему руководству по стилю <styleguide>`, и отправьте изменения в ваш форк.
.. code-block:: bash
@@ -210,6 +210,10 @@
Мы поддерживаем документацию на двух языках: русском и английском.
.. important::
Для инкапсуляции различных команд, необходимых для настройки и запуска проекта мы используем ``just``, он же фигурирует в различных примерах в документации, поэтому рекомендуем вам `установить его <https://github.com/casey/just#installation>`_
Для улучшения документации вы можете следовать процессу, похожему на внесение вклада в код:
#. Убедитесь, что ваше окружение для разработки настроено, как описано в разделе `Ваш первый вклад в код`_.
@@ -220,33 +224,32 @@
cd docs
#. Внесите изменения в **русскую** версию документации (`docs/index.rst` и/или `docs/root/*`).
#. Чтобы собрать документацию локально и увидеть изменения, выполните:
#. Чтобы собрать документацию локально в режиме автоматического ребилда и увидеть изменения, выполните:
.. code-block:: bash
make live-ru
just live-ru
#. Откройте `127.0.0.1:8000` в браузере, чтобы просмотреть сгенерированную документацию.
#. После завершения работы над русской версией необходимо создать английский перевод:
.. code-block:: bash
make update-langs
just update-langs
#. После обновления шаблона обновите файлы перевода, расположенные в `docs/locales/en/LC_MESSAGES/`.
#. Когда изменения будут готовы, сделайте коммит и откройте `Pull Request`. Используйте префикс `docs:` в сообщении коммита.
-----
.. _Руководства по стилю:
.. _styleguide:
Руководства по стилю
--------------------
.. _Сообщения коммитов:
.. _commits_messages:
Сообщения коммитов
~~~~~~~~~~~~~~~~~~
**Сообщения коммитов**
Мы следуем спецификации `Conventional Commits <https://www.conventionalcommits.org/en/v1.0.0/>`_. Это делает историю проекта более читаемой и позволяет автоматически генерировать журнал изменений.
+19 -4
View File
@@ -1,19 +1,21 @@
Тестирование
============
В этом разделе описаны практики тестирования приложений на основе ``Argenta``. Примеры основаны на фактическом публичном API: ``App``, ``Router``, ``Command``, ``Orchestrator``, DI через ``dishka`` и интеграцию в ``argenta.di.integration``.
В этом разделе описаны практики тестирования приложений на основе ``Argenta``. Примеры основаны на фактическом публичном API.
Модульное тестирование хендлеров
--------------------------------
Обработчики в Argenta — обычные функции. Их удобно тестировать как чистые функции, не поднимая весь цикл приложения. Рекомендуются ``unittest`` или ``pytest``.
Пример с ``unittest`` для простого хендлера без DI:
Пример для простого хендлера без DI:
.. literalinclude:: ../code_snippets/testing/simple_handler_unittest.py
:language: python
:linenos:
-----
Тестирование с внедрением зависимостей (DI)
-------------------------------------------
@@ -23,6 +25,8 @@
:language: python
:linenos:
-----
Интеграционное тестирование приложения
--------------------------------------
@@ -32,10 +36,21 @@
:language: python
:linenos:
E2E-тестирование цикла (опционально)
-----
E2E-тестирование цикла
------------------------------------
Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк во ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — вынесите конфигурацию в функцию ``main()`` и запускайте модуль в подпроцессе с подготовленным вводом/выводом.
Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк в ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже.
.. danger ::
Обязательно передавайте строковый триггер команды выхода последним элементом в списке, который передаёте в контекстном менеджере при патче ``input`` как аргумент ``side_effects``, иначе тестируемое приложение будет ожидать ввода следующей команды и не сможет корректно завершиться.
.. literalinclude:: ../code_snippets/testing/app_e2e_test.py
:language: python
:linenos:
-----
Советы по тестированию
----------------------
+32
View File
@@ -0,0 +1,32 @@
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
set shell := ["bash", "-c"]
# Вывести список всех рецептов
default:
@just --list
# Запустить тесты через pytest
tests:
python -m pytest tests
# Запустить тесты с отчетом о покрытии
tests-cov:
python -m pytest --cov=argenta tests
# Отформатировать код (Ruff + isort)
format:
python -m ruff format ./src
python -m isort ./src
# Проверить типы через mypy (strict)
mypy:
python -m mypy -p argenta --strict
# Проверить стиль через wemake-python-styleguide
wps:
python -m flake8 --format=wemake ./src
# Запустить линтер Ruff
ruff:
python -m ruff check ./src
+4
View File
@@ -45,6 +45,7 @@ exclude = [
".__pycache__",
"tests"
]
line-length=110
[tool.pyright]
typeCheckingMode = "strict"
@@ -52,6 +53,9 @@ typeCheckingMode = "strict"
[tool.mypy]
disable_error_code = "import-untyped"
[tool.isort]
line_length=90
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
+1 -1
View File
@@ -1,6 +1,6 @@
from argenta.app.models import App as App
from argenta.command.models import Command as Command
from argenta.data_bridge.entity import DataBridge as DataBridge
from argenta.orchestrator.entity import Orchestrator as Orchestrator
from argenta.response.entity import Response as Response
from argenta.router.entity import Router as Router
from argenta.data_bridge.entity import DataBridge as DataBridge
+2 -4
View File
@@ -1,7 +1,5 @@
from argenta.app.autocompleter.entity import AutoCompleter as AutoCompleter
from argenta.app.defaults import PredefinedMessages as PredefinedMessages
from argenta.app.dividing_line.models import \
DynamicDividingLine as DynamicDividingLine
from argenta.app.dividing_line.models import \
StaticDividingLine as StaticDividingLine
from argenta.app.dividing_line.models import DynamicDividingLine as DynamicDividingLine
from argenta.app.dividing_line.models import StaticDividingLine as StaticDividingLine
from argenta.app.models import App as App
+5 -16
View File
@@ -6,10 +6,7 @@ from typing import Never
class AutoCompleter:
def __init__(
self, history_filename: str | None = None,
autocomplete_button: str = "tab"
) -> None:
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
@@ -26,18 +23,12 @@ class AutoCompleter:
: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)
)
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]
):
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:
@@ -92,12 +83,10 @@ def _is_command_exist(command: str, existing_commands: list[str], ignore_command
return command.lower() in existing_commands
return command in existing_commands
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)
]
return [readline.get_history_item(i) for i in range(1, readline.get_current_history_length() + 1)]
+1
View File
@@ -7,6 +7,7 @@ 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>"
+2 -4
View File
@@ -1,4 +1,2 @@
from argenta.app.dividing_line.models import \
DynamicDividingLine as DynamicDividingLine
from argenta.app.dividing_line.models import \
StaticDividingLine as StaticDividingLine
from argenta.app.dividing_line.models import DynamicDividingLine as DynamicDividingLine
from argenta.app.dividing_line.models import StaticDividingLine as StaticDividingLine
+51 -93
View File
@@ -5,22 +5,25 @@ import re
from contextlib import redirect_stdout
from typing import Never, TypeAlias
from art import \
text2art # pyright: ignore[reportMissingTypeStubs, reportUnknownVariableType]
from art import text2art
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.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.exceptions import (
EmptyInputCommandException,
InputCommandException,
RepeatedInputFlagsException,
UnprocessedInputFlagException,
)
from argenta.command.models import Command, InputCommand
from argenta.response import Response
from argenta.router import Router
@@ -40,7 +43,7 @@ class BaseApp:
system_router_title: str | None,
ignore_command_register: bool,
dividing_line: StaticDividingLine | DynamicDividingLine,
repeat_command_groups: bool,
repeat_command_groups_printing: bool,
override_system_messages: bool,
autocompleter: AutoCompleter,
print_func: Printer,
@@ -51,7 +54,7 @@ class BaseApp:
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._repeat_command_groups_printing_description: bool = repeat_command_groups_printing
self._override_system_messages: bool = override_system_messages
self._autocompleter: AutoCompleter = autocompleter
@@ -73,25 +76,21 @@ class BaseApp:
else self._matching_default_triggers_with_routers
)
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = (
lambda _: print_func(f"Incorrect flag syntax: {_}")
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._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._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._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = (
lambda _: print_func(f"Unknown command: {_.trigger}")
)
self._exit_command_handler: NonStandardBehaviorHandler[Response] = (
lambda _: print_func(self._farewell_message)
self._exit_command_handler: NonStandardBehaviorHandler[Response] = lambda _: print_func(
self._farewell_message
)
def set_description_message_pattern(
self, _: DescriptionMessageGenerator, /
) -> None:
def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None:
"""
Public. Sets the output pattern of the available commands
:param _: output pattern of the available commands
@@ -99,9 +98,7 @@ class BaseApp:
"""
self._description_message_gen = _
def set_incorrect_input_syntax_handler(
self, _: NonStandardBehaviorHandler[str], /
) -> None:
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
@@ -109,9 +106,7 @@ class BaseApp:
"""
self._incorrect_input_syntax_handler = _
def set_repeated_input_flags_handler(
self, _: NonStandardBehaviorHandler[str], /
) -> None:
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
@@ -119,9 +114,7 @@ class BaseApp:
"""
self._repeated_input_flags_handler = _
def set_unknown_command_handler(
self, _: NonStandardBehaviorHandler[InputCommand], /
) -> None:
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
@@ -137,9 +130,7 @@ class BaseApp:
"""
self._empty_input_command_handler = _
def set_exit_command_handler(
self, _: NonStandardBehaviorHandler[Response], /
) -> None:
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
@@ -175,11 +166,7 @@ class BaseApp:
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
max_length_line if 10 <= max_length_line <= 80 else 80 if max_length_line > 80 else 10
)
self._print_func(
@@ -196,15 +183,11 @@ class BaseApp:
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
)
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
)
self._dividing_line.get_full_static_line(is_override=self._override_system_messages)
)
else:
@@ -238,14 +221,10 @@ class BaseApp:
"""
input_command_trigger = command.trigger
if self._ignore_command_register:
if input_command_trigger.lower() in list(
self._current_matching_triggers_with_routers.keys()
):
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()
):
if input_command_trigger in list(self._current_matching_triggers_with_routers.keys()):
return False
return True
@@ -303,9 +282,7 @@ class BaseApp:
: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._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]
@@ -323,20 +300,14 @@ class BaseApp:
self._repeated_input_flags_handler = lambda raw_command: self._print_func(
f"[red bold]Repeated input flags: {escape(raw_command)}"
)
self._empty_input_command_handler = lambda: self._print_func(
"[red bold]Empty input command"
)
self._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]"
)
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 ""
("[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)
@@ -356,13 +327,9 @@ class BaseApp:
for trigger in combined:
self._matching_default_triggers_with_routers[trigger] = router_entity
self._matching_lower_triggers_with_routers[trigger.lower()] = (
router_entity
)
self._matching_lower_triggers_with_routers[trigger.lower()] = router_entity
self._autocompleter.initial_setup(
list(self._current_matching_triggers_with_routers.keys())
)
self._autocompleter.initial_setup(list(self._current_matching_triggers_with_routers.keys()))
seen = {}
for item in list(self._current_matching_triggers_with_routers.keys()):
@@ -382,7 +349,7 @@ class BaseApp:
self._print_func(message)
if self._messages_on_startup:
print("\n")
if not self._repeat_command_groups_description:
if not self._repeat_command_groups_printing_description:
self._print_command_group_description()
@@ -405,7 +372,7 @@ class App(BaseApp):
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,
repeat_command_groups_printing: bool = True,
override_system_messages: bool = False,
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
print_func: Printer = DEFAULT_PRINT_FUNC,
@@ -420,7 +387,7 @@ class App(BaseApp):
: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 repeat_command_groups_printing: 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
@@ -434,7 +401,7 @@ class App(BaseApp):
system_router_title=system_router_title,
ignore_command_register=ignore_command_register,
dividing_line=dividing_line,
repeat_command_groups=repeat_command_groups,
repeat_command_groups_printing=repeat_command_groups_printing,
override_system_messages=override_system_messages,
autocompleter=autocompleter,
print_func=print_func,
@@ -447,15 +414,13 @@ class App(BaseApp):
"""
self._pre_cycle_setup()
while True:
if self._repeat_command_groups_description:
if self._repeat_command_groups_printing_description:
self._print_command_group_description()
raw_command: str = Console().input(self._prompt)
try:
input_command: InputCommand = InputCommand.parse(
raw_command=raw_command
)
input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
except InputCommandException as error:
with redirect_stdout(io.StringIO()) as stderr:
self._error_handler(error, raw_command)
@@ -466,8 +431,7 @@ class App(BaseApp):
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()),
self._ignore_command_register
list(self._current_matching_triggers_with_routers.keys()), self._ignore_command_register
)
return
@@ -478,24 +442,18 @@ class App(BaseApp):
self._print_framed_text(stdout_res)
continue
processing_router = self._current_matching_triggers_with_routers[
input_command.trigger.lower()
]
processing_router = self._current_matching_triggers_with_routers[input_command.trigger.lower()]
if processing_router.disable_redirect_stdout:
dividing_line_unit_part: str = self._dividing_line.get_unit_part()
self._print_func(
StaticDividingLine(
dividing_line_unit_part
).get_full_static_line(
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(
StaticDividingLine(dividing_line_unit_part).get_full_static_line(
is_override=self._override_system_messages
)
)
+2 -1
View File
@@ -2,13 +2,14 @@ __all__ = ["NonStandardBehaviorHandler", "EmptyCommandHandler", "Printer", "Desc
from typing import Protocol, TypeVar
T = TypeVar('T', contravariant=True) # noqa: WPS111
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
+8 -8
View File
@@ -1,8 +1,8 @@
__all__ = [
"InputCommandException",
"UnprocessedInputFlagException",
"RepeatedInputFlagsException",
"EmptyInputCommandException",
"InputCommandException",
"UnprocessedInputFlagException",
"RepeatedInputFlagsException",
"EmptyInputCommandException",
]
from abc import ABC, abstractmethod
@@ -15,6 +15,7 @@ class InputCommandException(ABC, Exception):
"""
Private. Base exception class for all exceptions raised when parse input command
"""
@override
@abstractmethod
def __str__(self) -> str:
@@ -25,6 +26,7 @@ class UnprocessedInputFlagException(InputCommandException):
"""
Private. Raised when an unprocessed input flag is detected
"""
@override
def __str__(self) -> str:
return "Unprocessed Input Flags"
@@ -42,16 +44,14 @@ class RepeatedInputFlagsException(InputCommandException):
@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}'"
)
return f"Repeated Input Flags\nDuplicate 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"
+14 -16
View File
@@ -9,23 +9,21 @@ DEFAULT_PREFIX: Literal["-", "--", "---"] = "-"
class PredefinedFlags:
HELP = Flag(name="help", possible_values=PossibleValues.NEITHER)
SHORT_HELP = Flag(name="H", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
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)
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)
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}$"),
)
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}$"))
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}$"))
+18 -16
View File
@@ -6,19 +6,21 @@ from typing import Literal, override
class PossibleValues(Enum):
NEITHER = 'NEITHER'
ALL = 'ALL'
NEITHER = "NEITHER"
ALL = "ALL"
class ValidationStatus(Enum):
VALID = 'VALID'
INVALID = 'INVALID'
UNDEFINED = 'UNDEFINED'
VALID = "VALID"
INVALID = "INVALID"
UNDEFINED = "UNDEFINED"
class Flag:
def __init__(
self, name: str, *,
self,
name: str,
*,
prefix: Literal["-", "--", "---"] = "--",
possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
) -> None:
@@ -65,7 +67,7 @@ class Flag:
@override
def __repr__(self) -> str:
return f'Flag<name={self.name}, prefix={self.prefix}>'
return f"Flag<name={self.name}, prefix={self.prefix}>"
@override
def __eq__(self, other: object) -> bool:
@@ -77,10 +79,12 @@ class Flag:
class InputFlag:
def __init__(
self, name: str, *,
prefix: Literal['-', '--', '---'] = '--',
self,
name: str,
*,
prefix: Literal["-", "--", "---"] = "--",
input_value: str | None,
status: ValidationStatus | None
status: ValidationStatus | None,
):
"""
Public. The entity of the flag of the entered command
@@ -90,7 +94,7 @@ class InputFlag:
:return: None
"""
self.name: str = name
self.prefix: Literal['-', '--', '---'] = prefix
self.prefix: Literal["-", "--", "---"] = prefix
self.input_value: str | None = input_value
self.status: ValidationStatus | None = status
@@ -105,17 +109,15 @@ class InputFlag:
@override
def __str__(self) -> str:
return f'{self.string_entity} {self.input_value}'
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}>'
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
)
return self.name == other.name
else:
raise NotImplementedError
+20 -23
View File
@@ -1,13 +1,12 @@
__all__ = [
"Command",
"InputCommand"
]
__all__ = ["Command", "InputCommand"]
from typing import Literal, Never, Self, cast
from argenta.command.exceptions import (EmptyInputCommandException,
RepeatedInputFlagsException,
UnprocessedInputFlagException)
from argenta.command.exceptions import (
EmptyInputCommandException,
RepeatedInputFlagsException,
UnprocessedInputFlagException,
)
from argenta.command.flag.flags.models import Flags, InputFlags
from argenta.command.flag.models import Flag, InputFlag, ValidationStatus
@@ -23,7 +22,8 @@ DEFAULT_WITHOUT_INPUT_FLAGS: InputFlags = InputFlags()
class Command:
def __init__(
self,
trigger: str, *,
trigger: str,
*,
description: str | None = None,
flags: Flag | Flags = DEFAULT_WITHOUT_FLAGS,
aliases: list[str] | None = None,
@@ -40,9 +40,7 @@ class Command:
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:
def validate_input_flag(self, flag: InputFlag) -> ValidationStatus:
"""
Private. Validates the input flag
:param flag: input flag for validation
@@ -60,8 +58,7 @@ class Command:
class InputCommand:
def __init__(self, trigger: str, *,
input_flags: InputFlag | InputFlags = DEFAULT_WITHOUT_INPUT_FLAGS):
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
@@ -69,7 +66,9 @@ class InputCommand:
:return: None
"""
self.trigger: str = trigger
self.input_flags: InputFlags = input_flags if isinstance(input_flags, InputFlags) else InputFlags([input_flags])
self.input_flags: InputFlags = (
input_flags if isinstance(input_flags, InputFlags) else InputFlags([input_flags])
)
@classmethod
def parse(cls, raw_command: str) -> Self:
@@ -108,13 +107,13 @@ class CommandParser:
continue
input_flag = InputFlag(
name=crnt_flg_name[crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1:],
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],
crnt_flg_name[: crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1],
),
input_value=crnt_flg_val,
status=None
status=None,
)
if input_flag in self._parsed_input_flags:
@@ -125,8 +124,7 @@ class CommandParser:
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:
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
@@ -134,17 +132,16 @@ class CommandParser:
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
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)]
prefix = token[: token.rfind(MIN_FLAG_PREFIX)]
if len(token) < 2 or len(prefix) > 2:
raise UnprocessedInputFlagException
+3 -5
View File
@@ -5,8 +5,8 @@ from typing import Any, Callable, TypeVar
from dishka import Container, FromDishka
from dishka.integrations.base import is_dishka_injected, wrap_injection
from argenta.app import App
from argenta.response import Response
from argenta.app.models import App
from argenta.response.entity import Response
T = TypeVar("T")
@@ -25,9 +25,7 @@ def setup_dishka(app: App, container: Container, *, auto_inject: bool = False) -
Response.patch_by_container(container)
def _get_container_from_response(
args: tuple[Any, ...], kwargs: dict[str, Any]
) -> Container:
def _get_container_from_response(args: tuple[Any, ...], kwargs: dict[str, Any]) -> Container:
for arg in args:
if isinstance(arg, Response):
if hasattr(arg, "_dishka_container"):
+3 -7
View File
@@ -1,5 +1,5 @@
__all__ = [
'SystemProvider',
"SystemProvider",
]
from dishka import Provider, Scope, provide
@@ -10,13 +10,9 @@ from argenta.orchestrator.argparser.entity import ArgSpace
class SystemProvider(Provider):
def __init__(self, arg_parser: ArgParser):
super().__init__()
self._arg_parser: ArgParser = arg_parser
@provide(scope=Scope.APP)
def get_argspace(self) -> ArgSpace:
return self._arg_parser.parsed_argspace
def get_argspace(self, arg_parser: ArgParser) -> ArgSpace:
return arg_parser.parsed_argspace
@provide(scope=Scope.APP)
def get_data_bridge(self) -> DataBridge:
+1 -2
View File
@@ -1,2 +1 @@
from argenta.metrics.main import \
get_time_of_pre_cycle_setup as get_time_of_pre_cycle_setup
from argenta.metrics.main import get_time_of_pre_cycle_setup as get_time_of_pre_cycle_setup
+2 -2
View File
@@ -1,5 +1,5 @@
__all__ = [
'get_time_of_pre_cycle_setup',
"get_time_of_pre_cycle_setup",
]
import io
@@ -17,6 +17,6 @@ def get_time_of_pre_cycle_setup(app: App) -> float:
"""
start = time()
with redirect_stdout(io.StringIO()):
app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage]
app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage]
end = time()
return end - start
@@ -1,6 +1,4 @@
from argenta.orchestrator.argparser.arguments import \
BooleanArgument as BooleanArgument
from argenta.orchestrator.argparser.arguments import \
ValueArgument as ValueArgument
from argenta.orchestrator.argparser.arguments import BooleanArgument as BooleanArgument
from argenta.orchestrator.argparser.arguments import ValueArgument as ValueArgument
from argenta.orchestrator.argparser.entity import ArgParser as ArgParser
from argenta.orchestrator.argparser.entity import ArgSpace as ArgSpace
@@ -1,6 +1,3 @@
from argenta.orchestrator.argparser.arguments.models import \
BooleanArgument as BooleanArgument
from argenta.orchestrator.argparser.arguments.models import \
InputArgument as InputArgument
from argenta.orchestrator.argparser.arguments.models import \
ValueArgument as ValueArgument
from argenta.orchestrator.argparser.arguments.models import BooleanArgument as BooleanArgument
from argenta.orchestrator.argparser.arguments.models import InputArgument as InputArgument
from argenta.orchestrator.argparser.arguments.models import ValueArgument as ValueArgument
@@ -1,8 +1,4 @@
__all__ = [
'BooleanArgument',
'ValueArgument',
'InputArgument'
]
__all__ = ["BooleanArgument", "ValueArgument", "InputArgument"]
from typing import Literal
@@ -11,10 +7,8 @@ class BaseArgument:
"""
Private. Base class for all arguments
"""
def __init__(self, name: str, *,
help: str,
is_deprecated: bool,
prefix: Literal["-", "--", "---"]):
def __init__(self, name: str, *, help: str, is_deprecated: bool, prefix: Literal["-", "--", "---"]):
"""
Public. Boolean argument, does not require a value
:param name: name of the argument
@@ -33,13 +27,17 @@ class BaseArgument:
class ValueArgument(BaseArgument):
def __init__(self, name: str, *,
prefix: Literal["-", "--", "---"] = "--",
help: str = "Help message for the value argument",
possible_values: list[str] | None = None,
default: str | None = None,
is_required: bool = False,
is_deprecated: bool = False):
def __init__(
self,
name: str,
*,
prefix: Literal["-", "--", "---"] = "--",
help: str = "Help message for the value argument",
possible_values: list[str] | None = None,
default: str | None = None,
is_required: bool = False,
is_deprecated: bool = False,
):
"""
Public. Value argument, must have the value
:param name: name of the argument
@@ -58,10 +56,14 @@ class ValueArgument(BaseArgument):
class BooleanArgument(BaseArgument):
def __init__(self, name: str, *,
prefix: Literal["-", "--", "---"] = "--",
help: str = "Help message for the boolean argument",
is_deprecated: bool = False):
def __init__(
self,
name: str,
*,
prefix: Literal["-", "--", "---"] = "--",
help: str = "Help message for the boolean argument",
is_deprecated: bool = False,
):
"""
Public. Boolean argument, does not require a value
:param name: name of the argument
@@ -74,9 +76,7 @@ class BooleanArgument(BaseArgument):
class InputArgument:
def __init__(self, name: str,
value: str | Literal[True],
founder_class: type[BaseArgument]) -> None:
def __init__(self, name: str, value: str | Literal[True], founder_class: type[BaseArgument]) -> None:
self.name: str = name
self.value: str | Literal[True] = value
self.founder_class: type[BaseArgument] = founder_class
+33 -28
View File
@@ -6,10 +6,12 @@ __all__ = [
from argparse import ArgumentParser, Namespace
from typing import Never, Self
from argenta.orchestrator.argparser.arguments.models import (BaseArgument,
BooleanArgument,
InputArgument,
ValueArgument)
from argenta.orchestrator.argparser.arguments.models import (
BaseArgument,
BooleanArgument,
InputArgument,
ValueArgument,
)
class ArgSpace:
@@ -17,16 +19,16 @@ class ArgSpace:
self.all_arguments = all_arguments
@classmethod
def from_namespace(cls, namespace: Namespace,
processed_args: list[ValueArgument | BooleanArgument]) -> Self:
name_type_paired_args: dict[str, type[BaseArgument]] = {
arg.name: type(arg)
for arg in processed_args
}
return cls([InputArgument(name=name,
value=value,
founder_class=name_type_paired_args[name])
for name, value in vars(namespace).items()])
def from_namespace(
cls, namespace: Namespace, processed_args: list[ValueArgument | BooleanArgument]
) -> Self:
name_type_paired_args: dict[str, type[BaseArgument]] = {arg.name: type(arg) for arg in processed_args}
return cls(
[
InputArgument(name=name, value=value, founder_class=name_type_paired_args[name])
for name, value in vars(namespace).items()
]
)
def get_by_name(self, name: str) -> InputArgument | None:
for arg in self.all_arguments:
@@ -41,7 +43,8 @@ class ArgSpace:
class ArgParser:
def __init__(
self,
processed_args: list[ValueArgument | BooleanArgument], *,
processed_args: list[ValueArgument | BooleanArgument],
*,
name: str = "Argenta",
description: str = "Argenta available arguments",
epilog: str = "github.com/koloideal/Argenta | made by kolo",
@@ -64,21 +67,23 @@ class ArgParser:
self._register_args(processed_args)
def _parse_args(self) -> None:
self.parsed_argspace = ArgSpace.from_namespace(namespace=self._core.parse_args(),
processed_args=self.processed_args)
self.parsed_argspace = ArgSpace.from_namespace(
namespace=self._core.parse_args(), processed_args=self.processed_args
)
def _register_args(self, processed_args: list[ValueArgument | BooleanArgument]) -> None:
for arg in processed_args:
if isinstance(arg, BooleanArgument):
_ = self._core.add_argument(arg.string_entity,
action=arg.action,
help=arg.help,
deprecated=arg.is_deprecated)
_ = self._core.add_argument(
arg.string_entity, action=arg.action, help=arg.help, deprecated=arg.is_deprecated
)
else:
_ = self._core.add_argument(arg.string_entity,
action=arg.action,
help=arg.help,
default=arg.default,
choices=arg.possible_values,
required=arg.is_required,
deprecated=arg.is_deprecated)
_ = self._core.add_argument(
arg.string_entity,
action=arg.action,
help=arg.help,
default=arg.default,
choices=arg.possible_values,
required=arg.is_required,
deprecated=arg.is_deprecated,
)
+9 -4
View File
@@ -11,9 +11,12 @@ DEFAULT_ARGPARSER: ArgParser = ArgParser(processed_args=[])
class Orchestrator:
def __init__(self, arg_parser: ArgParser = DEFAULT_ARGPARSER,
custom_providers: list[Provider] = [],
auto_inject_handlers: bool = True):
def __init__(
self,
arg_parser: ArgParser = DEFAULT_ARGPARSER,
custom_providers: list[Provider] = [],
auto_inject_handlers: bool = True,
):
"""
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
@@ -31,7 +34,9 @@ class Orchestrator:
:param app: a running application
:return: None
"""
container = make_container(SystemProvider(self._arg_parser), *self._custom_providers)
container = make_container(
SystemProvider(), *self._custom_providers, context={ArgParser: self._arg_parser}
)
setup_dishka(app, container, auto_inject=self._auto_inject_handlers)
app.run_polling()
+4 -4
View File
@@ -10,12 +10,12 @@ class ResponseStatus(Enum):
UNDEFINED_AND_INVALID_FLAGS = "UNDEFINED_AND_INVALID_FLAGS"
@classmethod
def from_flags(cls, *, has_invalid_value_flags: bool, has_undefined_flags: bool) -> 'ResponseStatus':
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,
(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]
+1 -3
View File
@@ -32,9 +32,7 @@ class CommandHandlers:
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 []
)
self.command_handlers: list[CommandHandler] = command_handlers if command_handlers else []
def add_handler(self, command_handler: CommandHandler) -> None:
"""
+12 -22
View File
@@ -1,7 +1,6 @@
__all__ = ["Router"]
from inspect import (get_annotations, getfullargspec, getsourcefile,
getsourcelines)
from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines
from typing import Callable, TypeAlias
from rich.console import Console
@@ -10,11 +9,12 @@ from argenta.command import Command, InputCommand
from argenta.command.flag import ValidationStatus
from argenta.command.flag.flags import Flags, InputFlags
from argenta.response import Response, ResponseStatus
from argenta.router.command_handler.entity import (CommandHandler,
CommandHandlers)
from argenta.router.exceptions import (RepeatedFlagNameException,
RequiredArgumentNotPassedException,
TriggerContainSpacesException)
from argenta.router.command_handler.entity import CommandHandler, CommandHandlers
from argenta.router.exceptions import (
RepeatedFlagNameException,
RequiredArgumentNotPassedException,
TriggerContainSpacesException,
)
HandlerFunc: TypeAlias = Callable[..., None]
@@ -78,9 +78,7 @@ class Router:
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:
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
@@ -90,9 +88,7 @@ class Router:
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
)
response: Response = _structuring_input_flags(handle_command, input_command_flags)
command_handler.handling(response)
else:
response = Response(ResponseStatus.ALL_FLAGS_VALID)
@@ -103,9 +99,7 @@ class Router:
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
)
response = Response(ResponseStatus.UNDEFINED_FLAGS, input_flags=undefined_flags)
command_handler.handling(response)
else:
response = Response(ResponseStatus.ALL_FLAGS_VALID)
@@ -142,15 +136,11 @@ class CommandDecorator:
def __call__(self, handler_func: Callable[..., None]) -> Callable[..., None]:
_validate_func_args(handler_func)
self.router.command_handlers.add_handler(
CommandHandler(handler_func, self.command)
)
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:
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
-71
View File
@@ -1,71 +0,0 @@
import polib
import deepl
import getopt
import sys
import re
DEEPL_API_TOKEN = 'ADD YOUR API KEY HERE!'
global argv
global opts
global args
argv = sys.argv[1:]
opts, args = getopt.getopt(argv, "f:l:")
def translate(text, lang):
# Define a dictionary to hold the mappings of tokens to placeholders
placeholders = {}
# Use a regular expression to find all the tokens
tokens = re.findall(r'%\((.*?)\)s', text)
# Replace each token with a unique placeholder
for i, token in enumerate(tokens):
placeholder = f'__PLACEHOLDER_{i}__'
placeholders[placeholder] = f'%({token})s'
text = text.replace(f'%({token})s', placeholder)
# Perform the translation
translator = deepl.Translator(DEEPL_API_TOKEN)
translated_text = str(translator.translate_text(text, target_lang=lang))
# Replace the placeholders back with the original tokens
for placeholder, token in placeholders.items():
translated_text = translated_text.replace(placeholder, token)
return translated_text
def get_filename():
# read arguments from command line
for opt, arg in opts:
if opt in ['-f']:
filename = arg
if not filename:
print('Please enter the filename of the PO file e.g. /directory/django.po:')
filename = input()
return filename
def get_target_language():
# read arguments from command line
for opt, arg in opts:
if opt in ['-l']:
lang = arg
if not lang:
print('Please enter two letter ISO language code e.g. DE:')
lang = input()
return lang
def process_file(filename, lang):
po = polib.pofile(filename)
for entry in po.untranslated_entries():
if not entry.msgstr:
print(entry.msgid)
print('translating...')
entry.msgstr = translate(entry.msgid, lang)
print(entry.msgstr)
print('\n')
po.save(filename)
if __name__ == '__main__':
process_file(get_filename(), get_target_language())