cli module better

This commit is contained in:
2026-03-15 18:06:45 +03:00
parent 7ffc6cd987
commit a3d7630219
4 changed files with 74 additions and 67 deletions
+25 -6
View File
@@ -1,14 +1,33 @@
from typer import Typer from typer import Typer
from .commands import run_handler, init_handler, new_handler from .commands import init_handler, new_handler, run_handler
def main() -> None: def main() -> None:
app = Typer() app = Typer()
app.command("run", help='Command to start the orchestrator repl; the path to the callable object is required')(run_handler) app.command(
app.command("init", help="Creates a flat/src boilerplate architecture in an existing project")(init_handler) "run",
app.command("new", help="Creates a project and in it flat/src boilerplate architecture")(new_handler) 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() app()
if __name__ == '__main__':
main() if __name__ == "__main__":
main()
+2 -7
View File
@@ -1,8 +1,6 @@
__all__ = ["run_handler"] __all__ = ["run_handler"]
import os import os
from pathlib import Path
import sys
from ..infrastructure.entrypoint_resolver.entity import ( from ..infrastructure.entrypoint_resolver.entity import (
CallableEntryPoint, CallableEntryPoint,
@@ -18,12 +16,9 @@ def run_handler(entrypoint_path: str) -> None:
raise ResolveFromStringError( raise ResolveFromStringError(
"Path to callable object that run orchestrator repl must be in the format <path/to/file.py>:<object_name>" "Path to callable object that run orchestrator repl must be in the format <path/to/file.py>:<object_name>"
) )
if str(Path.cwd()) not in sys.path:
sys.path.insert(0, str(Path.cwd()))
runner = EntrypointResolver(entrypoint_path).parse_entrypoint_with_type( runner = EntrypointResolver[CallableEntryPoint](entrypoint_path).parse_entrypoint_with_type(
entrypoint_callable_name, CallableEntryPoint entrypoint_callable_name
) )
runner.instance_object() runner.instance_object()
@@ -1,10 +1,10 @@
__all__ = ['EntrypointResolver', 'EntryPointAsApp', 'CallableEntryPoint'] __all__ = ['EntrypointResolver', 'EntryPointAsApp', 'CallableEntryPoint']
import importlib.util
import inspect import inspect
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Callable, Protocol, cast, overload import sys
from typing import Callable, Protocol, cast, get_args
from argenta.app.models import App from argenta.app.models import App
@@ -35,28 +35,18 @@ class EntryPointAsApp:
instance_object: App instance_object: App
class EntrypointResolver: class EntrypointResolver[T: (CallableEntryPoint, EntryPointAsApp)]:
def __init__(self, path_to_entrypoint: str): def __init__(self, path_to_entrypoint: str):
self._path_to_entrypoint = path_to_entrypoint self._path_to_entrypoint = path_to_entrypoint
@overload
def parse_entrypoint_with_type( def parse_entrypoint_with_type(
self, entrypoint_object_name: str, entrypoint_type: type[CallableEntryPoint] self, entrypoint_object_name: str,
) -> EntryPoint[Callable[[], None]]: ... ) -> T:
@overload entrypoint_type: type[T] = get_args(self.__orig_class__)[0] # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
def parse_entrypoint_with_type(
self, entrypoint_object_name: str, entrypoint_type: type[EntryPointAsApp]
) -> EntryPoint[App]: ...
def parse_entrypoint_with_type(
self,
entrypoint_object_name: str,
entrypoint_type: type[CallableEntryPoint] | type[EntryPointAsApp],
) -> EntryPoint[Callable[[], None]] | EntryPoint[App]:
if entrypoint_type is CallableEntryPoint: if entrypoint_type is CallableEntryPoint:
return self._parse_callable_entrypoint(entrypoint_object_name) return cast(T, self._parse_callable_entrypoint(entrypoint_object_name))
elif entrypoint_type is EntryPointAsApp: elif entrypoint_type is EntryPointAsApp:
return self._parse_entrypoint_as_app(entrypoint_object_name) return cast(T, self._parse_entrypoint_as_app(entrypoint_object_name))
raise NotImplementedError raise NotImplementedError
def _parse_callable_entrypoint(self, entrypoint_object_name: str) -> CallableEntryPoint: def _parse_callable_entrypoint(self, entrypoint_object_name: str) -> CallableEntryPoint:
@@ -82,23 +72,31 @@ class EntrypointResolver:
return EntryPointAsApp(raw_path=resolved_entrypoint[0], instance_object=instance_object) return EntryPointAsApp(raw_path=resolved_entrypoint[0], instance_object=instance_object)
def _resolve_from_string(self, entrypoint_object_name: str) -> tuple[str, object]: def _resolve_from_string(self, entrypoint_object_name: str) -> tuple[str, object]:
file_path: str = self._path_to_entrypoint abs_path = Path(self._path_to_entrypoint).resolve()
attr_name: str = entrypoint_object_name if not abs_path.exists():
raise ResolveFromStringError(f'File "{self._path_to_entrypoint}" not found')
path = Path(file_path).resolve()
if not path.exists(): package_root = abs_path.parent
raise ResolveFromStringError(f'File "{file_path}" not found') while (package_root / "__init__.py").exists():
package_root = package_root.parent
spec = importlib.util.spec_from_file_location(path.stem, path)
if spec is None or spec.loader is None: pkg_root_str = str(package_root)
raise ResolveFromStringError(f'Cannot load module from "{file_path}"') if pkg_root_str not in sys.path:
sys.path.insert(0, pkg_root_str)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) module_name = ".".join(abs_path.relative_to(package_root).with_suffix("").parts)
try: try:
instance = getattr(module, attr_name) module = importlib.import_module(module_name)
except ImportError as e:
raise ResolveFromStringError(f'Cannot import module "{module_name}": {e}')
try:
instance = getattr(module, entrypoint_object_name)
except AttributeError: except AttributeError:
raise ResolveFromStringError(f'"{attr_name}" not found in "{file_path}"') raise ResolveFromStringError(
f'"{entrypoint_object_name}" not found in "{self._path_to_entrypoint}"'
)
return str(abs_path), instance
return file_path, instance
Generated
+14 -19
View File
@@ -55,10 +55,15 @@ cli = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "cairosvg" },
{ name = "esbonio" }, { name = "esbonio" },
{ name = "isort" }, { name = "isort" },
{ name = "matplotlib" },
{ name = "mypy" }, { name = "mypy" },
{ name = "psutil" },
{ name = "py-cpuinfo" },
{ name = "pyfakefs" }, { name = "pyfakefs" },
{ name = "pygal" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pytest-mock" }, { name = "pytest-mock" },
@@ -77,20 +82,17 @@ docs = [
{ name = "sphinx-autobuild" }, { name = "sphinx-autobuild" },
{ name = "sphinx-intl" }, { name = "sphinx-intl" },
] ]
in-test = [
{ name = "cairosvg" },
{ name = "plotext" },
{ name = "pygal" },
]
linters = [ linters = [
{ name = "isort" }, { name = "isort" },
{ name = "ruff" }, { name = "ruff" },
{ name = "wemake-python-styleguide" }, { name = "wemake-python-styleguide" },
] ]
metrics = [ metrics = [
{ name = "cairosvg" },
{ name = "matplotlib" }, { name = "matplotlib" },
{ name = "psutil" }, { name = "psutil" },
{ name = "py-cpuinfo" }, { name = "py-cpuinfo" },
{ name = "pygal" },
] ]
tests = [ tests = [
{ name = "pyfakefs" }, { name = "pyfakefs" },
@@ -114,10 +116,15 @@ provides-extras = ["cli"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "esbonio", specifier = ">=1.0.0" }, { name = "esbonio", specifier = ">=1.0.0" },
{ name = "isort", specifier = ">=7.0.0" }, { name = "isort", specifier = ">=7.0.0" },
{ name = "matplotlib", specifier = ">=3.10.8" },
{ name = "mypy", specifier = ">=1.14.1" }, { name = "mypy", specifier = ">=1.14.1" },
{ name = "psutil", specifier = ">=7.2.1" },
{ name = "py-cpuinfo", specifier = ">=9.0.0" },
{ name = "pyfakefs", specifier = ">=5.5.0" }, { name = "pyfakefs", specifier = ">=5.5.0" },
{ name = "pygal", specifier = ">=3.1.0" },
{ name = "pytest", specifier = ">=8.3.2" }, { name = "pytest", specifier = ">=8.3.2" },
{ name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pytest-mock", specifier = ">=3.15.1" },
@@ -136,20 +143,17 @@ docs = [
{ name = "sphinx-autobuild", specifier = ">=2025.8.25" }, { name = "sphinx-autobuild", specifier = ">=2025.8.25" },
{ name = "sphinx-intl", specifier = ">=2.3.2" }, { name = "sphinx-intl", specifier = ">=2.3.2" },
] ]
in-test = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "plotext", specifier = ">=5.3.2" },
{ name = "pygal", specifier = ">=3.1.0" },
]
linters = [ linters = [
{ name = "isort", specifier = ">=7.0.0" }, { name = "isort", specifier = ">=7.0.0" },
{ name = "ruff", specifier = ">=0.12.12" }, { name = "ruff", specifier = ">=0.12.12" },
{ name = "wemake-python-styleguide", specifier = ">=0.17.0" }, { name = "wemake-python-styleguide", specifier = ">=0.17.0" },
] ]
metrics = [ metrics = [
{ name = "cairosvg", specifier = ">=2.8.2" },
{ name = "matplotlib", specifier = ">=3.10.8" }, { name = "matplotlib", specifier = ">=3.10.8" },
{ name = "psutil", specifier = ">=7.2.1" }, { name = "psutil", specifier = ">=7.2.1" },
{ name = "py-cpuinfo", specifier = ">=9.0.0" }, { name = "py-cpuinfo", specifier = ">=9.0.0" },
{ name = "pygal", specifier = ">=3.1.0" },
] ]
tests = [ tests = [
{ name = "pyfakefs", specifier = ">=5.5.0" }, { name = "pyfakefs", specifier = ">=5.5.0" },
@@ -1142,15 +1146,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
] ]
[[package]]
name = "plotext"
version = "5.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/d7/f75f397af966fe252d0d34ffd3cae765317fce2134f925f95e7d6725d1ce/plotext-5.3.2.tar.gz", hash = "sha256:52d1e932e67c177bf357a3f0fe6ce14d1a96f7f7d5679d7b455b929df517068e", size = 61967, upload-time = "2024-09-24T15:13:37.728Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/1e/12fe7c40cd2099a1f454518754ed229b01beaf3bbb343127f0cc13ce6c22/plotext-5.3.2-py3-none-any.whl", hash = "sha256:394362349c1ddbf319548cfac17ca65e6d5dfc03200c40dfdc0503b3e95a2283", size = 64047, upload-time = "2024-09-24T15:13:36.296Z" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"