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