diff --git a/docs/code_snippets/cli/app_factory.py b/docs/code_snippets/cli/app_factory.py new file mode 100644 index 0000000..d3eff4d --- /dev/null +++ b/docs/code_snippets/cli/app_factory.py @@ -0,0 +1,22 @@ +from argenta import App, Orchestrator +from argenta.command import Router, Command, Response + +router = Router(title="Example") + +@router.command(Command("hello", description="Say hello")) +def hello_handler(response: Response): + print("Hello, world!") + + +def create_app() -> App: + app = App() + app.include_router(router) + return app + + +def main() -> None: + orchestrator = Orchestrator() + orchestrator.run_repl(create_app()) + +if __name__ == "__main__": + main() diff --git a/docs/code_snippets/cli/app_instance.py b/docs/code_snippets/cli/app_instance.py new file mode 100644 index 0000000..c00555c --- /dev/null +++ b/docs/code_snippets/cli/app_instance.py @@ -0,0 +1,16 @@ +from argenta import App, Orchestrator +from argenta.command import Router, Command, Response + +router = Router(title="Example") + +@router.command(Command("hello", description="Say hello")) +def hello_handler(response: Response): + print("Hello, world!") + +app = App() +app.include_router(router) + +orchestrator = Orchestrator() + +if __name__ == "__main__": + orchestrator.run_repl(app) diff --git a/docs/code_snippets/cli/flat_structure.txt b/docs/code_snippets/cli/flat_structure.txt new file mode 100644 index 0000000..2231d8f --- /dev/null +++ b/docs/code_snippets/cli/flat_structure.txt @@ -0,0 +1,4 @@ +my_project/ +├── main.py +├── handlers.py +└── .gitignore diff --git a/docs/code_snippets/cli/src_structure.txt b/docs/code_snippets/cli/src_structure.txt new file mode 100644 index 0000000..b92b85b --- /dev/null +++ b/docs/code_snippets/cli/src_structure.txt @@ -0,0 +1,11 @@ +my_project/ +├── src/ +│ └── my_project/ +│ └── application/ +│ ├── __init__.py +│ ├── __main__.py +│ ├── routers.py +│ └── handlers/ +│ ├── __init__.py +│ └── hello_world_handler.py +└── .gitignore diff --git a/docs/index.rst b/docs/index.rst index 2033b34..07c2855 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -56,6 +56,7 @@ Argenta предназначена для создания приложений, root/redirect_stdout root/dependency_injection + root/cli root/testing .. toctree:: diff --git a/docs/locales/en/LC_MESSAGES/root/cli.po b/docs/locales/en/LC_MESSAGES/root/cli.po new file mode 100644 index 0000000..98e3eed --- /dev/null +++ b/docs/locales/en/LC_MESSAGES/root/cli.po @@ -0,0 +1,265 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2025, kolo +# This file is distributed under the same license as the Argenta package. +# FIRST AUTHOR , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: Argenta \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-07-25 18:00+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: en\n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.17.0\n" + +#: ../../root/cli.rst:3 +msgid "Командная строка" +msgstr "Command Line Interface" + +#: ../../root/cli.rst:5 +msgid "" +"Помимо библиотеки, ``Argenta`` поставляется с собственным CLI-инструментом, " +"который помогает создавать проекты, запускать приложения, инспектировать " +"маршруты и собирать бинарники." +msgstr "" +"In addition to the library, ``Argenta`` ships with its own CLI tool that " +"helps scaffold projects, run applications, inspect routes, and build binaries." + +#: ../../root/cli.rst:8 +msgid "Установка" +msgstr "Installation" + +#: ../../root/cli.rst:10 +msgid "CLI доступен как опциональная зависимость:" +msgstr "The CLI is available as an optional dependency:" + +#: ../../root/cli.rst:16 +msgid "" +"После установки команда ``argenta`` доступна в терминале:" +msgstr "" +"After installation, the ``argenta`` command is available in the terminal:" + +#: ../../root/cli.rst:22 +msgid "" +"Если вы устанавливали ``argenta`` без extras, CLI-инструмент не будет " +"доступен. Установите с ``[cli]`` для доступа к команде ``argenta``." +msgstr "" +"If you installed ``argenta`` without extras, the CLI tool will not be " +"available. Install with ``[cli]`` to access the ``argenta`` command." + +#: ../../root/cli.rst:25 +msgid "Флаг ``--version``" +msgstr "The ``--version`` flag" + +#: ../../root/cli.rst:27 +msgid "Показать установленную версию ``Argenta``:" +msgstr "Show the installed ``Argenta`` version:" + +#: ../../root/cli.rst:35 +msgid "Создание проектов" +msgstr "Scaffolding Projects" + +#: ../../root/cli.rst:38 +msgid "Команда ``new``" +msgstr "The ``new`` command" + +#: ../../root/cli.rst:40 +msgid "Создаёт новую директорию проекта с boilerplate-кодом." +msgstr "Creates a new project directory with boilerplate code." + +#: ../../root/cli.rst:44 +msgid "``project_name`` — имя директории проекта (обязательный аргумент)." +msgstr "``project_name`` — project directory name (required argument)." + +#: ../../root/cli.rst:45 +msgid "``--with-arch`` — архитектура проекта: ``flat`` (по умолчанию) или ``src``." +msgstr "``--with-arch`` — project architecture: ``flat`` (default) or ``src``." + +#: ../../root/cli.rst:53 +msgid "При архитектуре ``flat`` создаётся следующая структура:" +msgstr "With the ``flat`` architecture, the following structure is created:" + +#: ../../root/cli.rst:59 +msgid "При архитектуре ``src``:" +msgstr "With the ``src`` architecture:" + +#: ../../root/cli.rst:67 +msgid "Команда ``init``" +msgstr "The ``init`` command" + +#: ../../root/cli.rst:69 +msgid "" +"Инициализирует boilerplate в текущей директории. Удобно, когда проект уже " +"существует и нужно добавить структуру Argenta." +msgstr "" +"Initializes boilerplate in the current directory. Useful when the project " +"already exists and you need to add Argenta structure." + +#: ../../root/cli.rst:75 +msgid "" +"Команда ``init`` не перезаписывает существующие файлы — они будут пропущены." +msgstr "" +"The ``init`` command does not overwrite existing files — they will be skipped." + +#: ../../root/cli.rst:80 +msgid "Запуск приложений" +msgstr "Running Applications" + +#: ../../root/cli.rst:83 +msgid "Команда ``run``" +msgstr "The ``run`` command" + +#: ../../root/cli.rst:85 +msgid "" +"Запускает оркестратор ``Argenta`` из callable-ентрипойнта. Это альтернатива " +"прямому вызову ``python main.py``, но с автоматической настройкой окружения." +msgstr "" +"Starts the ``Argenta`` orchestrator from a callable entrypoint. This is an " +"alternative to directly calling ``python main.py``, but with automatic " +"environment setup." + +#: ../../root/cli.rst:89 +msgid "" +"``entrypoint`` — путь к callable в формате " +"``:`` или ``:``." +msgstr "" +"``entrypoint`` — path to a callable in the format " +"``:`` or ``:``." + +#: ../../root/cli.rst:99 +msgid "" +"Команда ``run`` устанавливает переменную окружения " +"``RUN_FROM_ARGENTA_RUNNER=1``, что отключает парсинг аргументов командной " +"строки в ``ArgParser``. Это позволяет запустить REPL без конфликтов с " +"CLI-аргументами ``argenta``." +msgstr "" +"The ``run`` command sets the ``RUN_FROM_ARGENTA_RUNNER=1`` environment " +"variable, which disables command-line argument parsing in ``ArgParser``. " +"This allows starting the REPL without conflicts with ``argenta`` CLI arguments." + +#: ../../root/cli.rst:104 +msgid "Инспекция маршрутов" +msgstr "Inspecting Routes" + +#: ../../root/cli.rst:107 +msgid "Команда ``routes``" +msgstr "The ``routes`` command" + +#: ../../root/cli.rst:109 +msgid "" +"Отображает все зарегистрированные роутеры, команды, алиасы и флаги в виде " +"дерева. Принимает как инстанс ``App``, так и callable, возвращающий ``App``." +msgstr "" +"Displays all registered routers, commands, aliases, and flags as a tree. " +"Accepts either an ``App`` instance or a callable returning ``App``." + +#: ../../root/cli.rst:113 +msgid "" +"``entrypoint`` — путь к ``App`` или callable в формате " +"``:``." +msgstr "" +"``entrypoint`` — path to an ``App`` or callable in the format " +"``:``." + +#: ../../root/cli.rst:125 +msgid "Если передан инстанс ``App``:" +msgstr "If an ``App`` instance is passed:" + +#: ../../root/cli.rst:131 +msgid "" +"Если передан callable (фабрика), он будет вызван, и результат будет " +"использован для отображения маршрутов:" +msgstr "" +"If a callable (factory) is passed, it will be called, and the result will " +"be used to display routes:" + +#: ../../root/cli.rst:139 +msgid "" +"При использовании callable-ентрипойнта (например, ``create_app``) REPL не " +"запускается — фабрика вызывается, и маршруты считываются из возвращённого " +"``App``. Это полезно, когда роутеры подключаются внутри функции, а не на " +"уровне модуля." +msgstr "" +"When using a callable entrypoint (e.g., ``create_app``), the REPL is not " +"started — the factory is called, and routes are read from the returned " +"``App``. This is useful when routers are registered inside a function " +"rather than at the module level." + +#: ../../root/cli.rst:144 +msgid "Сборка бинарников" +msgstr "Building Binaries" + +#: ../../root/cli.rst:147 +msgid "Команда ``build``" +msgstr "The ``build`` command" + +#: ../../root/cli.rst:149 +msgid "Компилирует проект в standalone-бинарник с помощью `Nuitka `_." +msgstr "Compiles a project into a standalone binary using `Nuitka `_." + +#: ../../root/cli.rst:153 +msgid "" +"``entrypoint`` — путь к callable в формате " +"``:``." +msgstr "" +"``entrypoint`` — path to a callable in the format " +"``:``." + +#: ../../root/cli.rst:154 +msgid "" +"``--output`` / ``-o`` — имя выходного бинарника (по умолчанию — имя файла " +"или пакета)." +msgstr "" +"``--output`` / ``-o`` — output binary name (defaults to the file or package name)." + +#: ../../root/cli.rst:166 +msgid "" +"Для использования команды ``build`` необходимо установить ``Nuitka``:" +msgstr "" +"To use the ``build`` command, you must install ``Nuitka``:" + +#: ../../root/cli.rst:174 +msgid "Информация об окружении" +msgstr "Environment Information" + +#: ../../root/cli.rst:177 +msgid "Команда ``info``" +msgstr "The ``info`` command" + +#: ../../root/cli.rst:179 +msgid "" +"Отображает версию ``Argenta``, версию Python, платформу и ссылку на " +"документацию." +msgstr "" +"Displays the ``Argenta`` version, Python version, platform, and a link to " +"the documentation." + +#: ../../root/cli.rst:187 +msgid "Формат ентрипойнтов" +msgstr "Entrypoint Format" + +#: ../../root/cli.rst:190 +msgid "" +"Все команды, принимающие ентрипойнт (``run``, ``routes``, ``build``), " +"используют единый формат:" +msgstr "" +"All commands that accept an entrypoint (``run``, ``routes``, ``build``) " +"use a unified format:" + +#: ../../root/cli.rst:196 +msgid "" +"Поддерживаются как пути к файлам, так и dotted-модули. Если передан путь к " +"директории с ``__main__.py``, он будет разрешён автоматически." +msgstr "" +"Both file paths and dotted modules are supported. If a directory path " +"containing ``__main__.py`` is passed, it will be resolved automatically." + +#: ../../root/cli.rst:199 +msgid "Примеры валидных ентрипойнтов:" +msgstr "Examples of valid entrypoints:" diff --git a/docs/root/cli.rst b/docs/root/cli.rst new file mode 100644 index 0000000..739ee93 --- /dev/null +++ b/docs/root/cli.rst @@ -0,0 +1,244 @@ +.. _root_cli: + +Командная строка +================== + +Помимо библиотеки, ``Argenta`` поставляется с собственным CLI-инструментом, который помогает создавать проекты, запускать приложения, инспектировать маршруты и собирать бинарники. + +Установка +--------- + +CLI доступен как опциональная зависимость: + +.. code-block:: shell + + pip install argenta[cli] + +После установки команда ``argenta`` доступна в терминале: + +.. code-block:: shell + + argenta --help + +.. image:: _static/cli/help.png + :alt: Argenta CLI help + +.. note:: + Если вы устанавливали ``argenta`` без extras, CLI-инструмент не будет доступен. Установите с ``[cli]`` для доступа к команде ``argenta``. + +Флаг ``--version`` +~~~~~~~~~~~~~~~~~~~ + +Показать установленную версию ``Argenta``: + +.. code-block:: shell + + argenta --version + +.. code-block:: shell + + argenta -v + +----- + +Создание проектов +----------------- + +Команда ``new`` +~~~~~~~~~~~~~~~~ + +Создаёт новую директорию проекта с boilerplate-кодом. + +.. code-block:: shell + + argenta new [--with-arch flat|src] + +* ``project_name`` — имя директории проекта (обязательный аргумент). +* ``--with-arch`` — архитектура проекта: ``flat`` (по умолчанию) или ``src``. + +**Примеры:** + +.. code-block:: shell + + argenta new my-app + argenta new my-app --with-arch src + +При архитектуре ``flat`` создаётся следующая структура: + +.. literalinclude:: ../code_snippets/cli/flat_structure.txt + :language: text + +При архитектуре ``src``: + +.. literalinclude:: ../code_snippets/cli/src_structure.txt + :language: text + +.. image:: _static/cli/new_command.png + :alt: argenta new command output + +Команда ``init`` +~~~~~~~~~~~~~~~~~ + +Инициализирует boilerplate в текущей директории. Удобно, когда проект уже существует и нужно добавить структуру Argenta. + +.. code-block:: shell + + argenta init [--with-arch flat|src] + +* ``--with-arch`` — архитектура проекта: ``flat`` (по умолчанию) или ``src``. + +**Примеры:** + +.. code-block:: shell + + argenta init + argenta init --with-arch src + +.. note:: + Команда ``init`` не перезаписывает существующие файлы — они будут пропущены. + +----- + +Запуск приложений +----------------- + +Команда ``run`` +~~~~~~~~~~~~~~~~ + +Запускает оркестратор ``Argenta`` из callable-ентрипойнта. Это альтернатива прямому вызову ``python main.py``, но с автоматической настройкой окружения. + +.. code-block:: shell + + argenta run + +* ``entrypoint`` — путь к callable в формате ``:`` или ``:``. + +**Примеры:** + +.. code-block:: shell + + argenta run app/main.py:main + argenta run my_project.application:main + +.. image:: _static/cli/run_command.png + :alt: argenta run command output + +.. note:: + Команда ``run`` устанавливает переменную окружения ``RUN_FROM_ARGENTA_RUNNER=1``, что отключает парсинг аргументов командной строки в ``ArgParser``. Это позволяет запустить REPL без конфликтов с CLI-аргументами ``argenta``. + +----- + +Инспекция маршрутов +------------------- + +Команда ``routes`` +~~~~~~~~~~~~~~~~~~~ + +Отображает все зарегистрированные роутеры, команды, алиасы и флаги в виде дерева. Принимает как инстанс ``App``, так и callable, возвращающий ``App``. + +.. code-block:: shell + + argenta routes + +* ``entrypoint`` — путь к ``App`` или callable в формате ``:``. + +**Примеры:** + +.. code-block:: shell + + argenta routes app/main.py:app + argenta routes app/main.py:create_app + +Если передан инстанс ``App``: + +.. literalinclude:: ../code_snippets/cli/app_instance.py + :language: python + :linenos: + +Если передан callable (фабрика), он будет вызван, и результат будет использован для отображения маршрутов: + +.. literalinclude:: ../code_snippets/cli/app_factory.py + :language: python + :linenos: + +.. image:: _static/cli/routes_command.png + :alt: argenta routes command output + +.. note:: + При использовании callable-ентрипойнта (например, ``create_app``) REPL не запускается — фабрика вызывается, и маршруты считываются из возвращённого ``App``. Это полезно, когда роутеры подключаются внутри функции, а не на уровне модуля. + +----- + +Сборка бинарников +----------------- + +Команда ``build`` +~~~~~~~~~~~~~~~~~~ + +Компилирует проект в standalone-бинарник с помощью `Nuitka `_. + +.. code-block:: shell + + argenta build [--output ] + +* ``entrypoint`` — путь к callable в формате ``:``. +* ``--output`` / ``-o`` — имя выходного бинарника (по умолчанию — имя файла или пакета). + +**Примеры:** + +.. code-block:: shell + + argenta build app/main.py:main + argenta build app/main.py:main --output myapp + argenta build app/__main__.py:main -o myapp + +.. warning:: + Для использования команды ``build`` необходимо установить ``Nuitka``: + + .. code-block:: shell + + pip install nuitka + +.. image:: _static/cli/build_command.png + :alt: argenta build command output + +----- + +Информация об окружении +----------------------- + +Команда ``info`` +~~~~~~~~~~~~~~~~~ + +Отображает версию ``Argenta``, версию Python, платформу и ссылку на документацию. + +.. code-block:: shell + + argenta info + +.. image:: _static/cli/info_command.png + :alt: argenta info command output + +----- + +Формат ентрипойнтов +------------------- + +Все команды, принимающие ентрипойнт (``run``, ``routes``, ``build``), используют единый формат: + +.. code-block:: text + + : + : + +Поддерживаются как пути к файлам, так и dotted-модули. Если передан путь к директории с ``__main__.py``, он будет разрешён автоматически. + +**Примеры валидных ентрипойнтов:** + +.. code-block:: text + + app/main.py:main + app/main.py:app + app/main.py:create_app + my_project.application:main + my_project/application/__main__.py:main diff --git a/mock/__init__.py b/examples/__init__.py similarity index 100% rename from mock/__init__.py rename to examples/__init__.py diff --git a/mock/mock_app/__init__.py b/examples/example_app/__init__.py similarity index 100% rename from mock/mock_app/__init__.py rename to examples/example_app/__init__.py diff --git a/mock/mock_app/main.py b/examples/example_app/main.py similarity index 82% rename from mock/mock_app/main.py rename to examples/example_app/main.py index b356c24..c1981b7 100644 --- a/mock/mock_app/main.py +++ b/examples/example_app/main.py @@ -4,12 +4,12 @@ from argenta import App, Orchestrator from argenta.app import PredefinedMessages, StaticDividingLine, AutoCompleter from argenta.app.dividing_line.models import DynamicDividingLine from argenta.orchestrator import ArgParser -from mock.mock_app.routers import work_router +from examples.example_app.routers import work_router app: App = App( dividing_line=StaticDividingLine('~') ) -orchestrator: Orchestrator = Orchestrator(arg_parser=ArgParser(processed_args=[])) +orchestrator: Orchestrator = Orchestrator() def main(): diff --git a/mock/mock_app/routers.py b/examples/example_app/routers.py similarity index 100% rename from mock/mock_app/routers.py rename to examples/example_app/routers.py diff --git a/mock/min_app/main.py b/examples/example_min_app/main.py similarity index 100% rename from mock/min_app/main.py rename to examples/example_min_app/main.py diff --git a/mock/min_app/routers.py b/examples/example_min_app/routers.py similarity index 100% rename from mock/min_app/routers.py rename to examples/example_min_app/routers.py diff --git a/src/argenta/_cli/__main__.py b/src/argenta/_cli/__main__.py index a6d411f..aa33690 100644 --- a/src/argenta/_cli/__main__.py +++ b/src/argenta/_cli/__main__.py @@ -1,3 +1,6 @@ +from importlib.metadata import version + +import typer from typer import Typer from .commands import ( @@ -9,50 +12,99 @@ from .commands import ( run_handler, ) +app = Typer( + name="argenta", + help="Argenta CLI — scaffold, run, inspect, and build CLI apps.", + no_args_is_help=True, +) + + +def _version_callback(value: bool) -> None: + if value: + typer.echo(f"argenta {version('argenta')}") + raise typer.Exit() + + +@app.callback() +def _root( + version_flag: bool = typer.Option( + None, + "--version", + "-v", + callback=_version_callback, + is_eager=True, + help="Show Argenta version and exit.", + ), +) -> None: + """Argenta CLI — scaffold, run, inspect, and build CLI apps.""" + + +@app.command( + "run", + help="Start the orchestrator REPL from a callable entrypoint.", + short_help="Start the orchestrator REPL", + epilog="Example: argenta run app/main.py:main", +) +def _run(entrypoint_path: str = typer.Argument(help="Entrypoint as :")) -> None: + run_handler(entrypoint_path) + + +@app.command( + "init", + help="Scaffold a flat or src boilerplate in the current project directory.", + short_help="Initialize architecture in existing project", + epilog="Run from the project root. Example: argenta init --with-arch src", +) +def _init(with_arch: str = typer.Option("flat", "--with-arch", help="Architecture: flat or src")) -> None: + init_handler(with_arch=with_arch) # type: ignore[arg-type] + + +@app.command( + "new", + help="Create a new project directory with a flat or src boilerplate.", + short_help="Create a new project with boilerplate", + epilog="Example: argenta new my-app --with-arch src", +) +def _new( + project_name: str = typer.Argument(help="Name of the new project directory"), + with_arch: str = typer.Option("flat", "--with-arch", help="Architecture: flat or src"), +) -> None: + new_handler(project_name=project_name, with_arch=with_arch) # type: ignore[arg-type] + + +@app.command( + "routes", + help="Display all registered routes, commands, aliases, and flags. Accepts an App instance or a callable returning App.", + short_help="Show registered routes and commands", + epilog="Examples:\n argenta routes app/main.py:app\n argenta routes app/main.py:create_app", +) +def _routes(entrypoint_path: str = typer.Argument(help="Entrypoint as :")) -> None: + routes_handler(entrypoint_path) + + +@app.command( + name="info", + help="Display Argenta version, Python version, and platform info.", + short_help="Show Argenta version and environment info", +) +def _info() -> None: + info_handler() + + +@app.command( + name="build", + help="Compile a project entrypoint into a standalone binary using Nuitka.", + short_help="Build a standalone binary", + epilog="Example: argenta build app/main.py:main --output myapp", +) +def _build( + entry_point: str = typer.Argument(help="Entrypoint as :"), + output_name: str | None = typer.Option(None, "--output", "-o", help="Output binary name"), +) -> None: + build_handler(entry_point=entry_point, output_name=output_name) + def main() -> None: - app = Typer() - app.command( - "run", - help="Command to start the orchestrator repl; the path to the callable object is required", - short_help="Start the orchestrator REPL", - epilog="Example: run app/main.py:main", - )(run_handler) - - app.command( - "init", - help="Creates a flat/src boilerplate architecture in an existing project", - short_help="Initialize architecture in existing project", - epilog="Make sure you are in the project root before running this command.", - )(init_handler) - - app.command( - "new", - help="Creates a project and in it flat/src boilerplate architecture", - short_help="Create a new project with boilerplate", - epilog="This will create a new directory with the project structure.", - )(new_handler) - - app.command( - "routes", - help="Creates a project and in it flat/src boilerplate architecture", - short_help="Create a new project with boilerplate", - epilog="This will create a new directory with the project structure.", - )(routes_handler) - - app.command( - name="info", - help="Displays information about the installed Argenta package and environment", - short_help="Show Argenta version and environment info", - epilog="Uses metadata to retrieve the installed package version.", - )(info_handler) - - app.command( - name="build", - help="Compiles the project into a standalone binary using Nuitka", - short_help="Build a standalone binary", - )(build_handler) - app() diff --git a/src/argenta/_cli/commands/_templates.py b/src/argenta/_cli/commands/_templates.py new file mode 100644 index 0000000..ae0b8a5 --- /dev/null +++ b/src/argenta/_cli/commands/_templates.py @@ -0,0 +1,88 @@ +__all__ = [ + "GITIGNORE_CONTENT", + "FLAT_MAIN_TEMPLATE", + "FLAT_HANDLERS_TEMPLATE", + "SRC_MAIN_TEMPLATE", + "SRC_ROUTERS_TEMPLATE", + "SRC_HANDLER_TEMPLATE", + "create_file", +] + +from pathlib import Path + +GITIGNORE_CONTENT = """ +__pycache__/ +*.py[cod] +.env +.venv/ +env/ +""" + +FLAT_MAIN_TEMPLATE = """ +from argenta import Orchestrator, App + +from handlers import router + + +def main(): + app = App() + app.include_router(router) + + orchestrator = Orchestrator() + orchestrator.run_repl(app) + +if __name__ == "__main__": + main() +""" + +FLAT_HANDLERS_TEMPLATE = """ +from argenta import Router, Response + +router = Router("Hello command") + +@router.command("hello") +def hello_handler(response: Response): + print("Hello world!") +""" + +SRC_MAIN_TEMPLATE = """ +from argenta import Orchestrator, App + +from .routers import router + + +def main(): + app = App() + app.include_router(router) + + orchestrator = Orchestrator() + orchestrator.run_repl(app) + +if __name__ == "__main__": + main() +""" + +SRC_ROUTERS_TEMPLATE = """ +from argenta import Router +from .handlers.hello_world_handler import hello_handler + +router = Router() + +router.command("hello")(hello_handler) +""" + +SRC_HANDLER_TEMPLATE = """ +from argenta import Response + + +def hello_handler(response: Response) -> None: + print("Hello world!") +""" + + +def create_file(path: Path, content: str) -> None: + if not path.exists(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content.strip(), encoding="utf-8") + else: + print(f"Skipped: {path} (already exists)") diff --git a/src/argenta/_cli/commands/build.py b/src/argenta/_cli/commands/build.py index d7ba8cc..d6c7a61 100644 --- a/src/argenta/_cli/commands/build.py +++ b/src/argenta/_cli/commands/build.py @@ -39,7 +39,7 @@ def build_handler(entry_point: str, output_name: str | None = None) -> None: "--standalone", "--onefile", f"--output-filename={name}", - f"--jobs={os.cpu_count()}", + f"--jobs={os.cpu_count() or 1}", "--lto=no", "--include-windows-runtime-dlls=no", ] diff --git a/src/argenta/_cli/commands/info.py b/src/argenta/_cli/commands/info.py index 525c6be..178aa86 100644 --- a/src/argenta/_cli/commands/info.py +++ b/src/argenta/_cli/commands/info.py @@ -31,6 +31,6 @@ def info_handler() -> None: table.add_row("Platform", f"{platform.system()} {platform.release()} ({platform.machine()})") table.add_row("Docs", "https://argenta.readthedocs.io") - console.print(f"[bold red]{text2art("Argenta", font='tarty1')}[/bold red]") + console.print(f"[bold red]{text2art('Argenta', font='tarty1')}[/bold red]") console.print(Padding(table, pad=(2, 5))) console.print(Padding("[i]made with ❤ by [b]kolo[/b][/i]", pad=(0, 17))) diff --git a/src/argenta/_cli/commands/init.py b/src/argenta/_cli/commands/init.py index 0406eb2..5a95bb6 100644 --- a/src/argenta/_cli/commands/init.py +++ b/src/argenta/_cli/commands/init.py @@ -3,83 +3,15 @@ __all__ = ["init_handler"] from pathlib import Path from typing import Literal -GITIGNORE_CONTENT = """ -__pycache__/ -*.py[cod] -.env -.venv/ -env/ -""" - -FLAT_MAIN_TEMPLATE = """ -from argenta import Orchestrator, App - -from handlers import router - - -def main(): - app = App() - app.include_router(router) - - orchestrator = Orchestrator() - orchestrator.run_repl(app) - -if __name__ == "__main__": - main() -""" - -FLAT_HANDLERS_TEMPLATE = """ -from argenta import Router, Response - -router = Router("Hello command") - -@router.command("hello") -def start_handler(response: Response): - print("Hello world!") -""" - -SRC_MAIN_TEMPLATE = """ -from argenta import Orchestrator, App - -from .routers import router - - -def main(): - app = App() - app.include_router(router) - - orchestrator = Orchestrator() - orchestrator.run_repl(app) - -if __name__ == "__main__": - main() -""" - -SRC_ROUTERS_TEMPLATE = """ -from argenta import Router -from .handlers.hello_world_handler import hello_handler - -router = Router() - -router.command('hello')(hello_handler) -""" - -SRC_HANDLER_TEMPLATE = """ -from argenta import Response - - -def hello_handler(response: Response) -> None: - print("Hello world!") - -""" - - -def create_file(path: Path, content: str) -> None: - if not path.exists(): - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content.strip(), encoding="utf-8") - else: - print(f"Skipped: {path} (already exists)") +from ._templates import ( + FLAT_HANDLERS_TEMPLATE, + FLAT_MAIN_TEMPLATE, + GITIGNORE_CONTENT, + SRC_HANDLER_TEMPLATE, + SRC_MAIN_TEMPLATE, + SRC_ROUTERS_TEMPLATE, + create_file, +) def init_handler(with_arch: Literal["flat", "src"] = "flat") -> None: diff --git a/src/argenta/_cli/commands/new.py b/src/argenta/_cli/commands/new.py index b5ced3c..c8132ee 100644 --- a/src/argenta/_cli/commands/new.py +++ b/src/argenta/_cli/commands/new.py @@ -4,83 +4,15 @@ import sys from pathlib import Path from typing import Literal - -GITIGNORE_CONTENT = """ -__pycache__/ -*.py[cod] -.env -.venv/ -env/ -""" - -FLAT_MAIN_TEMPLATE = """ -from argenta import Orchestrator, App - -from handlers import router - - -def main(): - app = App() - app.include_router(router) - - orchestrator = Orchestrator() - orchestrator.run_repl(app) - -if __name__ == "__main__": - main() -""" - -FLAT_HANDLERS_TEMPLATE = """ -from argenta import Router, Response - -router = Router("Hello command") - -@router.command("hello") -def start_handler(response: Response): - print("Hello world!") -""" - -SRC_MAIN_TEMPLATE = """ -from argenta import Orchestrator, App - -from .routers import router - - -def main(): - app = App() - app.include_router(router) - - orchestrator = Orchestrator() - orchestrator.run_repl(app) - -if __name__ == "__main__": - main() -""" - -SRC_ROUTERS_TEMPLATE = """ -from argenta import Router -from .handlers.hello_world_handler import hello_handler - -router = Router() - -router.command('hello')(hello_handler) -""" - -SRC_HANDLER_TEMPLATE = """ -from argenta import Response - - -def hello_handler(response: Response) -> None: - print("Hello world!") -""" - - -def create_file(path: Path, content: str) -> None: - if not path.exists(): - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content.strip(), encoding="utf-8") - else: - print(f"Skipped: {path} (already exists)") +from ._templates import ( + FLAT_HANDLERS_TEMPLATE, + FLAT_MAIN_TEMPLATE, + GITIGNORE_CONTENT, + SRC_HANDLER_TEMPLATE, + SRC_MAIN_TEMPLATE, + SRC_ROUTERS_TEMPLATE, + create_file, +) def new_handler(project_name: str, with_arch: Literal["flat", "src"] = "flat") -> None: @@ -88,7 +20,7 @@ def new_handler(project_name: str, with_arch: Literal["flat", "src"] = "flat") - if base_dir.exists(): print(f"Error: Directory '{project_name}' already exists.") - sys.exit(1) + raise SystemExit(1) base_dir.mkdir(parents=True) print(f"Initialized project directory: {base_dir}") diff --git a/src/argenta/_cli/commands/routes.py b/src/argenta/_cli/commands/routes.py index 39122c1..3835f55 100644 --- a/src/argenta/_cli/commands/routes.py +++ b/src/argenta/_cli/commands/routes.py @@ -6,25 +6,53 @@ from rich.console import Console from rich.panel import Panel from rich.tree import Tree +from argenta.app.models import App + from ..infrastructure.entrypoint_resolver.entity import ( + CallableEntryPoint, EntryPointAsApp, EntrypointResolver, +) +from ..infrastructure.entrypoint_resolver.exceptions import ( + EntrypointError, + EntrypointNotAppInstanceError, ResolveFromStringError, ) def routes_handler(entrypoint_path: str) -> None: - entrypoint_path, _, entrypoint_callable_name = entrypoint_path.partition(":") - if not entrypoint_callable_name: - raise ResolveFromStringError( - "Path to callable object that run orchestrator repl must be in the format :" + file_path, _, callable_name = entrypoint_path.partition(":") + if not callable_name: + Console().print( + f'[bold red]Error:[/bold red] "{entrypoint_path}" must be in format ' + f'":" or ":"' ) + raise SystemExit(1) - app_instance = EntrypointResolver[EntryPointAsApp](entrypoint_path).parse_entrypoint_with_type( - entrypoint_callable_name - ) - - app = app_instance.instance_object + try: + app_instance = EntrypointResolver[EntryPointAsApp](file_path).parse_entrypoint_with_type( + callable_name + ) + app = app_instance.instance_object + except EntrypointNotAppInstanceError: + try: + callable_entrypoint = EntrypointResolver[CallableEntryPoint]( + file_path + ).parse_entrypoint_with_type( + callable_name + ) + except (ResolveFromStringError, EntrypointError) as e: + Console().print(f"[bold red]Error:[/bold red] {e}") + raise SystemExit(1) + app = callable_entrypoint.instance_object() + if not isinstance(app, App): + Console().print( + f"[bold red]Error:[/bold red] callable must return an App instance, got {type(app).__name__}" + ) + raise SystemExit(1) + except (ResolveFromStringError, EntrypointError) as e: + Console().print(f"[bold red]Error:[/bold red] {e}") + raise SystemExit(1) routers = app.registered_routers console = Console() diff --git a/src/argenta/_cli/commands/run.py b/src/argenta/_cli/commands/run.py index b9183b1..dc35508 100644 --- a/src/argenta/_cli/commands/run.py +++ b/src/argenta/_cli/commands/run.py @@ -2,23 +2,33 @@ __all__ = ["run_handler"] import os +from rich.console import Console + from ..infrastructure.entrypoint_resolver.entity import ( CallableEntryPoint, EntrypointResolver, +) +from ..infrastructure.entrypoint_resolver.exceptions import ( + EntrypointError, ResolveFromStringError, ) def run_handler(entrypoint_path: str) -> None: os.environ["RUN_FROM_ARGENTA_RUNNER"] = "1" - entrypoint_path, _, entrypoint_callable_name = entrypoint_path.partition(":") - if not entrypoint_callable_name: - raise ResolveFromStringError( - "Path to callable object that run orchestrator repl must be in the format : or :" + file_path, _, callable_name = entrypoint_path.partition(":") + if not callable_name: + Console().print( + f'[bold red]Error:[/bold red] "{entrypoint_path}" must be in format ' + f'":" or ":"' ) + raise SystemExit(1) - runner = EntrypointResolver[CallableEntryPoint](entrypoint_path).parse_entrypoint_with_type( - entrypoint_callable_name - ) - - runner.instance_object() + try: + runner = EntrypointResolver[CallableEntryPoint](file_path).parse_entrypoint_with_type( + callable_name + ) + runner.instance_object() + except (ResolveFromStringError, EntrypointError) as e: + Console().print(f"[bold red]Error:[/bold red] {e}") + raise SystemExit(1)