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 io
import unittest
from contextlib import redirect_stdout from contextlib import redirect_stdout
from argenta import App, Router, Command, Response from argenta import App, Router, Command, Response
from argenta.command import InputCommand from argenta.command import InputCommand
class TestAppIntegration(unittest.TestCase): def test_simple_app() -> None:
def setUp(self) -> None: app = App(override_system_messages=True, repeat_command_groups_printing=False)
self.app = App(override_system_messages=True, repeat_command_groups=False) router = Router(title="App")
self.router = Router(title="App")
@self.router.command(Command("HELP", description="Show help")) @router.command(Command("HELP", description="Show help"))
def help_cmd(response: Response): def help_cmd(response: Response):
print("Available commands: HELP") print("Available commands: HELP")
_ = help_cmd # appease linter: function is registered via decorator app.include_router(router)
self.app.include_router(self.router)
def test_help_command(self):
with redirect_stdout(io.StringIO()) as stdout: with redirect_stdout(io.StringIO()) as stdout:
self.router.finds_appropriate_handler(InputCommand.parse("HELP")) 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 io
import unittest
from contextlib import redirect_stdout 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 import Router, Response
from argenta.di.integration import inject, setup_dishka, FromDishka from argenta.di.integration import setup_dishka, FromDishka
from argenta.response import Response
from argenta.response.status import ResponseStatus
class Service: class Service:
def hello(self) -> str: def hello(self) -> str:
return "world" return "world"
def get_service() -> Service:
return Service()
router = Router(title="DI") router = Router(title="DI")
@router.command("HELLO")
@router.command(Command("HELLO"))
@inject # Auto-inject dependencies from the Response container
def hello(response: Response, service: FromDishka[Service]) -> None: def hello(response: Response, service: FromDishka[Service]) -> None:
print(f"hello {service.hello()}") 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: class _FakeApp:
# Minimal stub for setup_dishka; app object is not used in unit tests # 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 io
import unittest
from contextlib import redirect_stdout from contextlib import redirect_stdout
from argenta import Router, Command, Response from argenta import Router, Command, Response
@@ -8,15 +7,13 @@ from argenta.command import InputCommand
router = Router(title="Demo") router = Router(title="Demo")
@router.command(Command("PING", description="Ping command")) @router.command(Command("PING", description="Ping command"))
def ping(response: Response): def ping(response: Response):
print("PONG") print("PONG")
class TestSimpleHandler(unittest.TestCase): def test_ping_prints_pong():
def test_ping_prints_pong(self): # Call handler
# Имитация запуска хендлера через роутер
with redirect_stdout(io.StringIO()) as stdout: with redirect_stdout(io.StringIO()) as stdout:
router.finds_appropriate_handler(InputCommand.parse("PING")) router.finds_appropriate_handler(InputCommand.parse("PING"))
self.assertIn("PONG", stdout.getvalue()) assert "PONG" in stdout.getvalue()
+1 -5
View File
@@ -12,7 +12,7 @@ default:
@{{sphinxbuild}} -M help "{{sourcedir}}" "{{builddir}}" {{sphinxopts}} @{{sphinxbuild}} -M help "{{sourcedir}}" "{{builddir}}" {{sphinxopts}}
# Build all language versions # Build all language versions
build-all: build:
{{sphinxbuild}} -b html -D language=ru {{sourcedir}} {{builddir}}/html/ru {{sphinxbuild}} -b html -D language=ru {{sourcedir}} {{builddir}}/html/ru
{{sphinxbuild}} -b html -D language =en {{sourcedir}} {{builddir}}/html/en {{sphinxbuild}} -b html -D language =en {{sourcedir}} {{builddir}}/html/en
@@ -28,7 +28,3 @@ live-en:
update-langs: update-langs:
{{sphinxbuild}} -b gettext . _build/gettext {{sphinxbuild}} -b gettext . _build/gettext
sphinx-intl update -p _build/gettext -l en 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 #: ../../root/api/app/index.rst:47
msgid "" msgid ""
"``repeat_command_groups``: Если ``True``, список доступных команд " "``repeat_command_groups_printing``: Если ``True``, список доступных команд "
"выводится перед каждым вводом." "выводится перед каждым вводом."
msgstr "" msgstr ""
@@ -296,7 +296,7 @@ msgstr ""
#~ msgstr "" #~ msgstr ""
#~ msgid "" #~ msgid ""
#~ "``repeat_command_groups``: Если **True** (по " #~ "``repeat_command_groups_printing``: Если **True** (по "
#~ "умолчанию), описание доступных команд будет" #~ "умолчанию), описание доступных команд будет"
#~ " выводиться перед каждым вводом." #~ " выводиться перед каждым вводом."
#~ msgstr "" #~ msgstr ""
+9 -9
View File
@@ -30,7 +30,7 @@ App
system_router_title: str | None = "System points:", system_router_title: str | None = "System points:",
ignore_command_register: bool = True, ignore_command_register: bool = True,
dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE, dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE,
repeat_command_groups: bool = True, repeat_command_groups_printing: bool = True,
override_system_messages: bool = False, override_system_messages: bool = False,
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER, autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
print_func: Printer = DEFAULT_PRINT_FUNC) -> None print_func: Printer = DEFAULT_PRINT_FUNC) -> None
@@ -40,14 +40,14 @@ App
* ``prompt``: Приглашение к вводу, отображаемое перед каждой командой. * ``prompt``: Приглашение к вводу, отображаемое перед каждой командой.
* ``initial_message``: Сообщение, выводимое при запуске приложения. * ``initial_message``: Сообщение, выводимое при запуске приложения.
* ``farewell_message``: Сообщение, выводимое при выходе из приложения. * ``farewell_message``: Сообщение, выводимое при выходе из приложения.
* ``exit_command``: Команда, используемая для выхода из приложения. * ``exit_command``: Команда, которая маркируется как триггер для выхода из приложения.
* ``system_router_title``: Заголовок для системного роутера (содержит команду выхода). * ``system_router_title``: Заголовок для системного роутера (содержит команду выхода).
* ``ignore_command_register``: Если ``True``, регистр команд игнорируется при поиске обработчика. * ``ignore_command_register``: Если ``True``, регистр вводимых команд игнорируется при поиске обработчика.
* ``dividing_line``: Стиль разделительной линии (``StaticDividingLine`` или ``DynamicDividingLine``). * ``dividing_line``: Тип разделительной линии (``StaticDividingLine`` или ``DynamicDividingLine``).
* ``repeat_command_groups``: Если ``True``, список доступных команд выводится перед каждым вводом. * ``repeat_command_groups_printing``: Если ``True``, список доступных команд выводится перед каждым вводом.
* ``override_system_messages``: Если ``True``, стандартное форматирование (цвета, ASCII-арт) отключается. * ``override_system_messages``: Если ``True``, стандартное форматирование (цвета, ASCII-арт) отключается.
* ``autocompleter``: Объект, отвечающий за автодополнение команд. * ``autocompleter``: Экземпляр класса :ref:`AutoCompleter <root_api_app_autocompleter>`, отвечающий за автодополнение команд.
* ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.print``). * ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.Console().print``).
----- -----
@@ -68,7 +68,7 @@ App
- .. py:method:: add_message_on_startup(self, message: str) -> None - .. py:method:: add_message_on_startup(self, message: str) -> None
Добавляет текстовое сообщение, которое выводится при запуске приложения после `initial_message`. Добавляет текстовое сообщение, которое выводится при запуске приложения после ``initial_message``.
:param message: Строка с сообщением. :param message: Строка с сообщением.
@@ -89,7 +89,7 @@ App
.. py:method:: set_description_message_pattern(self, handler: Callable[[str, str], str]) -> None .. py:method:: set_description_message_pattern(self, handler: Callable[[str, str], str]) -> None
Устанавливает шаблон для форматирования строки описания команды. Устанавливает шаблон для форматирования описания команды.
Обработчик принимает триггер команды (``str``) и её описание (``str``), а возвращает отформатированную строку. Обработчик принимает триггер команды (``str``) и её описание (``str``), а возвращает отформатированную строку.
+12 -14
View File
@@ -27,14 +27,12 @@
.. code-block:: python .. code-block:: python
from argenta import ( from argenta import App, Orchestrator, Router, Command, Response
App, Orchestrator, Router, Command, Response
)
* :ref:`App <root_api_app_index>`Основной класс приложения. * :ref:`App <root_api_app_index>`Объект приложения, который отвечает за логику роутинга, настройки, валидации и т.д.
* :ref:`Orchestrator <root_api_orchestrator_index>` — Класс для управления жизненным циклом. * :ref:`Orchestrator <root_api_orchestrator_index>` — Класс для конфигурирования и запуска всего приложения.
* :ref:`Router <root_api_router>` — Класс для группировки и регистрации команд. * :ref:`Router <root_api_router>` — Класс для группировки и регистрации команд.
* :ref:`Command <root_api_command_index>` — Класс для создания команд. * :ref:`Command <root_api_command_index>` — Класс для создания команд при инициализации хэндлеров.
* :ref:`Response <root_api_response>` — Объект ответа, передаваемый в обработчики. * :ref:`Response <root_api_response>` — Объект ответа, передаваемый в обработчики.
.. rubric:: Команды и флаги .. rubric:: Команды и флаги
@@ -55,9 +53,9 @@
* :ref:`Flags <root_api_command_flags>` — Коллекция для регистрации флагов. * :ref:`Flags <root_api_command_flags>` — Коллекция для регистрации флагов.
* :ref:`InputFlag <root_api_command_input_flag>` — Класс для введённого пользователем флага. * :ref:`InputFlag <root_api_command_input_flag>` — Класс для введённого пользователем флага.
* :ref:`InputFlags <root_api_command_input_flags>` — Коллекция введённых флагов. * :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>` — Статусы валидации флагов. * :ref:`ValidationStatus <root_api_command_validation_status>` — Статусы валидации флагов.
* ``PredefinedFlags`` — Готовые наборы флагов (например, ``--help``). * :ref:`PredefinedFlags <root_api_command_flag_predefined_flags>` — Коллекция предопределённых флагов.
.. rubric:: Настройка приложения .. rubric:: Настройка приложения
@@ -70,10 +68,10 @@
PredefinedMessages PredefinedMessages
) )
* :ref:`AutoCompleter <root_api_app_autocompleter>` — Базовый класс для автодополнения. * :ref:`AutoCompleter <root_api_app_autocompleter>` - Класс для настройки автодополнения.
* :ref:`StaticDividingLine <root_api_app_dividing_lines>` — Статическая разделительная линия. * :ref:`StaticDividingLine <root_api_app_dividing_lines>` — Статическая разделительная линия для оформления вывода.
* :ref:`DynamicDividingLine <root_api_app_dividing_lines>` — Динамическая разделительная линия. * :ref:`DynamicDividingLine <root_api_app_dividing_lines>` — Динамическая разделительная линия для оформления вывода.
* ``PredefinedMessages`` — Готовые системные сообщения. * :ref:`PredefinedMessages <root_api_predefined_messages>` — Готовые сообщения для вывода при старте приложения.
.. rubric:: Внедрение зависимостей .. rubric:: Внедрение зависимостей
@@ -84,8 +82,8 @@
inject inject
) )
* :ref:`FromDishka <root_dependency_injection>` — Маркер для внедрения зависимостей. * :ref:`FromDishka <root_dependency_injection>` — Маркер аргумента функции как зависимости, которая должна быть инжектирована.
* :ref:`inject <root_dependency_injection>` — Декоратор для асинхронного внедрения. * :ref:`inject <root_dependency_injection>` — Декоратор для инжектирования зависимостей, указанных в сигнатуре.
.. toctree:: .. toctree::
+1 -1
View File
@@ -6,7 +6,7 @@
Наше обязательство Наше обязательство
------------------ ------------------
В целях создания открытой и гостеприимной атмосферы мы, как участники и мейнтейнеры, обязуемся сделать участие в нашем проекте и сообществе свободным от преследований для всех, независимо от возраста, телосложения, инвалидности, этнической принадлежности, гендерной идентичности и самовыражения, уровня опыта, образования, социально-экономического статуса, национальности, внешности, расы, религии или сексуальной идентичности и ориентации. В целях создания открытой и гостеприимной атмосферы мы, как участники и мейнтейнеры, обязуемся сделать участие в нашем проекте и сообществе свободным от преследований для всех, независимо от возраста, телосложения, инвалидности, этнической принадлежности, уровня опыта, образования, социально-экономического статуса, национальности, внешности, расы или религии.
----- -----
+12 -9
View File
@@ -55,7 +55,7 @@
Поищите ответ в существующих `Issues <https://github.com/koloideal/Argenta/issues>`_. Если вы нашли похожий вопрос, но всё ещё нуждаетесь в разъяснениях, можете написать в нём. Также рекомендуем поискать ответ в интернете. Поищите ответ в существующих `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>`_ и предоставьте как можно больше контекста, включая версии проекта и платформы.
Мы займемся вашей задачей как можно скорее. Мы займемся вашей задачей как можно скорее.
@@ -187,7 +187,7 @@
python -m pytest tests python -m pytest tests
#. Сделайте коммит, следуя нашему руководству по стилю, и отправьте изменения в ваш форк. #. Сделайте коммит, следуя :ref:`нашему руководству по стилю <styleguide>`, и отправьте изменения в ваш форк.
.. code-block:: bash .. code-block:: bash
@@ -210,6 +210,10 @@
Мы поддерживаем документацию на двух языках: русском и английском. Мы поддерживаем документацию на двух языках: русском и английском.
.. important::
Для инкапсуляции различных команд, необходимых для настройки и запуска проекта мы используем ``just``, он же фигурирует в различных примерах в документации, поэтому рекомендуем вам `установить его <https://github.com/casey/just#installation>`_
Для улучшения документации вы можете следовать процессу, похожему на внесение вклада в код: Для улучшения документации вы можете следовать процессу, похожему на внесение вклада в код:
#. Убедитесь, что ваше окружение для разработки настроено, как описано в разделе `Ваш первый вклад в код`_. #. Убедитесь, что ваше окружение для разработки настроено, как описано в разделе `Ваш первый вклад в код`_.
@@ -220,33 +224,32 @@
cd docs cd docs
#. Внесите изменения в **русскую** версию документации (`docs/index.rst` и/или `docs/root/*`). #. Внесите изменения в **русскую** версию документации (`docs/index.rst` и/или `docs/root/*`).
#. Чтобы собрать документацию локально и увидеть изменения, выполните: #. Чтобы собрать документацию локально в режиме автоматического ребилда и увидеть изменения, выполните:
.. code-block:: bash .. code-block:: bash
make live-ru just live-ru
#. Откройте `127.0.0.1:8000` в браузере, чтобы просмотреть сгенерированную документацию. #. Откройте `127.0.0.1:8000` в браузере, чтобы просмотреть сгенерированную документацию.
#. После завершения работы над русской версией необходимо создать английский перевод: #. После завершения работы над русской версией необходимо создать английский перевод:
.. code-block:: bash .. code-block:: bash
make update-langs just update-langs
#. После обновления шаблона обновите файлы перевода, расположенные в `docs/locales/en/LC_MESSAGES/`. #. После обновления шаблона обновите файлы перевода, расположенные в `docs/locales/en/LC_MESSAGES/`.
#. Когда изменения будут готовы, сделайте коммит и откройте `Pull Request`. Используйте префикс `docs:` в сообщении коммита. #. Когда изменения будут готовы, сделайте коммит и откройте `Pull Request`. Используйте префикс `docs:` в сообщении коммита.
----- -----
.. _Руководства по стилю: .. _styleguide:
Руководства по стилю Руководства по стилю
-------------------- --------------------
.. _Сообщения коммитов: .. _commits_messages:
Сообщения коммитов **Сообщения коммитов**
~~~~~~~~~~~~~~~~~~
Мы следуем спецификации `Conventional Commits <https://www.conventionalcommits.org/en/v1.0.0/>`_. Это делает историю проекта более читаемой и позволяет автоматически генерировать журнал изменений. Мы следуем спецификации `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``. Обработчики в Argenta — обычные функции. Их удобно тестировать как чистые функции, не поднимая весь цикл приложения. Рекомендуются ``unittest`` или ``pytest``.
Пример с ``unittest`` для простого хендлера без DI: Пример для простого хендлера без DI:
.. literalinclude:: ../code_snippets/testing/simple_handler_unittest.py .. literalinclude:: ../code_snippets/testing/simple_handler_unittest.py
:language: python :language: python
:linenos: :linenos:
-----
Тестирование с внедрением зависимостей (DI) Тестирование с внедрением зависимостей (DI)
------------------------------------------- -------------------------------------------
@@ -23,6 +25,8 @@
:language: python :language: python
:linenos: :linenos:
-----
Интеграционное тестирование приложения Интеграционное тестирование приложения
-------------------------------------- --------------------------------------
@@ -32,10 +36,21 @@
:language: python :language: python
:linenos: :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__", ".__pycache__",
"tests" "tests"
] ]
line-length=110
[tool.pyright] [tool.pyright]
typeCheckingMode = "strict" typeCheckingMode = "strict"
@@ -52,6 +53,9 @@ typeCheckingMode = "strict"
[tool.mypy] [tool.mypy]
disable_error_code = "import-untyped" disable_error_code = "import-untyped"
[tool.isort]
line_length=90
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
+1 -1
View File
@@ -1,6 +1,6 @@
from argenta.app.models import App as App from argenta.app.models import App as App
from argenta.command.models import Command as Command 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.orchestrator.entity import Orchestrator as Orchestrator
from argenta.response.entity import Response as Response from argenta.response.entity import Response as Response
from argenta.router.entity import Router as Router 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.autocompleter.entity import AutoCompleter as AutoCompleter
from argenta.app.defaults import PredefinedMessages as PredefinedMessages from argenta.app.defaults import PredefinedMessages as PredefinedMessages
from argenta.app.dividing_line.models import \ from argenta.app.dividing_line.models import DynamicDividingLine as DynamicDividingLine
DynamicDividingLine as DynamicDividingLine from argenta.app.dividing_line.models import StaticDividingLine as StaticDividingLine
from argenta.app.dividing_line.models import \
StaticDividingLine as StaticDividingLine
from argenta.app.models import App as App from argenta.app.models import App as App
+5 -16
View File
@@ -6,10 +6,7 @@ from typing import Never
class AutoCompleter: class AutoCompleter:
def __init__( def __init__(self, history_filename: str | None = None, autocomplete_button: str = "tab") -> None:
self, history_filename: str | None = None,
autocomplete_button: str = "tab"
) -> None:
""" """
Public. Configures and implements auto-completion of input command Public. Configures and implements auto-completion of input command
:param history_filename: the name of the file for saving the history of the autocompleter :param 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 :param state: the current cursor position is relative to the beginning of the line
:return: the desired candidate as str or None :return: the desired candidate as str or None
""" """
matches: list[str] = sorted( matches: list[str] = sorted(cmd for cmd in _get_history_items() if cmd.startswith(text))
cmd for cmd in _get_history_items() if cmd.startswith(text)
)
if len(matches) > 1: if len(matches) > 1:
common_prefix = matches[0] common_prefix = matches[0]
for match in matches[1:]: for match in matches[1:]:
i = 0 i = 0
while ( while i < len(common_prefix) and i < len(match) and common_prefix[i] == match[i]:
i < len(common_prefix)
and i < len(match)
and common_prefix[i] == match[i]
):
i += 1 i += 1
common_prefix = common_prefix[:i] common_prefix = common_prefix[:i]
if state == 0: 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.lower() in existing_commands
return command in existing_commands return command in existing_commands
def _get_history_items() -> list[str] | list[Never]: def _get_history_items() -> list[str] | list[Never]:
""" """
Private. Returns a list of all commands entered by the user Private. Returns a list of all commands entered by the user
:return: all commands entered by the user as list[str] | list[Never] :return: all commands entered by the user as list[str] | list[Never]
""" """
return [ return [readline.get_history_item(i) for i in range(1, readline.get_current_history_length() + 1)]
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 Public. A dataclass with predetermined messages for quick use
""" """
USAGE = "[b dim]Usage[/b dim]: [i]<command> <[green]flags[/green]>[/i]" USAGE = "[b dim]Usage[/b dim]: [i]<command> <[green]flags[/green]>[/i]"
HELP = "[b dim]Help[/b dim]: [i]<command>[/i] [b red]--help[/b red]" HELP = "[b dim]Help[/b dim]: [i]<command>[/i] [b red]--help[/b red]"
AUTOCOMPLETE = "[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>" AUTOCOMPLETE = "[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>"
+2 -4
View File
@@ -1,4 +1,2 @@
from argenta.app.dividing_line.models import \ from argenta.app.dividing_line.models import DynamicDividingLine as DynamicDividingLine
DynamicDividingLine as DynamicDividingLine from argenta.app.dividing_line.models import StaticDividingLine as StaticDividingLine
from argenta.app.dividing_line.models import \
StaticDividingLine as StaticDividingLine
+48 -90
View File
@@ -5,22 +5,25 @@ import re
from contextlib import redirect_stdout from contextlib import redirect_stdout
from typing import Never, TypeAlias from typing import Never, TypeAlias
from art import \ from art import text2art
text2art # pyright: ignore[reportMissingTypeStubs, reportUnknownVariableType]
from rich.console import Console from rich.console import Console
from rich.markup import escape from rich.markup import escape
from argenta.app.autocompleter import AutoCompleter from argenta.app.autocompleter import AutoCompleter
from argenta.app.dividing_line.models import (DynamicDividingLine, from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine
StaticDividingLine) from argenta.app.protocols import (
from argenta.app.protocols import (DescriptionMessageGenerator, DescriptionMessageGenerator,
EmptyCommandHandler, EmptyCommandHandler,
NonStandardBehaviorHandler, Printer) NonStandardBehaviorHandler,
Printer,
)
from argenta.app.registered_routers.entity import RegisteredRouters from argenta.app.registered_routers.entity import RegisteredRouters
from argenta.command.exceptions import (EmptyInputCommandException, from argenta.command.exceptions import (
EmptyInputCommandException,
InputCommandException, InputCommandException,
RepeatedInputFlagsException, RepeatedInputFlagsException,
UnprocessedInputFlagException) UnprocessedInputFlagException,
)
from argenta.command.models import Command, InputCommand from argenta.command.models import Command, InputCommand
from argenta.response import Response from argenta.response import Response
from argenta.router import Router from argenta.router import Router
@@ -40,7 +43,7 @@ class BaseApp:
system_router_title: str | None, system_router_title: str | None,
ignore_command_register: bool, ignore_command_register: bool,
dividing_line: StaticDividingLine | DynamicDividingLine, dividing_line: StaticDividingLine | DynamicDividingLine,
repeat_command_groups: bool, repeat_command_groups_printing: bool,
override_system_messages: bool, override_system_messages: bool,
autocompleter: AutoCompleter, autocompleter: AutoCompleter,
print_func: Printer, print_func: Printer,
@@ -51,7 +54,7 @@ class BaseApp:
self._system_router_title: str | None = system_router_title self._system_router_title: str | None = system_router_title
self._dividing_line: StaticDividingLine | DynamicDividingLine = dividing_line self._dividing_line: StaticDividingLine | DynamicDividingLine = dividing_line
self._ignore_command_register: bool = ignore_command_register 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._override_system_messages: bool = override_system_messages
self._autocompleter: AutoCompleter = autocompleter self._autocompleter: AutoCompleter = autocompleter
@@ -73,25 +76,21 @@ class BaseApp:
else self._matching_default_triggers_with_routers else self._matching_default_triggers_with_routers
) )
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = ( self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = lambda _: print_func(
lambda _: print_func(f"Incorrect flag syntax: {_}") f"Incorrect flag syntax: {_}"
) )
self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = ( self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = lambda _: print_func(
lambda _: print_func(f"Repeated input flags: {_}") f"Repeated input flags: {_}"
) )
self._empty_input_command_handler: EmptyCommandHandler = lambda: print_func( self._empty_input_command_handler: EmptyCommandHandler = lambda: print_func("Empty input command")
"Empty input command" self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = lambda _: print_func(
f"Unknown command: {_.trigger}"
) )
self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = ( self._exit_command_handler: NonStandardBehaviorHandler[Response] = lambda _: print_func(
lambda _: print_func(f"Unknown command: {_.trigger}") self._farewell_message
)
self._exit_command_handler: NonStandardBehaviorHandler[Response] = (
lambda _: print_func(self._farewell_message)
) )
def set_description_message_pattern( def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None:
self, _: DescriptionMessageGenerator, /
) -> None:
""" """
Public. Sets the output pattern of the available commands Public. Sets the output pattern of the available commands
:param _: output pattern of the available commands :param _: output pattern of the available commands
@@ -99,9 +98,7 @@ class BaseApp:
""" """
self._description_message_gen = _ self._description_message_gen = _
def set_incorrect_input_syntax_handler( def set_incorrect_input_syntax_handler(self, _: NonStandardBehaviorHandler[str], /) -> None:
self, _: NonStandardBehaviorHandler[str], /
) -> None:
""" """
Public. Sets the handler for incorrect flags when entering a command Public. Sets the handler for incorrect flags when entering a command
:param _: 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 = _ self._incorrect_input_syntax_handler = _
def set_repeated_input_flags_handler( def set_repeated_input_flags_handler(self, _: NonStandardBehaviorHandler[str], /) -> None:
self, _: NonStandardBehaviorHandler[str], /
) -> None:
""" """
Public. Sets the handler for repeated flags when entering a command Public. Sets the handler for repeated flags when entering a command
:param _: 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 = _ self._repeated_input_flags_handler = _
def set_unknown_command_handler( def set_unknown_command_handler(self, _: NonStandardBehaviorHandler[InputCommand], /) -> None:
self, _: NonStandardBehaviorHandler[InputCommand], /
) -> None:
""" """
Public. Sets the handler for unknown commands when entering a command Public. Sets the handler for unknown commands when entering a command
:param _: 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 = _ self._empty_input_command_handler = _
def set_exit_command_handler( def set_exit_command_handler(self, _: NonStandardBehaviorHandler[Response], /) -> None:
self, _: NonStandardBehaviorHandler[Response], /
) -> None:
""" """
Public. Sets the handler for exit command when entering a command Public. Sets the handler for exit command when entering a command
:param _: 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) 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([len(line) for line in clear_text.split("\n")])
max_length_line = ( max_length_line = (
max_length_line max_length_line if 10 <= max_length_line <= 80 else 80 if max_length_line > 80 else 10
if 10 <= max_length_line <= 80
else 80
if max_length_line > 80
else 10
) )
self._print_func( self._print_func(
@@ -196,15 +183,11 @@ class BaseApp:
elif isinstance(self._dividing_line, StaticDividingLine): # pyright: ignore[reportUnnecessaryIsInstance] elif isinstance(self._dividing_line, StaticDividingLine): # pyright: ignore[reportUnnecessaryIsInstance]
self._print_func( self._print_func(
self._dividing_line.get_full_static_line( self._dividing_line.get_full_static_line(is_override=self._override_system_messages)
is_override=self._override_system_messages
)
) )
print(text.strip("\n")) print(text.strip("\n"))
self._print_func( self._print_func(
self._dividing_line.get_full_static_line( self._dividing_line.get_full_static_line(is_override=self._override_system_messages)
is_override=self._override_system_messages
)
) )
else: else:
@@ -238,14 +221,10 @@ class BaseApp:
""" """
input_command_trigger = command.trigger input_command_trigger = command.trigger
if self._ignore_command_register: if self._ignore_command_register:
if input_command_trigger.lower() in list( if input_command_trigger.lower() in list(self._current_matching_triggers_with_routers.keys()):
self._current_matching_triggers_with_routers.keys()
):
return False return False
else: else:
if input_command_trigger in list( if input_command_trigger in list(self._current_matching_triggers_with_routers.keys()):
self._current_matching_triggers_with_routers.keys()
):
return False return False
return True return True
@@ -303,9 +282,7 @@ class BaseApp:
:return: None :return: None
""" """
self._prompt = f"[italic dim bold]{self._prompt}" self._prompt = f"[italic dim bold]{self._prompt}"
self._initial_message = ( self._initial_message = "\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n"
"\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n"
)
self._farewell_message = ( self._farewell_message = (
"[bold red]\n\n" "[bold red]\n\n"
+ str(text2art(self._farewell_message, font="chanky")) # pyright: ignore[reportUnknownArgumentType] + 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( self._repeated_input_flags_handler = lambda raw_command: self._print_func(
f"[red bold]Repeated input flags: {escape(raw_command)}" f"[red bold]Repeated input flags: {escape(raw_command)}"
) )
self._empty_input_command_handler = lambda: self._print_func( self._empty_input_command_handler = lambda: self._print_func("[red bold]Empty input command")
"[red bold]Empty input command"
)
def unknown_command_handler(command: InputCommand) -> None: def unknown_command_handler(command: InputCommand) -> None:
cmd_trg: str = command.trigger cmd_trg: str = command.trigger
mst_sim_cmd: str | None = self._most_similar_command(cmd_trg) mst_sim_cmd: str | None = self._most_similar_command(cmd_trg)
first_part_of_text = ( first_part_of_text = f"[red]Unknown command:[/red] [blue]{escape(cmd_trg)}[/blue]"
f"[red]Unknown command:[/red] [blue]{escape(cmd_trg)}[/blue]"
)
second_part_of_text = ( second_part_of_text = (
("[red], most similar:[/red] " + ("[blue]" + mst_sim_cmd + "[/blue]")) ("[red], most similar:[/red] " + ("[blue]" + mst_sim_cmd + "[/blue]")) if mst_sim_cmd else ""
if mst_sim_cmd
else ""
) )
self._print_func(first_part_of_text + second_part_of_text) self._print_func(first_part_of_text + second_part_of_text)
@@ -356,13 +327,9 @@ class BaseApp:
for trigger in combined: for trigger in combined:
self._matching_default_triggers_with_routers[trigger] = router_entity self._matching_default_triggers_with_routers[trigger] = router_entity
self._matching_lower_triggers_with_routers[trigger.lower()] = ( self._matching_lower_triggers_with_routers[trigger.lower()] = router_entity
router_entity
)
self._autocompleter.initial_setup( self._autocompleter.initial_setup(list(self._current_matching_triggers_with_routers.keys()))
list(self._current_matching_triggers_with_routers.keys())
)
seen = {} seen = {}
for item in list(self._current_matching_triggers_with_routers.keys()): for item in list(self._current_matching_triggers_with_routers.keys()):
@@ -382,7 +349,7 @@ class BaseApp:
self._print_func(message) self._print_func(message)
if self._messages_on_startup: if self._messages_on_startup:
print("\n") print("\n")
if not self._repeat_command_groups_description: if not self._repeat_command_groups_printing_description:
self._print_command_group_description() self._print_command_group_description()
@@ -405,7 +372,7 @@ class App(BaseApp):
system_router_title: str | None = "System points:", system_router_title: str | None = "System points:",
ignore_command_register: bool = True, ignore_command_register: bool = True,
dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE, dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE,
repeat_command_groups: bool = True, repeat_command_groups_printing: bool = True,
override_system_messages: bool = False, override_system_messages: bool = False,
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER, autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
print_func: Printer = DEFAULT_PRINT_FUNC, print_func: Printer = DEFAULT_PRINT_FUNC,
@@ -420,7 +387,7 @@ class App(BaseApp):
:param system_router_title: system router title :param system_router_title: system router title
:param ignore_command_register: whether to ignore the case of the entered commands :param ignore_command_register: whether to ignore the case of the entered commands
:param dividing_line: the entity of the dividing line :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 override_system_messages: whether to redefine the default formatting of system messages
:param autocompleter: the entity of the autocompleter :param autocompleter: the entity of the autocompleter
:param print_func: system messages text output function :param print_func: system messages text output function
@@ -434,7 +401,7 @@ class App(BaseApp):
system_router_title=system_router_title, system_router_title=system_router_title,
ignore_command_register=ignore_command_register, ignore_command_register=ignore_command_register,
dividing_line=dividing_line, dividing_line=dividing_line,
repeat_command_groups=repeat_command_groups, repeat_command_groups_printing=repeat_command_groups_printing,
override_system_messages=override_system_messages, override_system_messages=override_system_messages,
autocompleter=autocompleter, autocompleter=autocompleter,
print_func=print_func, print_func=print_func,
@@ -447,15 +414,13 @@ class App(BaseApp):
""" """
self._pre_cycle_setup() self._pre_cycle_setup()
while True: while True:
if self._repeat_command_groups_description: if self._repeat_command_groups_printing_description:
self._print_command_group_description() self._print_command_group_description()
raw_command: str = Console().input(self._prompt) raw_command: str = Console().input(self._prompt)
try: try:
input_command: InputCommand = InputCommand.parse( input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
raw_command=raw_command
)
except InputCommandException as error: except InputCommandException as error:
with redirect_stdout(io.StringIO()) as stderr: with redirect_stdout(io.StringIO()) as stderr:
self._error_handler(error, raw_command) self._error_handler(error, raw_command)
@@ -466,8 +431,7 @@ class App(BaseApp):
if self._is_exit_command(input_command): if self._is_exit_command(input_command):
system_router.finds_appropriate_handler(input_command) system_router.finds_appropriate_handler(input_command)
self._autocompleter.exit_setup( self._autocompleter.exit_setup(
list(self._current_matching_triggers_with_routers.keys()), list(self._current_matching_triggers_with_routers.keys()), self._ignore_command_register
self._ignore_command_register
) )
return return
@@ -478,24 +442,18 @@ class App(BaseApp):
self._print_framed_text(stdout_res) self._print_framed_text(stdout_res)
continue continue
processing_router = self._current_matching_triggers_with_routers[ processing_router = self._current_matching_triggers_with_routers[input_command.trigger.lower()]
input_command.trigger.lower()
]
if processing_router.disable_redirect_stdout: if processing_router.disable_redirect_stdout:
dividing_line_unit_part: str = self._dividing_line.get_unit_part() dividing_line_unit_part: str = self._dividing_line.get_unit_part()
self._print_func( self._print_func(
StaticDividingLine( StaticDividingLine(dividing_line_unit_part).get_full_static_line(
dividing_line_unit_part
).get_full_static_line(
is_override=self._override_system_messages is_override=self._override_system_messages
) )
) )
processing_router.finds_appropriate_handler(input_command) processing_router.finds_appropriate_handler(input_command)
self._print_func( self._print_func(
StaticDividingLine( StaticDividingLine(dividing_line_unit_part).get_full_static_line(
dividing_line_unit_part
).get_full_static_line(
is_override=self._override_system_messages is_override=self._override_system_messages
) )
) )
+2 -1
View File
@@ -2,13 +2,14 @@ __all__ = ["NonStandardBehaviorHandler", "EmptyCommandHandler", "Printer", "Desc
from typing import Protocol, TypeVar from typing import Protocol, TypeVar
T = TypeVar('T', contravariant=True) # noqa: WPS111 T = TypeVar("T", contravariant=True) # noqa: WPS111
class NonStandardBehaviorHandler(Protocol[T]): class NonStandardBehaviorHandler(Protocol[T]):
def __call__(self, __param: T) -> None: def __call__(self, __param: T) -> None:
raise NotImplementedError raise NotImplementedError
class EmptyCommandHandler(Protocol): class EmptyCommandHandler(Protocol):
def __call__(self) -> None: def __call__(self) -> None:
raise NotImplementedError raise NotImplementedError
+4 -4
View File
@@ -15,6 +15,7 @@ class InputCommandException(ABC, Exception):
""" """
Private. Base exception class for all exceptions raised when parse input command Private. Base exception class for all exceptions raised when parse input command
""" """
@override @override
@abstractmethod @abstractmethod
def __str__(self) -> str: def __str__(self) -> str:
@@ -25,6 +26,7 @@ class UnprocessedInputFlagException(InputCommandException):
""" """
Private. Raised when an unprocessed input flag is detected Private. Raised when an unprocessed input flag is detected
""" """
@override @override
def __str__(self) -> str: def __str__(self) -> str:
return "Unprocessed Input Flags" return "Unprocessed Input Flags"
@@ -42,16 +44,14 @@ class RepeatedInputFlagsException(InputCommandException):
@override @override
def __str__(self) -> str: def __str__(self) -> str:
string_entity: str = self.flag.string_entity string_entity: str = self.flag.string_entity
return ( return f"Repeated Input Flags\nDuplicate flag was detected in the input: '{string_entity}'"
"Repeated Input Flags\n"
f"Duplicate flag was detected in the input: '{string_entity}'"
)
class EmptyInputCommandException(InputCommandException): class EmptyInputCommandException(InputCommandException):
""" """
Private. Raised when an empty input command is detected Private. Raised when an empty input command is detected
""" """
@override @override
def __str__(self) -> str: def __str__(self) -> str:
return "Input Command is empty" return "Input Command is empty"
+1 -3
View File
@@ -18,9 +18,7 @@ class PredefinedFlags:
ALL = Flag(name="all", possible_values=PossibleValues.NEITHER) ALL = Flag(name="all", possible_values=PossibleValues.NEITHER)
SHORT_ALL = Flag(name="A", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER) SHORT_ALL = Flag(name="A", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
HOST = Flag( HOST = Flag(name="host", possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"))
name="host", possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
)
SHORT_HOST = Flag( SHORT_HOST = Flag(
name="H", name="H",
prefix=DEFAULT_PREFIX, prefix=DEFAULT_PREFIX,
+18 -16
View File
@@ -6,19 +6,21 @@ from typing import Literal, override
class PossibleValues(Enum): class PossibleValues(Enum):
NEITHER = 'NEITHER' NEITHER = "NEITHER"
ALL = 'ALL' ALL = "ALL"
class ValidationStatus(Enum): class ValidationStatus(Enum):
VALID = 'VALID' VALID = "VALID"
INVALID = 'INVALID' INVALID = "INVALID"
UNDEFINED = 'UNDEFINED' UNDEFINED = "UNDEFINED"
class Flag: class Flag:
def __init__( def __init__(
self, name: str, *, self,
name: str,
*,
prefix: Literal["-", "--", "---"] = "--", prefix: Literal["-", "--", "---"] = "--",
possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL, possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
) -> None: ) -> None:
@@ -65,7 +67,7 @@ class Flag:
@override @override
def __repr__(self) -> str: def __repr__(self) -> str:
return f'Flag<name={self.name}, prefix={self.prefix}>' return f"Flag<name={self.name}, prefix={self.prefix}>"
@override @override
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
@@ -77,10 +79,12 @@ class Flag:
class InputFlag: class InputFlag:
def __init__( def __init__(
self, name: str, *, self,
prefix: Literal['-', '--', '---'] = '--', name: str,
*,
prefix: Literal["-", "--", "---"] = "--",
input_value: str | None, input_value: str | None,
status: ValidationStatus | None status: ValidationStatus | None,
): ):
""" """
Public. The entity of the flag of the entered command Public. The entity of the flag of the entered command
@@ -90,7 +94,7 @@ class InputFlag:
:return: None :return: None
""" """
self.name: str = name self.name: str = name
self.prefix: Literal['-', '--', '---'] = prefix self.prefix: Literal["-", "--", "---"] = prefix
self.input_value: str | None = input_value self.input_value: str | None = input_value
self.status: ValidationStatus | None = status self.status: ValidationStatus | None = status
@@ -105,17 +109,15 @@ class InputFlag:
@override @override
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.string_entity} {self.input_value}' return f"{self.string_entity} {self.input_value}"
@override @override
def __repr__(self) -> str: 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 @override
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
if isinstance(other, InputFlag): if isinstance(other, InputFlag):
return ( return self.name == other.name
self.name == other.name
)
else: else:
raise NotImplementedError raise NotImplementedError
+16 -19
View File
@@ -1,13 +1,12 @@
__all__ = [ __all__ = ["Command", "InputCommand"]
"Command",
"InputCommand"
]
from typing import Literal, Never, Self, cast from typing import Literal, Never, Self, cast
from argenta.command.exceptions import (EmptyInputCommandException, from argenta.command.exceptions import (
EmptyInputCommandException,
RepeatedInputFlagsException, RepeatedInputFlagsException,
UnprocessedInputFlagException) UnprocessedInputFlagException,
)
from argenta.command.flag.flags.models import Flags, InputFlags from argenta.command.flag.flags.models import Flags, InputFlags
from argenta.command.flag.models import Flag, InputFlag, ValidationStatus from argenta.command.flag.models import Flag, InputFlag, ValidationStatus
@@ -23,7 +22,8 @@ DEFAULT_WITHOUT_INPUT_FLAGS: InputFlags = InputFlags()
class Command: class Command:
def __init__( def __init__(
self, self,
trigger: str, *, trigger: str,
*,
description: str | None = None, description: str | None = None,
flags: Flag | Flags = DEFAULT_WITHOUT_FLAGS, flags: Flag | Flags = DEFAULT_WITHOUT_FLAGS,
aliases: list[str] | None = None, aliases: list[str] | None = None,
@@ -40,9 +40,7 @@ class Command:
self.description: str = description if description else "Command without description" self.description: str = description if description else "Command without description"
self.aliases: list[str] = aliases if aliases else [] self.aliases: list[str] = aliases if aliases else []
def validate_input_flag( def validate_input_flag(self, flag: InputFlag) -> ValidationStatus:
self, flag: InputFlag
) -> ValidationStatus:
""" """
Private. Validates the input flag Private. Validates the input flag
:param flag: input flag for validation :param flag: input flag for validation
@@ -60,8 +58,7 @@ class Command:
class InputCommand: class InputCommand:
def __init__(self, trigger: str, *, def __init__(self, trigger: str, *, input_flags: InputFlag | InputFlags = DEFAULT_WITHOUT_INPUT_FLAGS):
input_flags: InputFlag | InputFlags = DEFAULT_WITHOUT_INPUT_FLAGS):
""" """
Private. The model of the input command, after parsing Private. The model of the input command, after parsing
:param trigger:the trigger of the command :param trigger:the trigger of the command
@@ -69,7 +66,9 @@ class InputCommand:
:return: None :return: None
""" """
self.trigger: str = trigger 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 @classmethod
def parse(cls, raw_command: str) -> Self: def parse(cls, raw_command: str) -> Self:
@@ -114,7 +113,7 @@ class CommandParser:
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, input_value=crnt_flg_val,
status=None status=None,
) )
if input_flag in self._parsed_input_flags: 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) return (self._parsed_input_flags, crnt_flg_name, crnt_flg_val)
def _is_next_token_value(self, current_index: int, def _is_next_token_value(self, current_index: int, _tokens: list[str] | list[Never]) -> bool:
_tokens: list[str] | list[Never]) -> bool:
next_index = current_index + 1 next_index = current_index + 1
if next_index >= len(_tokens): if next_index >= len(_tokens):
return False return False
@@ -134,10 +132,9 @@ class CommandParser:
next_token = _tokens[next_index] next_token = _tokens[next_index]
return not next_token.startswith(MIN_FLAG_PREFIX) return not next_token.startswith(MIN_FLAG_PREFIX)
def _parse_single_token( def _parse_single_token(
token: str, token: str, crnt_flag_name: str | None, crnt_flag_val: str | None
crnt_flag_name: str | None,
crnt_flag_val: str | None
) -> tuple[str | None, str | None]: ) -> tuple[str | None, str | None]:
if not token.startswith(MIN_FLAG_PREFIX): if not token.startswith(MIN_FLAG_PREFIX):
if not crnt_flag_name or crnt_flag_val: if not crnt_flag_name or crnt_flag_val:
+3 -5
View File
@@ -5,8 +5,8 @@ from typing import Any, Callable, TypeVar
from dishka import Container, FromDishka from dishka import Container, FromDishka
from dishka.integrations.base import is_dishka_injected, wrap_injection from dishka.integrations.base import is_dishka_injected, wrap_injection
from argenta.app import App from argenta.app.models import App
from argenta.response import Response from argenta.response.entity import Response
T = TypeVar("T") T = TypeVar("T")
@@ -25,9 +25,7 @@ def setup_dishka(app: App, container: Container, *, auto_inject: bool = False) -
Response.patch_by_container(container) Response.patch_by_container(container)
def _get_container_from_response( def _get_container_from_response(args: tuple[Any, ...], kwargs: dict[str, Any]) -> Container:
args: tuple[Any, ...], kwargs: dict[str, Any]
) -> Container:
for arg in args: for arg in args:
if isinstance(arg, Response): if isinstance(arg, Response):
if hasattr(arg, "_dishka_container"): if hasattr(arg, "_dishka_container"):
+3 -7
View File
@@ -1,5 +1,5 @@
__all__ = [ __all__ = [
'SystemProvider', "SystemProvider",
] ]
from dishka import Provider, Scope, provide from dishka import Provider, Scope, provide
@@ -10,13 +10,9 @@ from argenta.orchestrator.argparser.entity import ArgSpace
class SystemProvider(Provider): class SystemProvider(Provider):
def __init__(self, arg_parser: ArgParser):
super().__init__()
self._arg_parser: ArgParser = arg_parser
@provide(scope=Scope.APP) @provide(scope=Scope.APP)
def get_argspace(self) -> ArgSpace: def get_argspace(self, arg_parser: ArgParser) -> ArgSpace:
return self._arg_parser.parsed_argspace return arg_parser.parsed_argspace
@provide(scope=Scope.APP) @provide(scope=Scope.APP)
def get_data_bridge(self) -> DataBridge: def get_data_bridge(self) -> DataBridge:
+1 -2
View File
@@ -1,2 +1 @@
from argenta.metrics.main import \ from argenta.metrics.main import get_time_of_pre_cycle_setup as get_time_of_pre_cycle_setup
get_time_of_pre_cycle_setup as get_time_of_pre_cycle_setup
+1 -1
View File
@@ -1,5 +1,5 @@
__all__ = [ __all__ = [
'get_time_of_pre_cycle_setup', "get_time_of_pre_cycle_setup",
] ]
import io import io
@@ -1,6 +1,4 @@
from argenta.orchestrator.argparser.arguments import \ from argenta.orchestrator.argparser.arguments import BooleanArgument as BooleanArgument
BooleanArgument as BooleanArgument from argenta.orchestrator.argparser.arguments import ValueArgument as ValueArgument
from argenta.orchestrator.argparser.arguments import \
ValueArgument as ValueArgument
from argenta.orchestrator.argparser.entity import ArgParser as ArgParser from argenta.orchestrator.argparser.entity import ArgParser as ArgParser
from argenta.orchestrator.argparser.entity import ArgSpace as ArgSpace from argenta.orchestrator.argparser.entity import ArgSpace as ArgSpace
@@ -1,6 +1,3 @@
from argenta.orchestrator.argparser.arguments.models import \ from argenta.orchestrator.argparser.arguments.models import BooleanArgument as BooleanArgument
BooleanArgument as BooleanArgument from argenta.orchestrator.argparser.arguments.models import InputArgument as InputArgument
from argenta.orchestrator.argparser.arguments.models import \ from argenta.orchestrator.argparser.arguments.models import ValueArgument as ValueArgument
InputArgument as InputArgument
from argenta.orchestrator.argparser.arguments.models import \
ValueArgument as ValueArgument
@@ -1,8 +1,4 @@
__all__ = [ __all__ = ["BooleanArgument", "ValueArgument", "InputArgument"]
'BooleanArgument',
'ValueArgument',
'InputArgument'
]
from typing import Literal from typing import Literal
@@ -11,10 +7,8 @@ class BaseArgument:
""" """
Private. Base class for all arguments Private. Base class for all arguments
""" """
def __init__(self, name: str, *,
help: str, def __init__(self, name: str, *, help: str, is_deprecated: bool, prefix: Literal["-", "--", "---"]):
is_deprecated: bool,
prefix: Literal["-", "--", "---"]):
""" """
Public. Boolean argument, does not require a value Public. Boolean argument, does not require a value
:param name: name of the argument :param name: name of the argument
@@ -33,13 +27,17 @@ class BaseArgument:
class ValueArgument(BaseArgument): class ValueArgument(BaseArgument):
def __init__(self, name: str, *, def __init__(
self,
name: str,
*,
prefix: Literal["-", "--", "---"] = "--", prefix: Literal["-", "--", "---"] = "--",
help: str = "Help message for the value argument", help: str = "Help message for the value argument",
possible_values: list[str] | None = None, possible_values: list[str] | None = None,
default: str | None = None, default: str | None = None,
is_required: bool = False, is_required: bool = False,
is_deprecated: bool = False): is_deprecated: bool = False,
):
""" """
Public. Value argument, must have the value Public. Value argument, must have the value
:param name: name of the argument :param name: name of the argument
@@ -58,10 +56,14 @@ class ValueArgument(BaseArgument):
class BooleanArgument(BaseArgument): class BooleanArgument(BaseArgument):
def __init__(self, name: str, *, def __init__(
self,
name: str,
*,
prefix: Literal["-", "--", "---"] = "--", prefix: Literal["-", "--", "---"] = "--",
help: str = "Help message for the boolean argument", help: str = "Help message for the boolean argument",
is_deprecated: bool = False): is_deprecated: bool = False,
):
""" """
Public. Boolean argument, does not require a value Public. Boolean argument, does not require a value
:param name: name of the argument :param name: name of the argument
@@ -74,9 +76,7 @@ class BooleanArgument(BaseArgument):
class InputArgument: class InputArgument:
def __init__(self, name: str, def __init__(self, name: str, value: str | Literal[True], founder_class: type[BaseArgument]) -> None:
value: str | Literal[True],
founder_class: type[BaseArgument]) -> None:
self.name: str = name self.name: str = name
self.value: str | Literal[True] = value self.value: str | Literal[True] = value
self.founder_class: type[BaseArgument] = founder_class self.founder_class: type[BaseArgument] = founder_class
+26 -21
View File
@@ -6,10 +6,12 @@ __all__ = [
from argparse import ArgumentParser, Namespace from argparse import ArgumentParser, Namespace
from typing import Never, Self from typing import Never, Self
from argenta.orchestrator.argparser.arguments.models import (BaseArgument, from argenta.orchestrator.argparser.arguments.models import (
BaseArgument,
BooleanArgument, BooleanArgument,
InputArgument, InputArgument,
ValueArgument) ValueArgument,
)
class ArgSpace: class ArgSpace:
@@ -17,16 +19,16 @@ class ArgSpace:
self.all_arguments = all_arguments self.all_arguments = all_arguments
@classmethod @classmethod
def from_namespace(cls, namespace: Namespace, def from_namespace(
processed_args: list[ValueArgument | BooleanArgument]) -> Self: cls, namespace: Namespace, processed_args: list[ValueArgument | BooleanArgument]
name_type_paired_args: dict[str, type[BaseArgument]] = { ) -> Self:
arg.name: type(arg) name_type_paired_args: dict[str, type[BaseArgument]] = {arg.name: type(arg) for arg in processed_args}
for arg in processed_args return cls(
} [
return cls([InputArgument(name=name, InputArgument(name=name, value=value, founder_class=name_type_paired_args[name])
value=value, for name, value in vars(namespace).items()
founder_class=name_type_paired_args[name]) ]
for name, value in vars(namespace).items()]) )
def get_by_name(self, name: str) -> InputArgument | None: def get_by_name(self, name: str) -> InputArgument | None:
for arg in self.all_arguments: for arg in self.all_arguments:
@@ -41,7 +43,8 @@ class ArgSpace:
class ArgParser: class ArgParser:
def __init__( def __init__(
self, self,
processed_args: list[ValueArgument | BooleanArgument], *, processed_args: list[ValueArgument | BooleanArgument],
*,
name: str = "Argenta", name: str = "Argenta",
description: str = "Argenta available arguments", description: str = "Argenta available arguments",
epilog: str = "github.com/koloideal/Argenta | made by kolo", epilog: str = "github.com/koloideal/Argenta | made by kolo",
@@ -64,21 +67,23 @@ class ArgParser:
self._register_args(processed_args) self._register_args(processed_args)
def _parse_args(self) -> None: def _parse_args(self) -> None:
self.parsed_argspace = ArgSpace.from_namespace(namespace=self._core.parse_args(), self.parsed_argspace = ArgSpace.from_namespace(
processed_args=self.processed_args) namespace=self._core.parse_args(), processed_args=self.processed_args
)
def _register_args(self, processed_args: list[ValueArgument | BooleanArgument]) -> None: def _register_args(self, processed_args: list[ValueArgument | BooleanArgument]) -> None:
for arg in processed_args: for arg in processed_args:
if isinstance(arg, BooleanArgument): if isinstance(arg, BooleanArgument):
_ = self._core.add_argument(arg.string_entity, _ = self._core.add_argument(
action=arg.action, arg.string_entity, action=arg.action, help=arg.help, deprecated=arg.is_deprecated
help=arg.help, )
deprecated=arg.is_deprecated)
else: else:
_ = self._core.add_argument(arg.string_entity, _ = self._core.add_argument(
arg.string_entity,
action=arg.action, action=arg.action,
help=arg.help, help=arg.help,
default=arg.default, default=arg.default,
choices=arg.possible_values, choices=arg.possible_values,
required=arg.is_required, required=arg.is_required,
deprecated=arg.is_deprecated) deprecated=arg.is_deprecated,
)
+8 -3
View File
@@ -11,9 +11,12 @@ DEFAULT_ARGPARSER: ArgParser = ArgParser(processed_args=[])
class Orchestrator: class Orchestrator:
def __init__(self, arg_parser: ArgParser = DEFAULT_ARGPARSER, def __init__(
self,
arg_parser: ArgParser = DEFAULT_ARGPARSER,
custom_providers: list[Provider] = [], custom_providers: list[Provider] = [],
auto_inject_handlers: bool = True): auto_inject_handlers: bool = True,
):
""" """
Public. An orchestrator and configurator that defines the behavior of an integrated system, one level higher than the App 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 :param arg_parser: Cmd argument parser and configurator at startup
@@ -31,7 +34,9 @@ class Orchestrator:
:param app: a running application :param app: a running application
:return: None :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) setup_dishka(app, container, auto_inject=self._auto_inject_handlers)
app.run_polling() app.run_polling()
+1 -1
View File
@@ -10,7 +10,7 @@ class ResponseStatus(Enum):
UNDEFINED_AND_INVALID_FLAGS = "UNDEFINED_AND_INVALID_FLAGS" UNDEFINED_AND_INVALID_FLAGS = "UNDEFINED_AND_INVALID_FLAGS"
@classmethod @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) key = (has_invalid_value_flags, has_undefined_flags)
status_map: dict[tuple[bool, bool], ResponseStatus] = { status_map: dict[tuple[bool, bool], ResponseStatus] = {
(True, True): cls.UNDEFINED_AND_INVALID_FLAGS, (True, True): cls.UNDEFINED_AND_INVALID_FLAGS,
+1 -3
View File
@@ -32,9 +32,7 @@ class CommandHandlers:
Private. The model that unites all CommandHandler of the routers Private. The model that unites all CommandHandler of the routers
:param command_handlers: list of CommandHandlers for register :param command_handlers: list of CommandHandlers for register
""" """
self.command_handlers: list[CommandHandler] = ( self.command_handlers: list[CommandHandler] = command_handlers if command_handlers else []
command_handlers if command_handlers else []
)
def add_handler(self, command_handler: CommandHandler) -> None: def add_handler(self, command_handler: CommandHandler) -> None:
""" """
+11 -21
View File
@@ -1,7 +1,6 @@
__all__ = ["Router"] __all__ = ["Router"]
from inspect import (get_annotations, getfullargspec, getsourcefile, from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines
getsourcelines)
from typing import Callable, TypeAlias from typing import Callable, TypeAlias
from rich.console import Console 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 import ValidationStatus
from argenta.command.flag.flags import Flags, InputFlags from argenta.command.flag.flags import Flags, InputFlags
from argenta.response import Response, ResponseStatus from argenta.response import Response, ResponseStatus
from argenta.router.command_handler.entity import (CommandHandler, from argenta.router.command_handler.entity import CommandHandler, CommandHandlers
CommandHandlers) from argenta.router.exceptions import (
from argenta.router.exceptions import (RepeatedFlagNameException, RepeatedFlagNameException,
RequiredArgumentNotPassedException, RequiredArgumentNotPassedException,
TriggerContainSpacesException) TriggerContainSpacesException,
)
HandlerFunc: TypeAlias = Callable[..., None] HandlerFunc: TypeAlias = Callable[..., None]
@@ -78,9 +78,7 @@ class Router:
if input_command_name.lower() in handle_command.aliases: if input_command_name.lower() in handle_command.aliases:
self.process_input_command(input_command_flags, command_handler) self.process_input_command(input_command_flags, command_handler)
def process_input_command( def process_input_command(self, input_command_flags: InputFlags, command_handler: CommandHandler) -> None:
self, input_command_flags: InputFlags, command_handler: CommandHandler
) -> None:
""" """
Private. Processes input command with the appropriate handler Private. Processes input command with the appropriate handler
:param input_command_flags: input command flags as InputFlags :param input_command_flags: input command flags as InputFlags
@@ -90,9 +88,7 @@ class Router:
handle_command = command_handler.handled_command handle_command = command_handler.handled_command
if handle_command.registered_flags.flags: if handle_command.registered_flags.flags:
if input_command_flags.flags: if input_command_flags.flags:
response: Response = _structuring_input_flags( response: Response = _structuring_input_flags(handle_command, input_command_flags)
handle_command, input_command_flags
)
command_handler.handling(response) command_handler.handling(response)
else: else:
response = Response(ResponseStatus.ALL_FLAGS_VALID) response = Response(ResponseStatus.ALL_FLAGS_VALID)
@@ -103,9 +99,7 @@ class Router:
for input_flag in input_command_flags: for input_flag in input_command_flags:
input_flag.status = ValidationStatus.UNDEFINED input_flag.status = ValidationStatus.UNDEFINED
undefined_flags.add_flag(input_flag) undefined_flags.add_flag(input_flag)
response = Response( response = Response(ResponseStatus.UNDEFINED_FLAGS, input_flags=undefined_flags)
ResponseStatus.UNDEFINED_FLAGS, input_flags=undefined_flags
)
command_handler.handling(response) command_handler.handling(response)
else: else:
response = Response(ResponseStatus.ALL_FLAGS_VALID) response = Response(ResponseStatus.ALL_FLAGS_VALID)
@@ -142,15 +136,11 @@ class CommandDecorator:
def __call__(self, handler_func: Callable[..., None]) -> Callable[..., None]: def __call__(self, handler_func: Callable[..., None]) -> Callable[..., None]:
_validate_func_args(handler_func) _validate_func_args(handler_func)
self.router.command_handlers.add_handler( self.router.command_handlers.add_handler(CommandHandler(handler_func, self.command))
CommandHandler(handler_func, self.command)
)
return handler_func return handler_func
def _structuring_input_flags( def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response:
handled_command: Command, input_flags: InputFlags
) -> Response:
""" """
Private. Validates flags of input command Private. Validates flags of input command
:param handled_command: entity of the handled 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())