This commit is contained in:
2025-11-04 11:21:35 +03:00
parent 7b85b0f08d
commit 4da876b774
16 changed files with 122 additions and 48 deletions
@@ -1,9 +1,7 @@
from argenta import App from argenta import App
def empty_command_handler(): def empty_command_handler():
print("Empty command handler called") print("Empty command handler called")
app: App = App() app: App = App()
app.set_empty_command_handler(empty_command_handler) app.set_empty_command_handler(empty_command_handler)
@@ -1,9 +1,7 @@
from argenta import App from argenta import App
def incorrect_input_syntax_handler(raw_command: str): def incorrect_input_syntax_handler(raw_command: str):
print(f"Incorrect input syntax for command: {raw_command}") print(f"Incorrect input syntax for command: {raw_command}")
app: App = App() app: App = App()
app.set_incorrect_input_syntax_handler(incorrect_input_syntax_handler) app.set_incorrect_input_syntax_handler(incorrect_input_syntax_handler)
@@ -1,9 +1,7 @@
from argenta import App from argenta import App
def repeated_input_flags_handler(raw_command: str): def repeated_input_flags_handler(raw_command: str):
print(f"Repeated input flags: {raw_command}") print(f"Repeated input flags: {raw_command}")
app: App = App() app: App = App()
app.set_repeated_input_flags_handler(repeated_input_flags_handler) app.set_repeated_input_flags_handler(repeated_input_flags_handler)
@@ -1,9 +1,7 @@
from argenta import App from argenta import App
def empty_command_handler(): def empty_command_handler():
print("Empty input command") print("Empty input command")
app: App = App() app: App = App()
app.set_empty_command_handler(empty_command_handler) app.set_empty_command_handler(empty_command_handler)
@@ -1,10 +1,8 @@
from argenta import App from argenta import App
from argenta.command import InputCommand from argenta.command import InputCommand
def unknown_command_handler(command: InputCommand): def unknown_command_handler(command: InputCommand):
print(f"Unknown input command with trigger: {command.trigger}") print(f"Unknown input command with trigger: {command.trigger}")
app: App = App() app: App = App()
app.set_unknown_command_handler(unknown_command_handler) app.set_unknown_command_handler(unknown_command_handler)
@@ -1,9 +1,7 @@
from argenta import App, Response from argenta import App, Response
def exit_command_handler(response: Response): def exit_command_handler(response: Response):
print("Exit command handler") print("Exit command handler")
app: App = App() app: App = App()
app.set_exit_command_handler(exit_command_handler) app.set_exit_command_handler(exit_command_handler)
+7 -5
View File
@@ -12,12 +12,13 @@ orchestrator = Orchestrator()
# 2. Создание роутера для группировки команд # 2. Создание роутера для группировки команд
main_router = Router(title="Основные команды") main_router = Router(title="Основные команды")
# 3. Определение команды и её обработчика # 3. Определение команды и её обработчика
@main_router.command(Command( @main_router.command(
"hello", Command(
description="Печатает приветственное сообщение", "hello", description="Печатает приветственное сообщение", flags=Flag("name")
flags=Flag("name") )
)) )
def hello_handler(response: Response): def hello_handler(response: Response):
"""Этот обработчик будет вызван для команды 'hello'.""" """Этот обработчик будет вызван для команды 'hello'."""
name = response.input_flags.get_flag_by_name("name") name = response.input_flags.get_flag_by_name("name")
@@ -26,6 +27,7 @@ def hello_handler(response: Response):
else: else:
print("Привет, мир!") print("Привет, мир!")
# 4. Подключение роутера к приложению # 4. Подключение роутера к приложению
app.include_router(main_router) app.include_router(main_router)
@@ -1,8 +1,7 @@
from typing import cast from typing import cast
from argenta import Command, Response, Router from argenta import Command, Response, Router
from argenta.command import Flag, Flags from argenta.command.flag import ValidationStatus, Flag, Flags
from argenta.command.flag.models import ValidationStatus
from argenta.di import FromDishka from argenta.di import FromDishka
from .repository import Priority, Task, TaskRepository from .repository import Priority, Task, TaskRepository
@@ -24,14 +23,12 @@ router = Router(title="Task Manager")
) )
def add_task(response: Response, repo: FromDishka[TaskRepository]): def add_task(response: Response, repo: FromDishka[TaskRepository]):
description_flag = response.input_flags.get_flag_by_name("description") description_flag = response.input_flags.get_flag_by_name("description")
if not description_flag or not description_flag.status == ValidationStatus.VALID:
if not description_flag or not description_flag.input_value:
print("Error: --description flag is required.") print("Error: --description flag is required.")
return return
task_description = description_flag.input_value task_description = description_flag.input_value or ""
priority_flag = response.input_flags.get_flag_by_name("priority") priority_flag = response.input_flags.get_flag_by_name("priority")
if priority_flag and priority_flag.status == ValidationStatus.VALID: if priority_flag and priority_flag.status == ValidationStatus.VALID:
priority_value = priority_flag.input_value priority_value = priority_flag.input_value
else: else:
@@ -3,16 +3,16 @@ from argenta import App, Orchestrator
from .handlers import router from .handlers import router
from .provider import TaskProvider from .provider import TaskProvider
# 1. Создаем экземпляр приложения # 1. Создаем экземпляр приложения и оркестратора
app = App( app = App(
initial_message="Task Manager", initial_message="Task Manager",
prompt="Enter a command: ", prompt="Enter a command: ",
) )
orchestrator = Orchestrator(custom_providers=[TaskProvider()])
# 2. Подключаем роутер с нашими командами # 2. Подключаем роутер с нашими командами
app.include_router(router) app.include_router(router)
# 3. Создаем и запускаем оркестратор # 3. Запускаем поллинг через оркестратор
if __name__ == "__main__": if __name__ == "__main__":
orchestrator = Orchestrator(custom_providers=[TaskProvider()])
orchestrator.start_polling(app) orchestrator.start_polling(app)
+15 -8
View File
@@ -3,19 +3,23 @@ from argenta.command import Flag
from argenta.di import FromDishka from argenta.di import FromDishka
# 1. Создаём роутер # 1. Создаём роутер
router = Router(title='Authentication') router = Router(title="Authentication")
# 2. Определяем сервис и обработчики # 2. Определяем сервис и обработчики
def authenticate_user(username: str) -> str: def authenticate_user(username: str) -> str:
"""Возвращает фиктивный токен для пользователя.""" """Возвращает фиктивный токен для пользователя."""
return f"token_for_{username}" return f"token_for_{username}"
@router.command(Command('login', flags=Flag('username')))
@router.command(Command("login", flags=Flag("username")))
def login_handler(response: Response, data_bridge: FromDishka[DataBridge]): def login_handler(response: Response, data_bridge: FromDishka[DataBridge]):
"""Обработчик для команды 'login'. Сохраняет токен в хранилище.""" """Обработчик для команды 'login'. Сохраняет токен в хранилище."""
username_flag = response.input_flags.get_flag_by_name('username') username_flag = response.input_flags.get_flag_by_name("username")
if not username_flag or not username_flag.input_value: if not username_flag or not username_flag.input_value:
print("[red]Ошибка:[/red] необходимо указать имя пользователя с помощью флага --username.") print(
"[red]Ошибка:[/red] необходимо указать имя пользователя с помощью флага --username."
)
return return
username = username_flag.input_value username = username_flag.input_value
@@ -25,19 +29,23 @@ def login_handler(response: Response, data_bridge: FromDishka[DataBridge]):
data_bridge.update({"auth_token": token}) data_bridge.update({"auth_token": token})
print(f"[green]Успешный вход![/green] Пользователь '{username}' аутентифицирован.") print(f"[green]Успешный вход![/green] Пользователь '{username}' аутентифицирован.")
@router.command('get-profile')
@router.command("get-profile")
def get_profile_handler(response: Response, data_bridge: FromDishka[DataBridge]): def get_profile_handler(response: Response, data_bridge: FromDishka[DataBridge]):
"""Обработчик для команды 'get-profile'. Использует токен из хранилища.""" """Обработчик для команды 'get-profile'. Использует токен из хранилища."""
session_data = data_bridge.get_all() session_data = data_bridge.get_all()
token = session_data.get("auth_token") token = session_data.get("auth_token")
if not token: if not token:
print("[red]Ошибка:[/red] вы не аутентифицированы. Сначала выполните команду 'login'.") print(
"[red]Ошибка:[/red] вы не аутентифицированы. Сначала выполните команду 'login'."
)
return return
print(f"Загрузка профиля с использованием токена: [yellow]{token}[/yellow]") print(f"Загрузка профиля с использованием токена: [yellow]{token}[/yellow]")
@router.command('logout')
@router.command("logout")
def logout_handler(response: Response, data_bridge: FromDishka[DataBridge]): def logout_handler(response: Response, data_bridge: FromDishka[DataBridge]):
"""Обработчик для команды 'logout'. Очищает токен.""" """Обработчик для команды 'logout'. Очищает токен."""
try: try:
@@ -45,4 +53,3 @@ def logout_handler(response: Response, data_bridge: FromDishka[DataBridge]):
print("[green]Выход выполнен.[/green] Данные сессии очищены.") print("[green]Выход выполнен.[/green] Данные сессии очищены.")
except KeyError: except KeyError:
print("Вы и так не были аутентифицированы.") print("Вы и так не были аутентифицированы.")
@@ -14,6 +14,7 @@ class TestAppIntegration(unittest.TestCase):
@self.router.command(Command("HELP", description="Show help")) @self.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 _ = help_cmd # appease linter: function is registered via decorator
self.app.include_router(self.router) self.app.include_router(self.router)
@@ -22,5 +23,3 @@ class TestAppIntegration(unittest.TestCase):
with redirect_stdout(io.StringIO()) as stdout: with redirect_stdout(io.StringIO()) as stdout:
self.router.finds_appropriate_handler(InputCommand.parse("HELP")) self.router.finds_appropriate_handler(InputCommand.parse("HELP"))
self.assertIn("Available commands:", stdout.getvalue()) self.assertIn("Available commands:", stdout.getvalue())
@@ -15,6 +15,7 @@ class Service:
def hello(self) -> str: def hello(self) -> str:
return "world" return "world"
router = Router(title="DI") router = Router(title="DI")
@@ -56,5 +57,3 @@ class TestDIHandler(unittest.TestCase):
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 = []
@@ -20,5 +20,3 @@ class TestSimpleHandler(unittest.TestCase):
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()) self.assertIn("PONG", stdout.getvalue())
+8 -3
View File
@@ -6,13 +6,16 @@
Argenta Argenta
======= =======
Что это и зачем?
----------------
**Библиотека для построения модульных CLI-приложений с простым и приятным API.** **Библиотека для построения модульных CLI-приложений с простым и приятным API.**
Если у вас есть функциональность, которую вы хотите предоставить в виде CLI-приложения, Argenta поможет вам в этом. Если у вас есть функциональность, которую вы хотите предоставить в виде CLI-приложения, Argenta поможет вам в этом.
Основная цель библиотеки — дать разработчикам возможность сосредоточиться на реализации своих идей, предоставляя для этого удобные абстракции. Основная цель библиотеки — дать разработчикам возможность сосредоточиться на реализации своих идей, предоставляя для этого удобные абстракции.
.. image:: https://github.com/koloideal/Argenta/blob/main/imgs/mock_app_preview4.png?raw=True .. image:: https://github.com/koloideal/Argenta/blob/main/imgs/mock_app_preview4.png?raw=True
:alt: Пример приложения :alt: App example
Argenta предназначена для создания приложений, работающих в собственном контексте (scope). Это означает, что при запуске пользователь входит в интерактивную сессию, где ему доступна вся реализованная вами функциональность. Argenta предназначена для создания приложений, работающих в собственном контексте (scope). Это означает, что при запуске пользователь входит в интерактивную сессию, где ему доступна вся реализованная вами функциональность.
@@ -22,9 +25,11 @@ Argenta предназначена для создания приложений,
* **Интерактивные сессии**. В отличие от традиционных CLI-инструментов, Argenta создаёт циклические сессии, позволяя пользователю выполнять команды последовательно, не перезапуская приложение. * **Интерактивные сессии**. В отличие от традиционных CLI-инструментов, Argenta создаёт циклические сессии, позволяя пользователю выполнять команды последовательно, не перезапуская приложение.
* **Декларативный синтаксис**. Команды и их обработчики объявляются с помощью простых декораторов, что делает код чистым и интуитивно понятным. * **Декларативный синтаксис**. Команды и их обработчики объявляются с помощью простых декораторов, что делает код чистым и интуитивно понятным.
* **Встроенное внедрение зависимостей (DI)**. Благодаря интеграции с `dishka <https://dishka.readthedocs.io/en/stable/>`_, вы можете легко внедрять сервисы (например, подключения к БД) прямо в обработчики команд, что упрощает их тестирование и переиспользование. * **Нативный DI**. Благодаря интеграции с `dishka <https://dishka.readthedocs.io/en/stable/>`_, вы можете легко внедрять зависимости прямо в обработчики команд, что упрощает их тестирование и переиспользование.
* **Автоматическая валидация и парсинг**. Библиотека берёт на себя обработку флагов и аргументов командной строки, включая их парсинг, валидацию и преобразование типов. * **Автоматическая валидация и парсинг**. Библиотека берёт на себя обработку флагов и аргументов командной строки, включая их парсинг, валидацию и преобразование типов.
* **Гибкая настройка**. Вы можете легко кастомизировать системные сообщения, форматирование вывода и даже перенаправлять стандартный вывод (stdout) в свои обработчики. * **Гибкая настройка**. Вы можете легко кастомизировать системные сообщения, форматирование вывода и т.д.
-----
Архитектура и жизненный цикл Архитектура и жизненный цикл
----------------------------- -----------------------------
+13 -5
View File
@@ -5,12 +5,18 @@
В этом руководстве мы рассмотрим два примера создания CLI-приложения с помощью Argenta: В этом руководстве мы рассмотрим два примера создания CLI-приложения с помощью Argenta:
* **Простой пример**: Быстрое знакомство с основными компонентами, такими как `App`, `Command` и `Router`. * **Простой пример**: Минимальное приложение, быстрое знакомство с основными компонентами.
* **Более сложный пример**: Полнофункциональное приложение «Менеджер задач» с внедрением зависимостей и бизнес-логикой. * **Более сложный пример**: Полнофункциональное приложение «Менеджер задач» с внедрением зависимостей и бизнес-логикой.
Простой пример Простой пример
--------------- ---------------
**Установка**
.. code-block:: shell
pip install argenta
Этот пример демонстрирует абсолютный минимум, необходимый для создания и запуска приложения. Вы можете скопировать этот код, запустить его и сразу увидеть результат. Этот пример демонстрирует абсолютный минимум, необходимый для создания и запуска приложения. Вы можете скопировать этот код, запустить его и сразу увидеть результат.
.. literalinclude:: ../code_snippets/quickstart/simple_app.py .. literalinclude:: ../code_snippets/quickstart/simple_app.py
@@ -21,6 +27,8 @@
.. image:: https://i.ibb.co/JwK9Vv4j/2025-11-03-135118.png .. image:: https://i.ibb.co/JwK9Vv4j/2025-11-03-135118.png
:alt: Simple App Example :alt: Simple App Example
-----
Более сложный пример: Менеджер задач Более сложный пример: Менеджер задач
-------------------------------------- --------------------------------------
@@ -43,7 +51,7 @@
3. **Создание провайдера для DI** 3. **Создание провайдера для DI**
Чтобы Argenta могла внедрять `TaskRepository` в наши обработчики, мы создадим провайдер для `dishka`. Чтобы Argenta могла внедрять ``TaskRepository`` в наши обработчики, мы создадим провайдер для ``dishka``.
.. literalinclude:: ../code_snippets/quickstart/task_manager/provider.py .. literalinclude:: ../code_snippets/quickstart/task_manager/provider.py
:language: python :language: python
@@ -51,7 +59,7 @@
4. **Создание обработчиков команд** 4. **Создание обработчиков команд**
Теперь создадим обработчики для команд `add-task` и `list-tasks`. Обратите внимание, как мы используем флаги и внедряем `TaskRepository`. Теперь создадим обработчики для команд ``add-task`` и ``list-tasks``. Обратите внимание, как мы используем флаги и внедряем ``TaskRepository``.
.. literalinclude:: ../code_snippets/quickstart/task_manager/handlers.py .. literalinclude:: ../code_snippets/quickstart/task_manager/handlers.py
:language: python :language: python
@@ -59,7 +67,7 @@
5. **Сборка и запуск приложения** 5. **Сборка и запуск приложения**
Наконец, соберем все вместе: создадим экземпляр `App`, подключим роутер и провайдер, а затем запустим приложение. Наконец, соберем все вместе: создадим экземпляр ``App``, подключим роутер и провайдер, а затем запустим приложение.
.. literalinclude:: ../code_snippets/quickstart/task_manager/main.py .. literalinclude:: ../code_snippets/quickstart/task_manager/main.py
:language: python :language: python
@@ -67,7 +75,7 @@
6. **Результат** 6. **Результат**
Теперь вы можете запустить `main.py` и взаимодействовать с вашим новым CLI-приложением. Теперь вы можете запустить ``main.py`` и взаимодействовать с вашим новым CLI-приложением.
.. image:: https://i.ibb.co/bgsCLZhP/image.png .. image:: https://i.ibb.co/bgsCLZhP/image.png
:alt: Task Manager Example :alt: Task Manager Example
+71
View File
@@ -0,0 +1,71 @@
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())