mirror of
https://github.com/koloideal/Argenta.git
synced 2026-06-10 01:55:29 +03:00
@@ -1,5 +1,7 @@
|
||||
#### joe made this: http://goel.io/joe
|
||||
|
||||
metrics/reports/diagrams
|
||||
|
||||
#### python ####
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
<a id='changelog-1.2.0'></a>
|
||||
## 1.2.0 — 2026-02-07
|
||||
|
||||
### Added
|
||||
|
||||
- 100% coverage of the code base with tests
|
||||
- 100% coverage with typhints
|
||||
- 100% coverage of public API documentation in two languages - Russian and English
|
||||
- cli attributes: highlighting valid commands, redesigned input history with auto-completion, interactive autocomplete selection menu for multiple candidates
|
||||
- a metrics module that allows you to test the performance of various library units
|
||||
- implementing a dependency injection pattern through an ioc container
|
||||
- implementation of a context object for transferring data between handlers within a session
|
||||
- adding a changelog
|
||||
|
||||
### Changed
|
||||
|
||||
- increased performance by several times (there will be real numbers in the next releases)
|
||||
- reworking the internal API, highlighting different layers and reducing connectivity
|
||||
- reworking the README and adding a translation for it
|
||||
@@ -1,13 +1,17 @@
|
||||
from argenta import App, Orchestrator
|
||||
from argenta.orchestrator.argparser import ArgParser, BooleanArgument, ValueArgument
|
||||
from argenta.orchestrator.argparser import ArgParser, BooleanArgument
|
||||
|
||||
arg_parser = ArgParser(processed_args=[BooleanArgument("dev"), ValueArgument('some', possible_values=['fuck', 'cruck'])])
|
||||
arg_parser = ArgParser(
|
||||
processed_args=[
|
||||
BooleanArgument("dev")
|
||||
]
|
||||
)
|
||||
orchestrator = Orchestrator(
|
||||
arg_parser=arg_parser,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if arg_parser.parsed_argspace.get_by_name('dev'):
|
||||
orchestrator.start_polling(App(initial_message='ArgentaDev'))
|
||||
if arg_parser.parsed_argspace.get_by_name("dev"):
|
||||
orchestrator.start_polling(App(initial_message="ArgentaDev"))
|
||||
else:
|
||||
orchestrator.start_polling(App())
|
||||
|
||||
@@ -6,11 +6,7 @@ arguments = [
|
||||
ValueArgument("port", help="Server port", is_required=True),
|
||||
]
|
||||
|
||||
argparser = ArgParser(
|
||||
processed_args=arguments,
|
||||
name="WebServer",
|
||||
description="Simple web server"
|
||||
)
|
||||
argparser = ArgParser(processed_args=arguments, name="WebServer", description="Simple web server")
|
||||
|
||||
app = App()
|
||||
orchestrator = Orchestrator(argparser)
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
config_arg = argspace.get_by_name("config")
|
||||
if config_arg:
|
||||
print(f"Config path: {config_arg.value}")
|
||||
from argenta import Response, Router
|
||||
from argenta.di import FromDishka
|
||||
from argenta.orchestrator.argparser import ArgSpace
|
||||
|
||||
verbose_arg = argspace.get_by_name("verbose")
|
||||
if verbose_arg and verbose_arg.value:
|
||||
print("Verbose mode enabled")
|
||||
router = Router()
|
||||
|
||||
unknown_arg = argspace.get_by_name("nonexistent")
|
||||
if unknown_arg is None:
|
||||
print("Argument not found")
|
||||
|
||||
@router.command("get_args")
|
||||
def get_args(response: Response, argspace: FromDishka[ArgSpace]):
|
||||
config_arg = argspace.get_by_name("config")
|
||||
if config_arg:
|
||||
print(f"Config path: {config_arg.value}")
|
||||
|
||||
verbose_arg = argspace.get_by_name("verbose")
|
||||
if verbose_arg and verbose_arg.value:
|
||||
print("Verbose mode enabled")
|
||||
|
||||
unknown_arg = argspace.get_by_name("nonexistent")
|
||||
if unknown_arg is None:
|
||||
print("Argument not found")
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
from argenta.orchestrator.argparser import ArgParser, ValueArgument
|
||||
|
||||
# Create arguments
|
||||
config_arg = ValueArgument(
|
||||
"config",
|
||||
help="Path to configuration file",
|
||||
default="config.yaml"
|
||||
)
|
||||
config_arg = ValueArgument("config", help="Path to configuration file", default="config.yaml")
|
||||
|
||||
log_level_arg = ValueArgument(
|
||||
"log-level",
|
||||
help="Logging level",
|
||||
possible_values=["DEBUG", "INFO", "WARNING", "ERROR"],
|
||||
default="INFO"
|
||||
default="INFO",
|
||||
)
|
||||
|
||||
host_arg = ValueArgument(
|
||||
"host",
|
||||
help="Server host address",
|
||||
is_required=True
|
||||
)
|
||||
host_arg = ValueArgument("host", help="Server host address", is_required=True)
|
||||
|
||||
# Register in ArgParser
|
||||
parser = ArgParser(
|
||||
processed_args=[config_arg, log_level_arg, host_arg],
|
||||
name="MyApp",
|
||||
description="My application with CLI arguments"
|
||||
description="My application with CLI arguments",
|
||||
)
|
||||
@@ -1,23 +1,9 @@
|
||||
from argenta.orchestrator.argparser import ArgParser, BooleanArgument
|
||||
|
||||
# Create boolean arguments
|
||||
verbose_arg = BooleanArgument(
|
||||
"verbose",
|
||||
help="Enable verbose output"
|
||||
)
|
||||
|
||||
debug_arg = BooleanArgument(
|
||||
"debug",
|
||||
help="Enable debug mode"
|
||||
)
|
||||
|
||||
no_cache_arg = BooleanArgument(
|
||||
"no-cache",
|
||||
help="Disable caching"
|
||||
)
|
||||
verbose_arg = BooleanArgument("verbose", help="Enable verbose output")
|
||||
debug_arg = BooleanArgument("debug", help="Enable debug mode")
|
||||
no_cache_arg = BooleanArgument("no-cache", help="Disable caching")
|
||||
|
||||
# Register in ArgParser
|
||||
parser = ArgParser(
|
||||
processed_args=[verbose_arg, debug_arg, no_cache_arg],
|
||||
name="MyApp"
|
||||
)
|
||||
parser = ArgParser(processed_args=[verbose_arg, debug_arg, no_cache_arg], name="MyApp")
|
||||
@@ -2,10 +2,13 @@ from argenta import Router, Command, Response
|
||||
|
||||
router = Router(title="System")
|
||||
|
||||
@router.command(Command(
|
||||
"shutdown",
|
||||
description="Shutdown the system",
|
||||
aliases=["poweroff", "halt", "stop"]
|
||||
))
|
||||
|
||||
@router.command(
|
||||
Command(
|
||||
"shutdown",
|
||||
description="Shutdown the system",
|
||||
aliases=["poweroff", "halt", "stop"]
|
||||
)
|
||||
)
|
||||
def handle_shutdown(response: Response):
|
||||
print("Shutting down the system...")
|
||||
@@ -3,7 +3,7 @@ from argenta.command import Flag
|
||||
verbose_flag = Flag(name="verbose", prefix="--")
|
||||
short_flag = Flag(name="v", prefix="-")
|
||||
|
||||
# Debug view
|
||||
# Debug presentation
|
||||
print(repr(verbose_flag)) # Flag<prefix=--, name=verbose>
|
||||
print(repr(short_flag)) # Flag<prefix=-, name=v>
|
||||
|
||||
|
||||
@@ -5,10 +5,7 @@ from argenta.command.flag import ValidationStatus
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.command(Command(
|
||||
"deploy",
|
||||
flags=Flag("verbose", possible_values=PossibleValues.NEITHER)
|
||||
))
|
||||
@router.command(Command("deploy", flags=Flag("verbose", possible_values=PossibleValues.NEITHER)))
|
||||
def deploy_handler(response: Response):
|
||||
# Check for toggle flag presence
|
||||
verbose_flag = response.input_flags.get_flag_by_name("verbose")
|
||||
|
||||
@@ -8,10 +8,7 @@ router = Router(title="Example")
|
||||
Command(
|
||||
"example",
|
||||
description="Example command with flags",
|
||||
flags=Flags([
|
||||
Flag("name"),
|
||||
Flag("age")
|
||||
]),
|
||||
flags=Flags([Flag("name"), Flag("age")]),
|
||||
)
|
||||
)
|
||||
def example_handler(response: Response):
|
||||
|
||||
@@ -8,11 +8,7 @@ router = Router(title="Get Flag Example")
|
||||
Command(
|
||||
"config",
|
||||
description="Configure settings",
|
||||
flags=Flags([
|
||||
Flag("host"),
|
||||
Flag("port"),
|
||||
Flag("debug")
|
||||
]),
|
||||
flags=Flags([Flag("host"), Flag("port"), Flag("debug")]),
|
||||
)
|
||||
)
|
||||
def config_handler(response: Response):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from argenta import Command, Response, Router
|
||||
from argenta.command.flag import InputFlag, InputFlags, ValidationStatus
|
||||
from argenta.command.flag import InputFlag, ValidationStatus
|
||||
from argenta.command import InputFlags
|
||||
|
||||
router = Router(title="Add Flag Example")
|
||||
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
from argenta.command.flag import InputFlag, InputFlags, ValidationStatus
|
||||
from argenta.command.flag import InputFlag, ValidationStatus
|
||||
from argenta.command import InputFlags
|
||||
|
||||
# Create InputFlags collection
|
||||
flags = InputFlags()
|
||||
|
||||
# Create several flags
|
||||
flag1 = InputFlag(
|
||||
name="option1", prefix="--", input_value="value1", status=ValidationStatus.VALID
|
||||
)
|
||||
flag1 = InputFlag(name="option1", prefix="--", input_value="value1", status=ValidationStatus.VALID)
|
||||
|
||||
flag2 = InputFlag(
|
||||
name="option2", prefix="--", input_value="value2", status=ValidationStatus.VALID
|
||||
)
|
||||
flag2 = InputFlag(name="option2", prefix="--", input_value="value2", status=ValidationStatus.VALID)
|
||||
|
||||
flag3 = InputFlag(
|
||||
name="option3", prefix="---", input_value="value3", status=ValidationStatus.VALID
|
||||
)
|
||||
flag3 = InputFlag(name="option3", prefix="---", input_value="value3", status=ValidationStatus.VALID)
|
||||
|
||||
# Add all flags in one call
|
||||
flags.add_flags([flag1, flag2, flag3])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from argenta.command.flag import InputFlag, ValidationStatus
|
||||
from argenta.command.flag.flags.models import InputFlags
|
||||
from argenta.command import InputFlags
|
||||
|
||||
# Create first collection
|
||||
flags1 = InputFlags(
|
||||
@@ -26,12 +26,8 @@ flags3 = InputFlags(
|
||||
)
|
||||
|
||||
print(f"flags1 == flags2: {flags1 == flags2}") # True (same names)
|
||||
print(
|
||||
f"flags1 == flags3: {flags1 == flags3}"
|
||||
) # True (same names, values are not considered)
|
||||
print(f"flags1 == flags3: {flags1 == flags3}") # True (same names, values are not considered)
|
||||
|
||||
# Different collections
|
||||
flags4 = InputFlags(
|
||||
[InputFlag(name="flag3", input_value="value3", status=ValidationStatus.VALID)]
|
||||
)
|
||||
flags4 = InputFlags([InputFlag(name="flag3", input_value="value3", status=ValidationStatus.VALID)])
|
||||
print(f"flags1 == flags4: {flags1 == flags4}") # False (different flags)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from metrics.benchmarks.entity import benchmarks
|
||||
|
||||
@benchmarks.register(
|
||||
type_="my_category",
|
||||
description="Description of what is being measured"
|
||||
)
|
||||
def benchmark_my_operation() -> None:
|
||||
# Code whose performance is being measured
|
||||
pass
|
||||
@@ -8,11 +8,8 @@ from argenta.response.status import ResponseStatus
|
||||
|
||||
router = Router("Calculator")
|
||||
|
||||
operations = {
|
||||
'mul': operator.mul,
|
||||
'sub': operator.sub,
|
||||
'add': operator.add
|
||||
}
|
||||
operations = {"mul": operator.mul, "sub": operator.sub, "add": operator.add}
|
||||
|
||||
|
||||
@router.command(
|
||||
Command(
|
||||
@@ -22,7 +19,9 @@ operations = {
|
||||
[
|
||||
Flag("a", possible_values=re.compile(r"^\d{,5}$")), # First number
|
||||
Flag("b", possible_values=re.compile(r"^\d{,5}$")), # Second number
|
||||
Flag("operation", possible_values=["add", "sub", "mul"]), # Operation: add, sub, mul
|
||||
Flag(
|
||||
"operation", possible_values=["add", "sub", "mul"]
|
||||
), # Operation: add, sub, mul
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ app = App(
|
||||
prompt=">> ",
|
||||
initial_message="Simple App",
|
||||
farewell_message="Goodbye!",
|
||||
repeat_command_groups_printing=False
|
||||
repeat_command_groups_printing=False,
|
||||
)
|
||||
orchestrator = Orchestrator()
|
||||
|
||||
@@ -15,11 +15,7 @@ main_router = Router(title="Main commands")
|
||||
|
||||
|
||||
# 3. Define command and its handler
|
||||
@main_router.command(Command(
|
||||
"hello",
|
||||
description="Prints greeting message",
|
||||
flags=Flag("name")
|
||||
))
|
||||
@main_router.command(Command("hello", description="Prints greeting message", flags=Flag("name")))
|
||||
def hello_handler(response: Response):
|
||||
"""This handler will be called for 'hello' command."""
|
||||
name = response.input_flags.get_flag_by_name("name")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from typing import cast
|
||||
|
||||
from argenta import Command, Response, Router
|
||||
from argenta.command.flag import Flag, Flags, ValidationStatus
|
||||
from argenta.command.flag import Flag, ValidationStatus
|
||||
from argenta.command import Flags
|
||||
from argenta.di import FromDishka
|
||||
|
||||
from .repository import Priority, Task, TaskRepository
|
||||
@@ -9,25 +10,29 @@ from .repository import Priority, Task, TaskRepository
|
||||
router = Router(title="Task Manager")
|
||||
|
||||
|
||||
@router.command(Command(
|
||||
@router.command(
|
||||
Command(
|
||||
"add-task",
|
||||
description="Add a new task",
|
||||
flags=Flags([
|
||||
flags=Flags(
|
||||
[
|
||||
Flag("description"),
|
||||
Flag("priority", possible_values=["low", "medium", "high"]),
|
||||
]),
|
||||
))
|
||||
]
|
||||
),
|
||||
)
|
||||
)
|
||||
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.status == ValidationStatus.VALID:
|
||||
print("Error: --description flag is required.")
|
||||
return
|
||||
|
||||
|
||||
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:
|
||||
@@ -37,14 +42,14 @@ def add_task(response: Response, repo: FromDishka[TaskRepository]):
|
||||
|
||||
task = Task(description=task_description, priority=priority)
|
||||
repo.add_task(task)
|
||||
|
||||
|
||||
print(f"Added task: '{task.description}' with priority '{task.priority}'")
|
||||
|
||||
|
||||
@router.command(Command("list-tasks", description="List all tasks"))
|
||||
def list_tasks(response: Response, repo: FromDishka[TaskRepository]):
|
||||
tasks = repo.get_all_tasks()
|
||||
|
||||
|
||||
if not tasks:
|
||||
print("No tasks found.")
|
||||
return
|
||||
|
||||
@@ -4,6 +4,7 @@ from argenta.di import FromDishka
|
||||
|
||||
router = Router(title="Authentication")
|
||||
|
||||
|
||||
def authenticate_user(username: str) -> str:
|
||||
return f"token_for_{username}"
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
from argenta import Command, Response, Router
|
||||
|
||||
router = Router(title="Data Example")
|
||||
|
||||
|
||||
@router.command(Command("set", description="Set data"))
|
||||
def set_handler(response: Response):
|
||||
# Update global data storage
|
||||
response.update_data(
|
||||
{
|
||||
"user_name": "John",
|
||||
"timestamp": "2024-01-01",
|
||||
"settings": {"theme": "dark", "language": "ru"},
|
||||
}
|
||||
)
|
||||
print("Data updated successfully")
|
||||
|
||||
|
||||
@router.command(Command("show", description="Show data"))
|
||||
def show_handler(response: Response):
|
||||
# Get data from global storage
|
||||
data = response.get_data()
|
||||
if "user_name" in data:
|
||||
print(f"User: {data['user_name']}")
|
||||
print(f"Settings: {data.get('settings', {})}")
|
||||
@@ -1,16 +0,0 @@
|
||||
from argenta import Command, Response, Router
|
||||
|
||||
router = Router(title="Get Data Example")
|
||||
|
||||
|
||||
@router.command(Command("info", description="Show all stored data"))
|
||||
def info_handler(response: Response):
|
||||
# Get all data from global storage
|
||||
all_data = response.get_data()
|
||||
|
||||
if all_data:
|
||||
print("Stored data:")
|
||||
for key, value in all_data.items():
|
||||
print(f" {key}: {value}")
|
||||
else:
|
||||
print("No data stored")
|
||||
@@ -1,19 +0,0 @@
|
||||
from argenta import Command, Response, Router
|
||||
|
||||
router = Router(title="Clear Data Example")
|
||||
|
||||
|
||||
@router.command(Command("clear", description="Clear all stored data"))
|
||||
def clear_handler(response: Response):
|
||||
# Clear all data storage
|
||||
response.clear_data()
|
||||
print("All data cleared")
|
||||
|
||||
|
||||
@router.command(Command("check", description="Check if data exists"))
|
||||
def check_handler(response: Response):
|
||||
data = response.get_data()
|
||||
if data:
|
||||
print(f"Storage contains {len(data)} item(s)")
|
||||
else:
|
||||
print("Storage is empty")
|
||||
@@ -1,29 +0,0 @@
|
||||
from argenta import Command, Response, Router
|
||||
|
||||
router = Router(title="Delete Data Example")
|
||||
|
||||
|
||||
@router.command(Command("store", description="Store data"))
|
||||
def store_handler(response: Response):
|
||||
response.update_data(
|
||||
{
|
||||
"temp_key": "temporary value",
|
||||
"important_key": "important value",
|
||||
"another_key": "another value",
|
||||
}
|
||||
)
|
||||
print("Data stored")
|
||||
|
||||
|
||||
@router.command(Command("remove", description="Remove specific key"))
|
||||
def remove_handler(response: Response):
|
||||
# Delete specific key from storage
|
||||
try:
|
||||
response.delete_from_data("temp_key")
|
||||
print("Key 'temp_key' deleted")
|
||||
|
||||
# Check what remains
|
||||
remaining = response.get_data()
|
||||
print(f"Remaining keys: {list(remaining.keys())}")
|
||||
except KeyError:
|
||||
print("Key not found")
|
||||
@@ -10,10 +10,7 @@ router = Router(title="Flags Example")
|
||||
Command(
|
||||
"process",
|
||||
description="Process with flags",
|
||||
flags=Flags([
|
||||
Flag("format", possible_values=["json", "xml"]),
|
||||
Flag("verbose")
|
||||
]),
|
||||
flags=Flags([Flag("format", possible_values=["json", "xml"]), Flag("verbose")]),
|
||||
)
|
||||
)
|
||||
def process_handler(response: Response):
|
||||
|
||||
@@ -8,22 +8,21 @@ from argenta import App, Orchestrator, Router, Command, Response
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patched_argv():
|
||||
with patch.object(sys, 'argv', ['program.py']):
|
||||
with patch.object(sys, "argv", ["program.py"]):
|
||||
yield
|
||||
|
||||
|
||||
def test_input_incorrect_command(capsys: CaptureFixture[str]):
|
||||
router = Router()
|
||||
orchestrator = Orchestrator()
|
||||
|
||||
@router.command(Command('test'))
|
||||
@router.command(Command("test"))
|
||||
def test(response: Response) -> None:
|
||||
print('test command')
|
||||
print("test command")
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
app.set_unknown_command_handler(
|
||||
lambda command: print(f'Unknown command: {command.trigger}')
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -17,5 +17,5 @@ def test_simple_app() -> None:
|
||||
|
||||
with redirect_stdout(io.StringIO()) as stdout:
|
||||
router.finds_appropriate_handler(InputCommand.parse("HELP"))
|
||||
|
||||
|
||||
assert "Available commands:" in stdout.getvalue()
|
||||
|
||||
@@ -11,18 +11,20 @@ from argenta.di.integration import setup_dishka, FromDishka
|
||||
class Service:
|
||||
def hello(self) -> str:
|
||||
return "world"
|
||||
|
||||
|
||||
|
||||
def get_service() -> Service:
|
||||
return Service()
|
||||
|
||||
|
||||
|
||||
router = Router(title="DI")
|
||||
|
||||
|
||||
@router.command("HELLO")
|
||||
def hello(response: Response, service: FromDishka[Service]) -> None:
|
||||
print(f"hello {service.hello()}")
|
||||
|
||||
|
||||
|
||||
|
||||
class _FakeApp:
|
||||
# Minimal stub for setup_dishka; app object is not used in unit tests
|
||||
registered_routers = [router]
|
||||
@@ -31,12 +33,12 @@ class _FakeApp:
|
||||
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'))
|
||||
router.finds_appropriate_handler(InputCommand.parse("HELLO"))
|
||||
|
||||
assert "hello world" in stdout.getvalue()
|
||||
|
||||
@@ -7,6 +7,7 @@ from argenta.command import InputCommand
|
||||
|
||||
router = Router(title="Demo")
|
||||
|
||||
|
||||
@router.command(Command("PING", description="Ping command"))
|
||||
def ping(response: Response):
|
||||
print("PONG")
|
||||
|
||||
@@ -64,6 +64,7 @@ Argenta предназначена для создания приложений,
|
||||
|
||||
root/contributing
|
||||
root/code_of_conduct
|
||||
root/metrics
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Argenta \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-13 21:50+0300\n"
|
||||
"POT-Creation-Date: 2026-02-06 23:44+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: en\n"
|
||||
@@ -29,32 +29,32 @@ msgid ""
|
||||
"взаимодействие с пользователем, координируя работу всех компонентов: "
|
||||
"роутеров, обработчиков и системных сообщений."
|
||||
msgstr ""
|
||||
"The ``App`` object is the core of your console application. It handles "
|
||||
"configuration, lifecycle management, command processing, and user "
|
||||
"interaction, coordinating the work of all components: routers, handlers, "
|
||||
"and system messages."
|
||||
"The ``App`` object is the implementations of your console application. It"
|
||||
" handles configuration, lifecycle management, command processing, and "
|
||||
"user interaction, coordinating the work of all components: routers, "
|
||||
"handlers, and system messages."
|
||||
|
||||
#: ../../root/api/app/index.rst:11
|
||||
msgid "Инициализация"
|
||||
msgstr "Initialization"
|
||||
|
||||
#: ../../root/api/app/index.rst:37
|
||||
#: ../../root/api/app/index.rst:31
|
||||
msgid "Создаёт и настраивает экземпляр приложения."
|
||||
msgstr "Creates and configures an application instance."
|
||||
|
||||
#: ../../root/api/app/index.rst:39
|
||||
#: ../../root/api/app/index.rst:33
|
||||
msgid "``prompt``: Приглашение к вводу, отображаемое перед каждой командой."
|
||||
msgstr "``prompt``: Input prompt displayed before each command."
|
||||
|
||||
#: ../../root/api/app/index.rst:40
|
||||
#: ../../root/api/app/index.rst:34
|
||||
msgid "``initial_message``: Сообщение, выводимое при запуске приложения."
|
||||
msgstr "``initial_message``: Message displayed when the application starts."
|
||||
|
||||
#: ../../root/api/app/index.rst:41
|
||||
#: ../../root/api/app/index.rst:35
|
||||
msgid "``farewell_message``: Сообщение, выводимое при выходе из приложения."
|
||||
msgstr "``farewell_message``: Message displayed when exiting the application."
|
||||
|
||||
#: ../../root/api/app/index.rst:42
|
||||
#: ../../root/api/app/index.rst:36
|
||||
msgid ""
|
||||
"``exit_command``: Команда, которая маркируется как триггер для выхода из "
|
||||
"приложения."
|
||||
@@ -62,7 +62,7 @@ msgstr ""
|
||||
"``exit_command``: Command that is marked as a trigger for exiting the "
|
||||
"application."
|
||||
|
||||
#: ../../root/api/app/index.rst:43
|
||||
#: ../../root/api/app/index.rst:37
|
||||
msgid ""
|
||||
"``system_router_title``: Заголовок для системного роутера (содержит "
|
||||
"команду выхода)."
|
||||
@@ -70,7 +70,7 @@ msgstr ""
|
||||
"``system_router_title``: Title for the system router (contains the exit "
|
||||
"command)."
|
||||
|
||||
#: ../../root/api/app/index.rst:44
|
||||
#: ../../root/api/app/index.rst:38
|
||||
msgid ""
|
||||
"``dividing_line``: Тип разделительной линии (``StaticDividingLine`` или "
|
||||
"``DynamicDividingLine``)."
|
||||
@@ -78,7 +78,7 @@ msgstr ""
|
||||
"``dividing_line``: Type of dividing line (``StaticDividingLine`` or "
|
||||
"``DynamicDividingLine``)."
|
||||
|
||||
#: ../../root/api/app/index.rst:45
|
||||
#: ../../root/api/app/index.rst:39
|
||||
msgid ""
|
||||
"``repeat_command_groups_printing``: Если ``True``, список доступных "
|
||||
"команд выводится перед каждым вводом."
|
||||
@@ -86,7 +86,7 @@ msgstr ""
|
||||
"``repeat_command_groups_printing``: If ``True``, the list of available "
|
||||
"commands is displayed before each input."
|
||||
|
||||
#: ../../root/api/app/index.rst:46
|
||||
#: ../../root/api/app/index.rst:40
|
||||
msgid ""
|
||||
"``override_system_messages``: Если ``True``, стандартное форматирование "
|
||||
"(цвета, ASCII-арт) отключается."
|
||||
@@ -94,7 +94,7 @@ msgstr ""
|
||||
"``override_system_messages``: If ``True``, standard formatting (colors, "
|
||||
"ASCII art) is disabled."
|
||||
|
||||
#: ../../root/api/app/index.rst:47
|
||||
#: ../../root/api/app/index.rst:41
|
||||
msgid ""
|
||||
"``autocompleter``: Экземпляр класса :ref:`AutoCompleter "
|
||||
"<root_api_app_autocompleter>`, отвечающий за автодополнение команд."
|
||||
@@ -103,29 +103,28 @@ msgstr ""
|
||||
"<root_api_app_autocompleter>` class responsible for command "
|
||||
"autocompletion."
|
||||
|
||||
#: ../../root/api/app/index.rst:48
|
||||
msgid ""
|
||||
"``print_func``: Функция для вывода всех системных сообщений (по умолчанию"
|
||||
" ``rich.Console().print``)."
|
||||
#: ../../root/api/app/index.rst:42
|
||||
#, fuzzy
|
||||
msgid "``printer``: Функция для вывода всех системных сообщений."
|
||||
msgstr ""
|
||||
"``print_func``: Function for outputting all system messages (defaults to "
|
||||
"``rich.Console().print``)."
|
||||
|
||||
#: ../../root/api/app/index.rst:53
|
||||
#: ../../root/api/app/index.rst:47
|
||||
msgid ""
|
||||
"В приложениях на Argenta регистр вводимых команд не важен, проверка на "
|
||||
"существование и роутинг команд производится на основании триггеров, "
|
||||
"приведённых к нижнему регистру."
|
||||
msgstr ""
|
||||
"In applications on Argenta, the case of the entered commands is not important, checking for the "
|
||||
" existence and routing of commands is performed based on triggers "
|
||||
"reduced to lowercase."
|
||||
"In applications on Argenta, the case of the entered commands is not "
|
||||
"important, checking for the existence and routing of commands is "
|
||||
"performed based on triggers reduced to lowercase."
|
||||
|
||||
#: ../../root/api/app/index.rst:56
|
||||
#: ../../root/api/app/index.rst:50
|
||||
msgid "Основные методы"
|
||||
msgstr "Main Methods"
|
||||
|
||||
#: ../../root/api/app/index.rst:60
|
||||
#: ../../root/api/app/index.rst:54
|
||||
msgid ""
|
||||
"Регистрирует роутер в приложении. Все команды из этого роутера становятся"
|
||||
" доступными для вызова."
|
||||
@@ -137,19 +136,19 @@ msgstr ""
|
||||
msgid "Parameters"
|
||||
msgstr "Parameters"
|
||||
|
||||
#: ../../root/api/app/index.rst:62
|
||||
#: ../../root/api/app/index.rst:56
|
||||
msgid "Экземпляр ``Router`` для регистрации."
|
||||
msgstr "``Router`` instance to register."
|
||||
|
||||
#: ../../root/api/app/index.rst:66
|
||||
#: ../../root/api/app/index.rst:60
|
||||
msgid "Регистрирует несколько роутеров одновременно."
|
||||
msgstr "Registers multiple routers simultaneously."
|
||||
|
||||
#: ../../root/api/app/index.rst:68
|
||||
#: ../../root/api/app/index.rst:62
|
||||
msgid "Последовательность экземпляров ``Router`` для регистрации."
|
||||
msgstr "Sequence of ``Router`` instances to register."
|
||||
|
||||
#: ../../root/api/app/index.rst:72
|
||||
#: ../../root/api/app/index.rst:66
|
||||
msgid ""
|
||||
"Добавляет текстовое сообщение, которое выводится при запуске приложения "
|
||||
"после ``initial_message``."
|
||||
@@ -157,11 +156,11 @@ msgstr ""
|
||||
"Adds a text message that is displayed when the application starts after "
|
||||
"``initial_message``."
|
||||
|
||||
#: ../../root/api/app/index.rst:74
|
||||
#: ../../root/api/app/index.rst:68
|
||||
msgid "Строка с сообщением."
|
||||
msgstr "String with the message."
|
||||
|
||||
#: ../../root/api/app/index.rst:77
|
||||
#: ../../root/api/app/index.rst:71
|
||||
msgid ""
|
||||
"Для вывода стандартных сообщений можно использовать готовые шаблоны из "
|
||||
":ref:`PredefinedMessages <root_api_predefined_messages>`."
|
||||
@@ -169,11 +168,11 @@ msgstr ""
|
||||
"For outputting standard messages, you can use ready-made templates from "
|
||||
":ref:`PredefinedMessages <root_api_predefined_messages>`."
|
||||
|
||||
#: ../../root/api/app/index.rst:82
|
||||
#: ../../root/api/app/index.rst:76
|
||||
msgid "Методы установки обработчиков"
|
||||
msgstr "Handler Setup Methods"
|
||||
|
||||
#: ../../root/api/app/index.rst:84
|
||||
#: ../../root/api/app/index.rst:78
|
||||
msgid ""
|
||||
"``App`` позволяет настраивать реакцию на различные события, такие как "
|
||||
"ошибки ввода или неизвестные команды."
|
||||
@@ -181,7 +180,7 @@ msgstr ""
|
||||
"``App`` allows you to configure responses to various events, such as "
|
||||
"input errors or unknown commands."
|
||||
|
||||
#: ../../root/api/app/index.rst:87
|
||||
#: ../../root/api/app/index.rst:81
|
||||
msgid ""
|
||||
"Подробнее об исключениях и их обработке в соответствующем :ref:`разделе "
|
||||
"документации <root_error_handling>`."
|
||||
@@ -189,59 +188,59 @@ msgstr ""
|
||||
"For more details on exceptions and their handling, see the corresponding "
|
||||
":ref:`documentation section <root_error_handling>`."
|
||||
|
||||
#: ../../root/api/app/index.rst:93
|
||||
#: ../../root/api/app/index.rst:87
|
||||
msgid "Устанавливает шаблон для форматирования описания команды."
|
||||
msgstr "Sets the template for formatting command descriptions."
|
||||
|
||||
#: ../../root/api/app/index.rst:95
|
||||
#: ../../root/api/app/index.rst:89
|
||||
msgid "Обработчик принимает триггер команды (``str``) и её описание (``str``)."
|
||||
msgstr ""
|
||||
"The handler accepts the command trigger (``str``) and its description "
|
||||
"(``str``)."
|
||||
|
||||
#: ../../root/api/app/index.rst:101
|
||||
#: ../../root/api/app/index.rst:95
|
||||
msgid "Устанавливает обработчик при некорректном введённом синтаксисе флагов."
|
||||
msgstr "Sets the handler for incorrect flag syntax input."
|
||||
|
||||
#: ../../root/api/app/index.rst:103 ../../root/api/app/index.rst:111
|
||||
#: ../../root/api/app/index.rst:97 ../../root/api/app/index.rst:105
|
||||
msgid "Обработчик принимает строку, введённую пользователем."
|
||||
msgstr "The handler accepts the string entered by the user."
|
||||
|
||||
#: ../../root/api/app/index.rst:109
|
||||
#: ../../root/api/app/index.rst:103
|
||||
msgid "Устанавливает обработчик при повторяющихся флагах в введённой команде."
|
||||
msgstr "Sets the handler for duplicate flags in the entered command."
|
||||
|
||||
#: ../../root/api/app/index.rst:117
|
||||
#: ../../root/api/app/index.rst:111
|
||||
msgid "Устанавливает обработчик при вводе неизвестной команды."
|
||||
msgstr "Sets the handler for entering an unknown command."
|
||||
|
||||
#: ../../root/api/app/index.rst:119
|
||||
#: ../../root/api/app/index.rst:113
|
||||
msgid "Обработчик принимает объект ``InputCommand`` - объект введённой команды."
|
||||
msgstr ""
|
||||
"The handler accepts an ``InputCommand`` object - the entered command "
|
||||
"object."
|
||||
|
||||
#: ../../root/api/app/index.rst:125
|
||||
#: ../../root/api/app/index.rst:119
|
||||
msgid "Устанавливает обработчик при вводе пустой строки."
|
||||
msgstr "Sets the handler for entering an empty string."
|
||||
|
||||
#: ../../root/api/app/index.rst:127
|
||||
#: ../../root/api/app/index.rst:121
|
||||
msgid "Обработчик не принимает аргументов."
|
||||
msgstr "The handler accepts no arguments."
|
||||
|
||||
#: ../../root/api/app/index.rst:133
|
||||
#: ../../root/api/app/index.rst:127
|
||||
msgid "Переопределяет стандартное поведение при вызове команды выхода."
|
||||
msgstr "Overrides the default behavior when the exit command is invoked."
|
||||
|
||||
#: ../../root/api/app/index.rst:135
|
||||
#: ../../root/api/app/index.rst:129
|
||||
msgid "Обработчик принимает объект ``Response``."
|
||||
msgstr "The handler accepts a ``Response`` object."
|
||||
|
||||
#: ../../root/api/app/index.rst:148
|
||||
#: ../../root/api/app/index.rst:142
|
||||
msgid "PredefinedMessages"
|
||||
msgstr "PredefinedMessages"
|
||||
|
||||
#: ../../root/api/app/index.rst:150
|
||||
#: ../../root/api/app/index.rst:144
|
||||
msgid ""
|
||||
"``PredefinedMessages`` — это контейнер, содержащий набор готовых к "
|
||||
"использованию сообщений. Они отформатированы с использованием синтаксиса "
|
||||
@@ -252,31 +251,31 @@ msgstr ""
|
||||
"messages. They are formatted using ``rich`` syntax and are intended for "
|
||||
"displaying standard information, such as usage hints."
|
||||
|
||||
#: ../../root/api/app/index.rst:152
|
||||
#: ../../root/api/app/index.rst:146
|
||||
msgid "Рекомендуется использовать их при старте приложения."
|
||||
msgstr "It is recommended to use them when starting the application."
|
||||
|
||||
#: ../../root/api/app/index.rst:179
|
||||
#: ../../root/api/app/index.rst:173
|
||||
msgid "Строка: ``[b dim]Usage[/b dim]: [i]<command> <[green]flags[/green]>[/i]``"
|
||||
msgstr "String: ``[b dim]Usage[/b dim]: [i]<command> <[green]flags[/green]>[/i]``"
|
||||
|
||||
#: ../../root/api/app/index.rst:181
|
||||
#: ../../root/api/app/index.rst:175
|
||||
msgid "Отображается как: ``Usage: <command> <flags>``"
|
||||
msgstr "Displayed as: ``Usage: <command> <flags>``"
|
||||
|
||||
#: ../../root/api/app/index.rst:185
|
||||
#: ../../root/api/app/index.rst:179
|
||||
msgid "Строка: ``[b dim]Help[/b dim]: [i]<command>[/i] [b red]--help[/b red]``"
|
||||
msgstr "String: ``[b dim]Help[/b dim]: [i]<command>[/i] [b red]--help[/b red]``"
|
||||
|
||||
#: ../../root/api/app/index.rst:187
|
||||
#: ../../root/api/app/index.rst:181
|
||||
msgid "Отображается как: ``Help: <command> --help``"
|
||||
msgstr "Displayed as: ``Help: <command> --help``"
|
||||
|
||||
#: ../../root/api/app/index.rst:191
|
||||
#: ../../root/api/app/index.rst:185
|
||||
msgid "Строка: ``[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>``"
|
||||
msgstr "String: ``[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>``"
|
||||
|
||||
#: ../../root/api/app/index.rst:193
|
||||
#: ../../root/api/app/index.rst:187
|
||||
msgid "Отображается как: ``Autocomplete: <part> <tab>``"
|
||||
msgstr "Displayed as: ``Autocomplete: <part> <tab>``"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Argenta \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-02 22:29+0300\n"
|
||||
"POT-Creation-Date: 2026-02-06 23:44+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: en\n"
|
||||
@@ -30,10 +30,11 @@ msgid ""
|
||||
"позволяет получать внешнюю конфигурацию в момент старта (например, путь к"
|
||||
" файлу настроек, флаги отладки или режим запуска)."
|
||||
msgstr ""
|
||||
"``ArgParser`` is designed for processing **command-line arguments** passed to the "
|
||||
"application at startup. It's important not to confuse them with flags that the user "
|
||||
"enters in interactive mode. ``ArgParser`` allows receiving external configuration at "
|
||||
"startup (e.g., path to settings file, debug flags, or launch mode)."
|
||||
"``ArgParser`` is designed for processing **command-line arguments** "
|
||||
"passed to the application at startup. It's important not to confuse them "
|
||||
"with flags that the user enters in interactive mode. ``ArgParser`` allows"
|
||||
" receiving external configuration at startup (e.g., path to settings "
|
||||
"file, debug flags, or launch mode)."
|
||||
|
||||
#: ../../root/api/orchestrator/argparser.rst:11
|
||||
msgid "Инициализация"
|
||||
@@ -81,8 +82,9 @@ msgid ""
|
||||
"экземпляр ``ArgParser``, атрибут ``parsed_argspace`` будет содержать "
|
||||
"пустой ``ArgSpace``."
|
||||
msgstr ""
|
||||
"Before initializing ``Orchestrator``, to whose constructor an ``ArgParser`` instance "
|
||||
"was passed, the ``parsed_argspace`` attribute will contain an empty ``ArgSpace``."
|
||||
"Before initializing ``Orchestrator``, to whose constructor an "
|
||||
"``ArgParser`` instance was passed, the ``parsed_argspace`` attribute will"
|
||||
" contain an empty ``ArgSpace``."
|
||||
|
||||
#: ../../root/api/orchestrator/argparser.rst:40
|
||||
msgid ""
|
||||
@@ -90,8 +92,9 @@ msgid ""
|
||||
"``Orchestrator``, поэтому использовать ``parsed_argspace`` "
|
||||
"**целесообразно только после** этого."
|
||||
msgstr ""
|
||||
"Parsing and validation of arguments occur during ``Orchestrator`` initialization, "
|
||||
"so using ``parsed_argspace`` is **advisable only after** that."
|
||||
"Parsing and validation of arguments occur during ``Orchestrator`` "
|
||||
"initialization, so using ``parsed_argspace`` is **advisable only after** "
|
||||
"that."
|
||||
|
||||
#: ../../root/api/orchestrator/argparser.rst:45
|
||||
msgid "Лучшие практики"
|
||||
@@ -104,9 +107,10 @@ msgid ""
|
||||
"``ArgSpace`` через DI. Подробнее см. :ref:`здесь "
|
||||
"<root_dependency_injection>`."
|
||||
msgstr ""
|
||||
"Using the ``parsed_argspace`` attribute is recommended only during the application "
|
||||
"setup phase. In handlers, the best practice is to obtain ``ArgSpace`` through DI. "
|
||||
"For more details, see :ref:`here <root_dependency_injection>`."
|
||||
"Using the ``parsed_argspace`` attribute is recommended only during the "
|
||||
"application setup phase. In handlers, the best practice is to obtain "
|
||||
"``ArgSpace`` through DI. For more details, see :ref:`here "
|
||||
"<root_dependency_injection>`."
|
||||
|
||||
#: ../../root/api/orchestrator/argparser.rst:49
|
||||
msgid "**Пример использования:**"
|
||||
@@ -129,8 +133,8 @@ msgid ""
|
||||
"При работе с аргументами командной строки стандартный ``ArgumentParser`` "
|
||||
"автоматически обрабатывает следующие ситуации:"
|
||||
msgstr ""
|
||||
"When working with command-line arguments, the standard ``ArgumentParser`` "
|
||||
"automatically handles the following situations:"
|
||||
"When working with command-line arguments, the standard ``ArgumentParser``"
|
||||
" automatically handles the following situations:"
|
||||
|
||||
#: ../../root/api/orchestrator/argparser.rst:63
|
||||
msgid "**Отсутствие обязательного аргумента:**"
|
||||
@@ -149,6 +153,12 @@ msgid ""
|
||||
"При использовании аргумента с ``is_deprecated=True`` выводится "
|
||||
"предупреждение, но выполнение продолжается:"
|
||||
msgstr ""
|
||||
"When using an argument with ``is_deprecated=True``, a warning is displayed, "
|
||||
"but execution continues:"
|
||||
"When using an argument with ``is_deprecated=True``, a warning is "
|
||||
"displayed, but execution continues:"
|
||||
|
||||
#: ../../root/api/orchestrator/argparser.rst:90
|
||||
msgid ""
|
||||
"Параметр поддерживается начиная с версии CPython 3.13, если версия ниже, "
|
||||
"то параметр будет игнорироваться."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -579,7 +579,7 @@ msgstr ""
|
||||
msgid ""
|
||||
"Откройте `127.0.0.1:8000` в браузере, чтобы просмотреть сгенерированную "
|
||||
"документацию."
|
||||
msgstr "Open `127.0.0.1:8000` in your browser to view the generated documentation."
|
||||
msgstr "Open `127.0.0.1:8000` in your browser to presentation the generated documentation."
|
||||
|
||||
#: ../../root/contributing.rst:233
|
||||
msgid ""
|
||||
|
||||
@@ -109,7 +109,7 @@ msgstr "How Does It Work?"
|
||||
|
||||
#: ../../root/dependency_injection.rst:51
|
||||
msgid "В основе DI в Argenta лежат **провайдеры** и **контейнер**."
|
||||
msgstr "At the core of DI in Argenta are **providers** and a **container**."
|
||||
msgstr "At the implementations of DI in Argenta are **providers** and a **container**."
|
||||
|
||||
#: ../../root/dependency_injection.rst:53
|
||||
msgid ""
|
||||
|
||||
@@ -49,7 +49,7 @@ msgstr ""
|
||||
"The main purpose of flags is to provide a way to change the command's "
|
||||
"logic without reworking it. A command can operate in several modes: "
|
||||
"standard, verbose, debug, or simplified. Flags switch these modes on user"
|
||||
" demand, keeping the core functionality unchanged."
|
||||
" demand, keeping the implementations functionality unchanged."
|
||||
|
||||
#: ../../root/flags.rst:17
|
||||
msgid "Опциональность и удобство"
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) 2025, kolo
|
||||
# This file is distributed under the same license as the Argenta package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Argenta \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-06 23:44+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: en\n"
|
||||
"Language-Team: en <LL@li.org>\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/metrics.rst:2
|
||||
msgid "Метрики"
|
||||
msgstr "Metrics"
|
||||
|
||||
#: ../../root/metrics.rst:4
|
||||
msgid ""
|
||||
"Система метрик ``Argenta`` предоставляет инструменты для измерения "
|
||||
"производительности ключевых компонентов библиотеки. Это позволяет "
|
||||
"отслеживать регрессию/прогрессию производительности между релизами и "
|
||||
"оптимизировать критические участки кода."
|
||||
msgstr ""
|
||||
"The ``Argenta`` metrics system provides tools for measuring the performance "
|
||||
"of key library components. This allows tracking performance regression/progression "
|
||||
"between releases and optimizing critical code sections."
|
||||
|
||||
#: ../../root/metrics.rst:9
|
||||
msgid "Запуск метрик"
|
||||
msgstr "Running Metrics"
|
||||
|
||||
#: ../../root/metrics.rst:11
|
||||
msgid ""
|
||||
"Для работы с метриками необходимо склонировать репозиторий и установить "
|
||||
"зависимости:"
|
||||
msgstr ""
|
||||
"To work with metrics, you need to clone the repository and install "
|
||||
"dependencies:"
|
||||
|
||||
#: ../../root/metrics.rst:19
|
||||
msgid "Запуск системы метрик:"
|
||||
msgstr "Running the metrics system:"
|
||||
|
||||
#: ../../root/metrics.rst:25
|
||||
msgid ""
|
||||
"После запуска откроется интерактивная сессия с доступными командами для "
|
||||
"работы с бенчмарками."
|
||||
msgstr ""
|
||||
"After launch, an interactive session will open with available commands for "
|
||||
"working with benchmarks."
|
||||
|
||||
#: ../../root/metrics.rst:30
|
||||
msgid "Доступные команды"
|
||||
msgstr "Available Commands"
|
||||
|
||||
#: ../../root/metrics.rst:33
|
||||
msgid "run-all"
|
||||
msgstr "run-all"
|
||||
|
||||
#: ../../root/metrics.rst:35
|
||||
msgid ""
|
||||
"Запускает все зарегистрированные бенчмарки и выводит результаты в виде "
|
||||
"таблиц."
|
||||
msgstr ""
|
||||
"Runs all registered benchmarks and outputs results as tables."
|
||||
|
||||
#: ../../root/metrics.rst:37 ../../root/metrics.rst:55
|
||||
#: ../../root/metrics.rst:78 ../../root/metrics.rst:97
|
||||
#: ../../root/metrics.rst:117
|
||||
msgid "**Синтаксис:**"
|
||||
msgstr "**Syntax:**"
|
||||
|
||||
#: ../../root/metrics.rst:43 ../../root/metrics.rst:84
|
||||
#: ../../root/metrics.rst:103
|
||||
msgid "**Флаги:**"
|
||||
msgstr "**Flags:**"
|
||||
|
||||
#: ../../root/metrics.rst:45
|
||||
msgid ""
|
||||
"``--without-gc`` — отключает сборщик мусора во время выполнения "
|
||||
"бенчмарков для более стабильных результатов"
|
||||
msgstr ""
|
||||
"``--without-gc`` — disables garbage collector during benchmark execution "
|
||||
"for more stable results"
|
||||
|
||||
#: ../../root/metrics.rst:46
|
||||
msgid "``--without-system-info`` — скрывает информацию о системе в выводе"
|
||||
msgstr "``--without-system-info`` — hides system information in output"
|
||||
|
||||
#: ../../root/metrics.rst:51
|
||||
msgid "list-types"
|
||||
msgstr "list-types"
|
||||
|
||||
#: ../../root/metrics.rst:53
|
||||
msgid ""
|
||||
"Выводит список всех доступных типов бенчмарков с количеством тестов в "
|
||||
"каждой категории."
|
||||
msgstr ""
|
||||
"Displays a list of all available benchmark types with the number of tests "
|
||||
"in each category."
|
||||
|
||||
#: ../../root/metrics.rst:61
|
||||
msgid "**Пример вывода:**"
|
||||
msgstr "**Example output:**"
|
||||
|
||||
#: ../../root/metrics.rst:74
|
||||
msgid "run-type"
|
||||
msgstr "run-type"
|
||||
|
||||
#: ../../root/metrics.rst:76
|
||||
msgid "Запускает бенчмарки определённого типа."
|
||||
msgstr "Runs benchmarks of a specific type."
|
||||
|
||||
#: ../../root/metrics.rst:86
|
||||
msgid "``--type`` — тип бенчмарков для запуска (обязательный)"
|
||||
msgstr "``--type`` — benchmark type to run (required)"
|
||||
|
||||
#: ../../root/metrics.rst:87 ../../root/metrics.rst:106
|
||||
msgid "``--without-gc`` — отключает сборщик мусора"
|
||||
msgstr "``--without-gc`` — disables garbage collector"
|
||||
|
||||
#: ../../root/metrics.rst:88
|
||||
msgid "``--without-system-info`` — скрывает информацию о системе"
|
||||
msgstr "``--without-system-info`` — hides system information"
|
||||
|
||||
#: ../../root/metrics.rst:93
|
||||
msgid "diagrams-generate"
|
||||
msgstr "diagrams-generate"
|
||||
|
||||
#: ../../root/metrics.rst:95
|
||||
msgid ""
|
||||
"Генерирует визуальные диаграммы сравнения производительности для всех "
|
||||
"бенчмарков."
|
||||
msgstr ""
|
||||
"Generates visual performance comparison diagrams for all benchmarks."
|
||||
|
||||
#: ../../root/metrics.rst:105
|
||||
msgid ""
|
||||
"``--iterations`` — количество итераций для каждого бенчмарка (по "
|
||||
"умолчанию 100)"
|
||||
msgstr ""
|
||||
"``--iterations`` — number of iterations for each benchmark (default 100)"
|
||||
|
||||
#: ../../root/metrics.rst:108
|
||||
msgid ""
|
||||
"Диаграммы сохраняются в директорию "
|
||||
"``metrics/reports/diagrams/<timestamp>/``."
|
||||
msgstr ""
|
||||
"Diagrams are saved to the ``metrics/reports/diagrams/<timestamp>/`` directory."
|
||||
|
||||
#: ../../root/metrics.rst:113
|
||||
msgid "release-generate"
|
||||
msgstr "release-generate"
|
||||
|
||||
#: ../../root/metrics.rst:115
|
||||
msgid ""
|
||||
"Генерирует полный отчёт о производительности для текущей версии "
|
||||
"библиотеки. Используется при подготовке релизов."
|
||||
msgstr ""
|
||||
"Generates a complete performance report for the current library version. "
|
||||
"Used when preparing releases."
|
||||
|
||||
#: ../../root/metrics.rst:123
|
||||
msgid "Команда автоматически:"
|
||||
msgstr "The command automatically:"
|
||||
|
||||
#: ../../root/metrics.rst:125
|
||||
msgid "Определяет текущую версию библиотеки"
|
||||
msgstr "Determines the current library version"
|
||||
|
||||
#: ../../root/metrics.rst:126
|
||||
msgid "Запускает все бенчмарки с 1000 итераций и отключённым GC"
|
||||
msgstr "Runs all benchmarks with 1000 iterations and disabled GC"
|
||||
|
||||
#: ../../root/metrics.rst:127
|
||||
msgid "Генерирует JSON-отчёты и диаграммы сравнения"
|
||||
msgstr "Generates JSON reports and comparison diagrams"
|
||||
|
||||
#: ../../root/metrics.rst:128
|
||||
msgid "Сохраняет результаты в ``metrics/reports/releases/<version>/``"
|
||||
msgstr "Saves results to ``metrics/reports/releases/<version>/``"
|
||||
|
||||
#: ../../root/metrics.rst:133
|
||||
msgid "Интерпретация результатов"
|
||||
msgstr "Interpreting Results"
|
||||
|
||||
#: ../../root/metrics.rst:135
|
||||
msgid "Результаты бенчмарков включают следующие метрики:"
|
||||
msgstr "Benchmark results include the following metrics:"
|
||||
|
||||
#: ../../root/metrics.rst:137
|
||||
msgid "**Среднее время (mean)**"
|
||||
msgstr "**Mean time (mean)**"
|
||||
|
||||
#: ../../root/metrics.rst:138
|
||||
msgid ""
|
||||
"Среднее время выполнения операции. Основная метрика для сравнения "
|
||||
"производительности."
|
||||
msgstr ""
|
||||
"Average operation execution time. The primary metric for performance comparison."
|
||||
|
||||
#: ../../root/metrics.rst:140
|
||||
msgid "**Медиана (median)**"
|
||||
msgstr "**Median (median)**"
|
||||
|
||||
#: ../../root/metrics.rst:141
|
||||
msgid ""
|
||||
"Медианное значение времени выполнения. Менее чувствительна к выбросам, "
|
||||
"чем среднее."
|
||||
msgstr ""
|
||||
"Median execution time value. Less sensitive to outliers than the mean."
|
||||
|
||||
#: ../../root/metrics.rst:143
|
||||
msgid "**Стандартное отклонение (std)**"
|
||||
msgstr "**Standard deviation (std)**"
|
||||
|
||||
#: ../../root/metrics.rst:144
|
||||
msgid ""
|
||||
"Показывает стабильность измерений. Меньшее значение означает более "
|
||||
"предсказуемую производительность."
|
||||
msgstr ""
|
||||
"Shows measurement stability. A lower value means more predictable performance."
|
||||
|
||||
#: ../../root/metrics.rst:149
|
||||
msgid "Рекомендации по использованию"
|
||||
msgstr "Usage Recommendations"
|
||||
|
||||
#: ../../root/metrics.rst:151
|
||||
msgid "**Для оптимизации**"
|
||||
msgstr "**For optimization**"
|
||||
|
||||
#: ../../root/metrics.rst:152
|
||||
msgid ""
|
||||
"Используйте ``run-type`` для фокусировки на конкретной области и "
|
||||
"``--without-gc`` для более точных измерений."
|
||||
msgstr ""
|
||||
"Use ``run-type`` to focus on a specific area and ``--without-gc`` for more "
|
||||
"accurate measurements."
|
||||
|
||||
#: ../../root/metrics.rst:154
|
||||
msgid "**Для визуализации**"
|
||||
msgstr "**For visualization**"
|
||||
|
||||
#: ../../root/metrics.rst:155
|
||||
msgid ""
|
||||
"Команда ``diagrams-generate`` создаёт наглядные графики, удобные для "
|
||||
"презентаций и документации."
|
||||
msgstr ""
|
||||
"The ``diagrams-generate`` command creates clear charts suitable for "
|
||||
"presentations and documentation."
|
||||
|
||||
#: ../../root/metrics.rst:157
|
||||
msgid "**Для стабильных результатов**"
|
||||
msgstr "**For stable results**"
|
||||
|
||||
#: ../../root/metrics.rst:158
|
||||
msgid ""
|
||||
"Закройте ресурсоёмкие приложения, используйте флаг ``--without-gc`` и "
|
||||
"увеличивайте количество итераций через ``--iterations``."
|
||||
msgstr ""
|
||||
"Close resource-intensive applications, use the ``--without-gc`` flag, and "
|
||||
"increase the number of iterations via ``--iterations``."
|
||||
|
||||
#: ../../root/metrics.rst:163
|
||||
msgid "Добавление новых бенчмарков"
|
||||
msgstr "Adding New Benchmarks"
|
||||
|
||||
#: ../../root/metrics.rst:165
|
||||
msgid ""
|
||||
"Вы можете реализовать свои бенчмарки для тестирования специфичных юнитов "
|
||||
"библиотеки. Новые бенчмарки добавляются через декоратор "
|
||||
"``@benchmarks.register``:"
|
||||
msgstr ""
|
||||
"You can implement your own benchmarks to test specific library units. "
|
||||
"New benchmarks are added via the ``@benchmarks.register`` decorator:"
|
||||
|
||||
#: ../../root/metrics.rst:173
|
||||
msgid ""
|
||||
"Бенчмарк должен быть импортирован в ``metrics/benchmarks/__init__.py`` "
|
||||
"для автоматической регистрации."
|
||||
msgstr ""
|
||||
"The benchmark must be imported in ``metrics/benchmarks/__init__.py`` for "
|
||||
"automatic registration."
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Argenta \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-04 20:39+0300\n"
|
||||
"POT-Creation-Date: 2026-02-06 23:44+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: en\n"
|
||||
@@ -90,20 +90,20 @@ msgstr "Output Customization"
|
||||
#: ../../root/overriding_formatting.rst:32
|
||||
msgid ""
|
||||
"Для полной замены логики вывода текста в конструкторе ``App`` "
|
||||
"предусмотрен параметр ``print_func``."
|
||||
"предусмотрен параметр ``printer``."
|
||||
msgstr ""
|
||||
"For complete replacement of text output logic, the ``App`` constructor "
|
||||
"provides the ``print_func`` parameter."
|
||||
"provides the ``printer`` parameter."
|
||||
|
||||
#: ../../root/overriding_formatting.rst:34
|
||||
msgid ""
|
||||
"**print_func**: ``Callable[[str], None]`` Этот параметр позволяет "
|
||||
"передать любую вызываемую сущность (например, функцию), которая будет "
|
||||
"**printer**: ``Callable[[str], None]`` Этот параметр позволяет передать "
|
||||
"любую вызываемую сущность (например, функцию), которая будет "
|
||||
"использоваться для вывода всех системных сообщений. По умолчанию это "
|
||||
"``rich.console.Console().print``. Вы можете передать сюда свою функцию, "
|
||||
"чтобы, например, логировать вывод в файл или отправлять его по сети."
|
||||
msgstr ""
|
||||
"**print_func**: ``Callable[[str], None]`` This parameter allows passing "
|
||||
"**printer**: ``Callable[[str], None]`` This parameter allows passing "
|
||||
"any callable entity (for example, a function) that will be used to output"
|
||||
" all system messages. By default, this is "
|
||||
"``rich.console.Console().print``. You can pass your own function here to,"
|
||||
|
||||
+15
-21
@@ -10,29 +10,23 @@ App
|
||||
Инициализация
|
||||
-------------
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine
|
||||
DEFAULT_DIVIDING_LINE: StaticDividingLine = StaticDividingLine()
|
||||
|
||||
DEFAULT_PRINT_FUNC: Printer = Console().print
|
||||
DEFAULT_AUTOCOMPLETER: AutoCompleter = AutoCompleter()
|
||||
DEFAULT_EXIT_COMMAND: Command = Command("Q", description="Exit command")
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
def __init__(self, *, prompt: str = "What do you want to do?\n\n",
|
||||
initial_message: str = "Argenta\n",
|
||||
farewell_message: str = "\nSee you\n",
|
||||
exit_command: Command = DEFAULT_EXIT_COMMAND,
|
||||
system_router_title: str | None = "System points:",
|
||||
dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE,
|
||||
repeat_command_groups_printing: bool = False,
|
||||
override_system_messages: bool = False,
|
||||
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
|
||||
print_func: Printer = DEFAULT_PRINT_FUNC) -> None
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
prompt: str = ">>> ",
|
||||
initial_message: str = "Argenta",
|
||||
farewell_message: str = "See you",
|
||||
exit_command: Command = Command("q", description="Exit command"),
|
||||
system_router_title: str = "System points:",
|
||||
dividing_line: StaticDividingLine | DynamicDividingLine | None = None,
|
||||
repeat_command_groups_printing: bool = False,
|
||||
override_system_messages: bool = False,
|
||||
autocompleter: AutoCompleter | None = None,
|
||||
printer: Printer = Console().print,
|
||||
) -> None:
|
||||
|
||||
Создаёт и настраивает экземпляр приложения.
|
||||
|
||||
@@ -45,7 +39,7 @@ App
|
||||
* ``repeat_command_groups_printing``: Если ``True``, список доступных команд выводится перед каждым вводом.
|
||||
* ``override_system_messages``: Если ``True``, стандартное форматирование (цвета, ASCII-арт) отключается.
|
||||
* ``autocompleter``: Экземпляр класса :ref:`AutoCompleter <root_api_app_autocompleter>`, отвечающий за автодополнение команд.
|
||||
* ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.Console().print``).
|
||||
* ``printer``: Функция для вывода всех системных сообщений.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -84,3 +84,8 @@ ArgParser
|
||||
|
||||
$ python app.py --old-param value
|
||||
Warning: argument --old-param is deprecated
|
||||
|
||||
.. warning::
|
||||
|
||||
Параметр поддерживается начиная с версии CPython 3.13, если версия ниже, то параметр будет игнорироваться.
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
Метрики
|
||||
=======
|
||||
|
||||
Система метрик ``Argenta`` предоставляет инструменты для измерения производительности ключевых компонентов библиотеки. Это позволяет отслеживать регрессию/прогрессию производительности между релизами и оптимизировать критические участки кода.
|
||||
|
||||
-----
|
||||
|
||||
Запуск метрик
|
||||
-------------
|
||||
|
||||
Для работы с метриками необходимо склонировать репозиторий и установить зависимости:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/koloideal/Argenta.git
|
||||
cd Argenta
|
||||
uv sync --group metrics
|
||||
|
||||
Запуск системы метрик:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m metrics
|
||||
|
||||
После запуска откроется интерактивная сессия с доступными командами для работы с бенчмарками.
|
||||
|
||||
-----
|
||||
|
||||
Доступные команды
|
||||
-----------------
|
||||
|
||||
run-all
|
||||
~~~~~~~
|
||||
|
||||
Запускает все зарегистрированные бенчмарки и выводит результаты в виде таблиц.
|
||||
|
||||
**Синтаксис:**
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
run-all [--without-gc] [--without-system-info]
|
||||
|
||||
**Флаги:**
|
||||
|
||||
- ``--without-gc`` — отключает сборщик мусора во время выполнения бенчмарков для более стабильных результатов
|
||||
- ``--without-system-info`` — скрывает информацию о системе в выводе
|
||||
|
||||
-----
|
||||
|
||||
list-types
|
||||
~~~~~~~~~~
|
||||
|
||||
Выводит список всех доступных типов бенчмарков с количеством тестов в каждой категории.
|
||||
|
||||
**Синтаксис:**
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
list-types
|
||||
|
||||
**Пример вывода:**
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Available benchmark types:
|
||||
|
||||
• flag_validation (9 benchmarks)
|
||||
• input_command_parse (7 benchmarks)
|
||||
• finds_appropriate_handler (5 benchmarks)
|
||||
|
||||
-----
|
||||
|
||||
run-type
|
||||
~~~~~~~~
|
||||
|
||||
Запускает бенчмарки определённого типа.
|
||||
|
||||
**Синтаксис:**
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
run-type --type <type_name> [--without-gc] [--without-system-info]
|
||||
|
||||
**Флаги:**
|
||||
|
||||
- ``--type`` — тип бенчмарков для запуска (обязательный)
|
||||
- ``--without-gc`` — отключает сборщик мусора
|
||||
- ``--without-system-info`` — скрывает информацию о системе
|
||||
|
||||
-----
|
||||
|
||||
diagrams-generate
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Генерирует визуальные диаграммы сравнения производительности для всех бенчмарков.
|
||||
|
||||
**Синтаксис:**
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
diagrams-generate [--iterations <number>] [--without-gc]
|
||||
|
||||
**Флаги:**
|
||||
|
||||
- ``--iterations`` — количество итераций для каждого бенчмарка (по умолчанию 100)
|
||||
- ``--without-gc`` — отключает сборщик мусора
|
||||
|
||||
Диаграммы сохраняются в директорию ``metrics/reports/diagrams/<timestamp>/``.
|
||||
|
||||
-----
|
||||
|
||||
release-generate
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Генерирует полный отчёт о производительности для текущей версии библиотеки. Используется при подготовке релизов.
|
||||
|
||||
**Синтаксис:**
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
release-generate
|
||||
|
||||
Команда автоматически:
|
||||
|
||||
1. Определяет текущую версию библиотеки
|
||||
2. Запускает все бенчмарки с 1000 итераций и отключённым GC
|
||||
3. Генерирует JSON-отчёты и диаграммы сравнения
|
||||
4. Сохраняет результаты в ``metrics/reports/releases/<version>/``
|
||||
|
||||
-----
|
||||
|
||||
Интерпретация результатов
|
||||
-------------------------
|
||||
|
||||
Результаты бенчмарков включают следующие метрики:
|
||||
|
||||
**Среднее время (mean)**
|
||||
Среднее время выполнения операции. Основная метрика для сравнения производительности.
|
||||
|
||||
**Медиана (median)**
|
||||
Медианное значение времени выполнения. Менее чувствительна к выбросам, чем среднее.
|
||||
|
||||
**Стандартное отклонение (std)**
|
||||
Показывает стабильность измерений. Меньшее значение означает более предсказуемую производительность.
|
||||
|
||||
-----
|
||||
|
||||
Рекомендации по использованию
|
||||
------------------------------
|
||||
|
||||
**Для оптимизации**
|
||||
Используйте ``run-type`` для фокусировки на конкретной области и ``--without-gc`` для более точных измерений.
|
||||
|
||||
**Для визуализации**
|
||||
Команда ``diagrams-generate`` создаёт наглядные графики, удобные для презентаций и документации.
|
||||
|
||||
**Для стабильных результатов**
|
||||
Закройте ресурсоёмкие приложения, используйте флаг ``--without-gc`` и увеличивайте количество итераций через ``--iterations``.
|
||||
|
||||
-----
|
||||
|
||||
Добавление новых бенчмарков
|
||||
----------------------------
|
||||
|
||||
Вы можете реализовать свои бенчмарки для тестирования специфичных юнитов библиотеки. Новые бенчмарки добавляются через декоратор ``@benchmarks.register``:
|
||||
|
||||
.. literalinclude:: ../code_snippets/metrics/add_new_benchmark.py
|
||||
:language: python
|
||||
:linenos:
|
||||
|
||||
.. important::
|
||||
|
||||
Бенчмарк должен быть импортирован в ``metrics/benchmarks/__init__.py`` для автоматической регистрации.
|
||||
@@ -29,9 +29,9 @@
|
||||
Кастомизация вывода
|
||||
-------------------
|
||||
|
||||
Для полной замены логики вывода текста в конструкторе ``App`` предусмотрен параметр ``print_func``.
|
||||
Для полной замены логики вывода текста в конструкторе ``App`` предусмотрен параметр ``printer``.
|
||||
|
||||
* **print_func**: ``Callable[[str], None]``
|
||||
* **printer**: ``Callable[[str], None]``
|
||||
Этот параметр позволяет передать любую вызываемую сущность (например, функцию), которая будет использоваться для вывода всех системных сообщений. По умолчанию это ``rich.console.Console().print``. Вы можете передать сюда свою функцию, чтобы, например, логировать вывод в файл или отправлять его по сети.
|
||||
|
||||
.. important::
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .benchmarks import *
|
||||
+11
-41
@@ -1,48 +1,18 @@
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
import os
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from metrics.utils import run_benchmark, BenchmarkResult
|
||||
from .registry import Benchmarks, Benchmark
|
||||
from argenta import App, Orchestrator, Command
|
||||
from argenta.app import DynamicDividingLine
|
||||
from .handlers import router
|
||||
|
||||
|
||||
def main():
|
||||
console = Console()
|
||||
all_benchmarks: list[Benchmark] = Benchmarks.get_benchmarks()
|
||||
app = App(initial_message="metrics", exit_command=Command('exit', aliases=['quit']))
|
||||
orchestrator = Orchestrator()
|
||||
|
||||
workers = os.cpu_count() or 1
|
||||
with ProcessPoolExecutor(max_workers=workers) as executor:
|
||||
results = executor.map(run_benchmark, all_benchmarks)
|
||||
|
||||
type_paired_benchmarks: dict[str, list[BenchmarkResult]] = {}
|
||||
|
||||
for result in results:
|
||||
type_paired_benchmarks.setdefault(result.type_, []).append(result)
|
||||
|
||||
for type_, benchmarks in type_paired_benchmarks.items():
|
||||
header_text = Text(f"TYPE: {type_.upper()}", style="bold magenta")
|
||||
console.print(Panel(header_text, expand=False, border_style="magenta"))
|
||||
|
||||
table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
|
||||
table.add_column("Name", style="green")
|
||||
table.add_column("Description", style="dim")
|
||||
table.add_column("Iterations", justify="right")
|
||||
table.add_column("Avg Time (ms)", justify="right", style="bold yellow")
|
||||
|
||||
for benchmark in benchmarks:
|
||||
table.add_row(
|
||||
benchmark.name,
|
||||
benchmark.description,
|
||||
str(benchmark.iterations),
|
||||
str(benchmark.avg_time)
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
def main() -> None:
|
||||
app.include_router(router)
|
||||
app.set_description_message_pattern(
|
||||
lambda command, description: f'[bold cyan]▸[/bold cyan] [bold white]{command}[/bold white] [dim]│[/dim] [yellow italic]{description}[/yellow italic]'
|
||||
)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
from .pre_cycle_setup import *
|
||||
from .pre_cycle_setup import *
|
||||
from .most_similar_command import *
|
||||
from .finds_appropriate_handler import *
|
||||
from .validate_routers_for_collisions import *
|
||||
from .input_command_parse import *
|
||||
from .flag_validation import *
|
||||
@@ -0,0 +1,20 @@
|
||||
class BenchmarkNotFound(Exception):
|
||||
def __init__(self, benchmark_name: str):
|
||||
self.benchmark_name = benchmark_name
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Benchmark with name '{self.benchmark_name}' not found"
|
||||
|
||||
|
||||
class BenchmarksNotFound(Exception):
|
||||
def __init__(self, type_: str):
|
||||
self.type_ = type_
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Benchmarks with type '{self.type_}' not found"
|
||||
|
||||
class BenchmarksWithSameNameAlreadyExists(Exception):
|
||||
def __init__(self, benchmark_name: str):
|
||||
self.benchmark_name = benchmark_name
|
||||
def __str__(self) -> str:
|
||||
return f"Benchmarks with name '{self.benchmark_name}' already exists"
|
||||
@@ -0,0 +1,165 @@
|
||||
__all__ = [
|
||||
"Benchmark",
|
||||
"Benchmarks",
|
||||
"BenchmarkResult",
|
||||
"BenchmarkGroupResult"
|
||||
]
|
||||
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
from dataclasses import dataclass
|
||||
import time
|
||||
import gc
|
||||
import statistics
|
||||
from typing import Callable, override
|
||||
|
||||
from .exceptions import BenchmarkNotFound, BenchmarksNotFound, BenchmarksWithSameNameAlreadyExists
|
||||
|
||||
FuncForBenchmark = Callable[[], None]
|
||||
MILLISECONDS_IN_SECONDS = 1000
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BenchmarkResult:
|
||||
type_: str
|
||||
name: str
|
||||
description: str
|
||||
iterations: int
|
||||
is_gc_disabled: bool
|
||||
avg_time: float
|
||||
median_time: float
|
||||
std_dev: float
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BenchmarkGroupResult:
|
||||
type_: str
|
||||
iterations: int
|
||||
is_gc_disabled: bool
|
||||
benchmark_results: list[BenchmarkResult]
|
||||
|
||||
|
||||
class Benchmark:
|
||||
def __init__(
|
||||
self,
|
||||
func: FuncForBenchmark,
|
||||
*,
|
||||
type_: str,
|
||||
name: str,
|
||||
description: str
|
||||
) -> None:
|
||||
self.func = func
|
||||
self.type_ = type_
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
def single_run(self) -> float:
|
||||
with redirect_stdout(io.StringIO()):
|
||||
start = time.perf_counter()
|
||||
self.func()
|
||||
end = time.perf_counter()
|
||||
return (end - start) * MILLISECONDS_IN_SECONDS
|
||||
|
||||
def multiple_runs(self, iterations: int, is_gc_disabled: bool = False) -> tuple[float, ...]:
|
||||
run_attempts: list[float] = []
|
||||
if is_gc_disabled:
|
||||
was_gc_enabled = gc.isenabled()
|
||||
gc.disable()
|
||||
for _ in range(iterations):
|
||||
run_attempts.append(self.single_run())
|
||||
if was_gc_enabled:
|
||||
gc.enable()
|
||||
gc.collect()
|
||||
return tuple(run_attempts)
|
||||
else:
|
||||
for _ in range(iterations):
|
||||
run_attempts.append(self.single_run())
|
||||
return tuple(run_attempts)
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f'Benchmark<{self.type_=}, {self.name=}, {self.description=}>'
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f'benchmark {self.name} with type {self.type_}'
|
||||
|
||||
|
||||
class Benchmarks:
|
||||
def __init__(self, *benchmarks: Benchmark) -> None:
|
||||
self._benchmarks: list[Benchmark] = list(benchmarks)
|
||||
self._benchmarks_grouped_by_type: dict[str, list[Benchmark]] = {}
|
||||
self._benchmarks_paired_by_name: dict[str, Benchmark] = {}
|
||||
|
||||
def register(
|
||||
self,
|
||||
type_: str,
|
||||
description: str = ""
|
||||
) -> Callable[[FuncForBenchmark], FuncForBenchmark]:
|
||||
def decorator(func: FuncForBenchmark) -> FuncForBenchmark:
|
||||
benchmark = Benchmark(
|
||||
func,
|
||||
type_=type_,
|
||||
name=func.__name__,
|
||||
description=description or f'description for {func.__name__} with type {type_}',
|
||||
)
|
||||
if self._benchmarks_paired_by_name.get(func.__name__):
|
||||
raise BenchmarksWithSameNameAlreadyExists(func.__name__)
|
||||
|
||||
self._benchmarks_paired_by_name[func.__name__] = benchmark
|
||||
self._benchmarks.append(benchmark)
|
||||
self._benchmarks_grouped_by_type.setdefault(type_, []).append(benchmark)
|
||||
return func
|
||||
return decorator
|
||||
|
||||
def run_benchmark_by_name(self, name: str, iterations: int = 100, is_gc_disables: bool = False) -> BenchmarkResult:
|
||||
benchmark = self.get_benchmark_by_name(name)
|
||||
if not benchmark:
|
||||
raise BenchmarkNotFound(name)
|
||||
run_attempts: tuple[float, ...] = benchmark.multiple_runs(iterations, is_gc_disables)
|
||||
|
||||
avg = round(statistics.mean(run_attempts), 4)
|
||||
median = round(statistics.median(run_attempts), 4)
|
||||
std_dev = round(statistics.stdev(run_attempts) if len(run_attempts) > 1 else 0, 4)
|
||||
|
||||
return BenchmarkResult(
|
||||
type_=benchmark.type_,
|
||||
name=benchmark.name,
|
||||
description=benchmark.description,
|
||||
iterations=iterations,
|
||||
is_gc_disabled=is_gc_disables,
|
||||
avg_time=avg,
|
||||
median_time=median,
|
||||
std_dev=std_dev
|
||||
)
|
||||
|
||||
def run_benchmarks_by_type(self, type_: str, iterations: int = 100, is_gc_disabled: bool = False) -> BenchmarkGroupResult:
|
||||
benchmarks = self.get_benchmarks_by_type(type_)
|
||||
if not benchmarks:
|
||||
raise BenchmarksNotFound(type_)
|
||||
benchmark_results: list[BenchmarkResult] = []
|
||||
|
||||
for benchmark in benchmarks:
|
||||
benchmark_results.append(self.run_benchmark_by_name(benchmark.name, iterations, is_gc_disabled))
|
||||
|
||||
return BenchmarkGroupResult(
|
||||
type_=type_,
|
||||
iterations=iterations,
|
||||
is_gc_disabled=is_gc_disabled,
|
||||
benchmark_results=benchmark_results
|
||||
)
|
||||
|
||||
def run_benchmarks_grouped_by_type(self, iterations: int = 100, is_gc_disabled: bool = False) -> list[BenchmarkGroupResult]:
|
||||
results: list[BenchmarkGroupResult] = []
|
||||
for type_, benchmarks in self._benchmarks_grouped_by_type.items():
|
||||
results.append(self.run_benchmarks_by_type(type_, iterations, is_gc_disabled))
|
||||
return results
|
||||
|
||||
def get_benchmarks_by_type(self, type_: str) -> list[Benchmark]:
|
||||
return self._benchmarks_grouped_by_type.get(type_, [])
|
||||
|
||||
def get_benchmark_by_name(self, name: str) -> Benchmark | None:
|
||||
return self._benchmarks_paired_by_name.get(name)
|
||||
|
||||
def get_types(self) -> set[str]:
|
||||
return set(self._benchmarks_grouped_by_type.keys())
|
||||
@@ -0,0 +1,3 @@
|
||||
from .core.models import Benchmarks
|
||||
|
||||
benchmarks = Benchmarks()
|
||||
@@ -0,0 +1,80 @@
|
||||
__all__ = [
|
||||
"benchmark_simple_command",
|
||||
"benchmark_command_with_flags",
|
||||
"benchmark_many_commands",
|
||||
"benchmark_command_with_many_flags",
|
||||
"benchmark_extreme_router"
|
||||
]
|
||||
|
||||
from argenta.command.models import Command, InputCommand
|
||||
from argenta.command import Flag, Flags
|
||||
from argenta.response import Response
|
||||
from argenta.router import Router
|
||||
|
||||
from .entity import benchmarks
|
||||
|
||||
|
||||
@benchmarks.register(type_="finds_appropriate_handler", description="Simple command (no flags)")
|
||||
def benchmark_simple_command() -> None:
|
||||
router = Router()
|
||||
|
||||
@router.command(Command('test'))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
input_cmd = InputCommand.parse('test')
|
||||
router.finds_appropriate_handler(input_cmd)
|
||||
|
||||
|
||||
@benchmarks.register(type_="finds_appropriate_handler", description="Command with flags (3 flags)")
|
||||
def benchmark_command_with_flags() -> None:
|
||||
router = Router()
|
||||
|
||||
@router.command(Command('test', flags=Flags([Flag('a'), Flag('b'), Flag('c')])))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
input_cmd = InputCommand.parse('test -a -b -c')
|
||||
router.finds_appropriate_handler(input_cmd)
|
||||
|
||||
|
||||
@benchmarks.register(type_="finds_appropriate_handler", description="Many commands (50 commands)")
|
||||
def benchmark_many_commands() -> None:
|
||||
router = Router()
|
||||
|
||||
for i in range(50):
|
||||
@router.command(Command(f'cmd{i}'))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
input_cmd = InputCommand.parse('cmd25')
|
||||
router.finds_appropriate_handler(input_cmd)
|
||||
|
||||
|
||||
@benchmarks.register(type_="finds_appropriate_handler", description="Command with many flags (20 flags)")
|
||||
def benchmark_command_with_many_flags() -> None:
|
||||
router = Router()
|
||||
|
||||
flags = Flags([Flag(f'flag{i}') for i in range(20)])
|
||||
|
||||
@router.command(Command('test', flags=flags))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
input_cmd = InputCommand.parse('test ' + ' '.join(f'-flag{i}' for i in range(10)))
|
||||
router.finds_appropriate_handler(input_cmd)
|
||||
|
||||
|
||||
@benchmarks.register(type_="finds_appropriate_handler", description="Extreme (100 commands, 10 flags each)")
|
||||
def benchmark_extreme_router() -> None:
|
||||
router = Router()
|
||||
|
||||
for i in range(100):
|
||||
flags = Flags([Flag(f'f{i}_{j}') for j in range(10)])
|
||||
|
||||
@router.command(Command(f'cmd{i}', flags=flags))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
input_cmd = InputCommand.parse('cmd50 -f50_0 -f50_1 -f50_2')
|
||||
router.finds_appropriate_handler(input_cmd)
|
||||
@@ -0,0 +1,102 @@
|
||||
__all__ = [
|
||||
"benchmark_validate_all_single_flag",
|
||||
"benchmark_validate_neither_single_flag",
|
||||
"benchmark_validate_list_small",
|
||||
"benchmark_validate_list_large",
|
||||
"benchmark_validate_regex_simple",
|
||||
"benchmark_validate_regex_complex",
|
||||
"benchmark_validate_multiple_flags_10",
|
||||
"benchmark_validate_multiple_flags_50",
|
||||
"benchmark_validate_extreme_100_flags"
|
||||
]
|
||||
|
||||
import re
|
||||
|
||||
from argenta.command.flag import Flag, InputFlag, PossibleValues
|
||||
|
||||
from .entity import benchmarks
|
||||
|
||||
|
||||
@benchmarks.register(type_="flag_validation", description="Single flag with PossibleValues.ALL")
|
||||
def benchmark_validate_all_single_flag() -> None:
|
||||
flag = Flag("test", possible_values=PossibleValues.ALL)
|
||||
flag.validate_input_flag_value("some_value")
|
||||
|
||||
|
||||
@benchmarks.register(type_="flag_validation", description="Single flag with PossibleValues.NEITHER")
|
||||
def benchmark_validate_neither_single_flag() -> None:
|
||||
flag = Flag("test", possible_values=PossibleValues.NEITHER)
|
||||
flag.validate_input_flag_value("")
|
||||
|
||||
|
||||
@benchmarks.register(type_="flag_validation", description="List validation (5 possible values)")
|
||||
def benchmark_validate_list_small() -> None:
|
||||
flag = Flag("env", possible_values=["dev", "staging", "prod", "test", "local"])
|
||||
flag.validate_input_flag_value("prod")
|
||||
|
||||
|
||||
@benchmarks.register(type_="flag_validation", description="List validation (50 possible values)")
|
||||
def benchmark_validate_list_large() -> None:
|
||||
possible_values = [f"value{i}" for i in range(50)]
|
||||
flag = Flag("option", possible_values=possible_values)
|
||||
flag.validate_input_flag_value("value25")
|
||||
|
||||
|
||||
@benchmarks.register(type_="flag_validation", description="Regex validation (simple pattern)")
|
||||
def benchmark_validate_regex_simple() -> None:
|
||||
pattern = re.compile(r"^\d+$")
|
||||
flag = Flag("port", possible_values=pattern)
|
||||
flag.validate_input_flag_value("8080")
|
||||
|
||||
|
||||
@benchmarks.register(type_="flag_validation", description="Regex validation (complex pattern)")
|
||||
def benchmark_validate_regex_complex() -> None:
|
||||
pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
||||
flag = Flag("email", possible_values=pattern)
|
||||
flag.validate_input_flag_value("user@example.com")
|
||||
|
||||
|
||||
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (10 flags)")
|
||||
def benchmark_validate_multiple_flags_10() -> None:
|
||||
flags = [
|
||||
Flag(f"flag{i}", possible_values=PossibleValues.ALL)
|
||||
for i in range(10)
|
||||
]
|
||||
input_flags = [
|
||||
InputFlag(f"flag{i}", input_value=f"value{i}")
|
||||
for i in range(10)
|
||||
]
|
||||
|
||||
for flag, input_flag in zip(flags, input_flags):
|
||||
flag.validate_input_flag_value(input_flag.input_value)
|
||||
|
||||
|
||||
@benchmarks.register(type_="flag_validation", description="Multiple flags validation (50 flags)")
|
||||
def benchmark_validate_multiple_flags_50() -> None:
|
||||
flags = [
|
||||
Flag(f"flag{i}", possible_values=PossibleValues.ALL)
|
||||
for i in range(50)
|
||||
]
|
||||
input_flags = [
|
||||
InputFlag(f"flag{i}", input_value=f"value{i}")
|
||||
for i in range(50)
|
||||
]
|
||||
|
||||
for flag, input_flag in zip(flags, input_flags):
|
||||
flag.validate_input_flag_value(input_flag.input_value)
|
||||
|
||||
|
||||
@benchmarks.register(type_="flag_validation", description="Extreme (100 flags with regex validation)")
|
||||
def benchmark_validate_extreme_100_flags() -> None:
|
||||
pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
|
||||
flags = [
|
||||
Flag(f"flag{i}", possible_values=pattern)
|
||||
for i in range(100)
|
||||
]
|
||||
input_flags = [
|
||||
InputFlag(f"flag{i}", input_value=f"valid_value_{i}")
|
||||
for i in range(100)
|
||||
]
|
||||
|
||||
for flag, input_flag in zip(flags, input_flags):
|
||||
flag.validate_input_flag_value(input_flag.input_value)
|
||||
@@ -0,0 +1,51 @@
|
||||
__all__ = [
|
||||
"benchmark_parse_simple_command",
|
||||
"benchmark_command_with_few_flags",
|
||||
"benchmark_command_with_flags_and_values",
|
||||
"benchmark_command_with_mixed_prefixes",
|
||||
"benchmark_command_with_long_values",
|
||||
"benchmark_command_with_quoted_values",
|
||||
"benchmark_extreme_many_flags"
|
||||
]
|
||||
|
||||
from argenta.command.models import InputCommand
|
||||
|
||||
from .entity import benchmarks
|
||||
|
||||
|
||||
@benchmarks.register(type_="input_command_parse", description="Simple command (no flags)")
|
||||
def benchmark_parse_simple_command() -> None:
|
||||
InputCommand.parse("start")
|
||||
|
||||
|
||||
@benchmarks.register(type_="input_command_parse", description="Command with few flags (3 flags)")
|
||||
def benchmark_command_with_few_flags() -> None:
|
||||
InputCommand.parse("start -a -b -c")
|
||||
|
||||
|
||||
@benchmarks.register(type_="input_command_parse", description="Command with flags and values (5 flags)")
|
||||
def benchmark_command_with_flags_and_values() -> None:
|
||||
InputCommand.parse("start --host localhost --port 8080 --debug --verbose -c config.json")
|
||||
|
||||
|
||||
@benchmarks.register(type_="input_command_parse", description="Command with mixed prefixes (-, --, ---)")
|
||||
def benchmark_command_with_mixed_prefixes() -> None:
|
||||
InputCommand.parse("cmd -a --bb ---ccc -d value --ee value2 ---fff value3")
|
||||
|
||||
|
||||
@benchmarks.register(type_="input_command_parse", description="Command with long values (10 flags)")
|
||||
def benchmark_command_with_long_values() -> None:
|
||||
long_value = "a" * 100
|
||||
cmd = f"process --data {long_value} --config {long_value} --output {long_value}"
|
||||
InputCommand.parse(cmd)
|
||||
|
||||
|
||||
@benchmarks.register(type_="input_command_parse", description="Command with quoted values (5 flags)")
|
||||
def benchmark_command_with_quoted_values() -> None:
|
||||
InputCommand.parse("cmd --text 'hello world' --path '/usr/local/bin' --msg \"test message\"")
|
||||
|
||||
|
||||
@benchmarks.register(type_="input_command_parse", description="Extreme (50 flags with values)")
|
||||
def benchmark_extreme_many_flags() -> None:
|
||||
flags = " ".join(f"--flag{i} value{i}" for i in range(50))
|
||||
InputCommand.parse(f"command {flags}")
|
||||
@@ -0,0 +1,59 @@
|
||||
__all__ = [
|
||||
"benchmark_few_commands",
|
||||
"benchmark_many_commands_most_similar",
|
||||
"benchmark_many_aliases",
|
||||
"benchmark_partial_match",
|
||||
"benchmark_extreme_commands"
|
||||
]
|
||||
|
||||
from argenta import App
|
||||
from argenta.command.models import Command
|
||||
from argenta.response import Response
|
||||
from argenta.router import Router
|
||||
|
||||
from .entity import benchmarks
|
||||
|
||||
|
||||
def setup_app_with_commands(command_count: int, aliases_per_command: int = 0) -> App:
|
||||
app = App(override_system_messages=True)
|
||||
router = Router()
|
||||
|
||||
for i in range(command_count):
|
||||
aliases = {f'alias{i}_{j}' for j in range(aliases_per_command)} if aliases_per_command else set()
|
||||
|
||||
@router.command(Command(f'command{i}', aliases=aliases))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
return app
|
||||
|
||||
|
||||
@benchmarks.register(type_="most_similar_command", description="Few commands (10 commands, no match)")
|
||||
def benchmark_few_commands() -> None:
|
||||
app = setup_app_with_commands(10)
|
||||
app._most_similar_command("unknown")
|
||||
|
||||
|
||||
@benchmarks.register(type_="most_similar_command", description="Many commands (50 commands, no match)")
|
||||
def benchmark_many_commands_most_similar() -> None:
|
||||
app = setup_app_with_commands(50)
|
||||
app._most_similar_command("unknown")
|
||||
|
||||
|
||||
@benchmarks.register(type_="most_similar_command", description="Many aliases (20 commands, 10 aliases each)")
|
||||
def benchmark_many_aliases() -> None:
|
||||
app = setup_app_with_commands(20, aliases_per_command=10)
|
||||
app._most_similar_command("unknown")
|
||||
|
||||
|
||||
@benchmarks.register(type_="most_similar_command", description="Partial match (50 commands, prefix match)")
|
||||
def benchmark_partial_match() -> None:
|
||||
app = setup_app_with_commands(50)
|
||||
app._most_similar_command("comm")
|
||||
|
||||
|
||||
@benchmarks.register(type_="most_similar_command", description="Extreme (100 commands, 20 aliases each)")
|
||||
def benchmark_extreme_commands() -> None:
|
||||
app = setup_app_with_commands(100, aliases_per_command=20)
|
||||
app._most_similar_command("comm")
|
||||
@@ -1,22 +1,21 @@
|
||||
__all__ = [
|
||||
"benchmark_no_aliases",
|
||||
"benchmark_many_aliases",
|
||||
"benchmark_with_many_aliases",
|
||||
"benchmark_few_aliases",
|
||||
"benchmark_extreme_aliases",
|
||||
"benchmark_very_many_aliases"
|
||||
]
|
||||
|
||||
from argenta import App
|
||||
from argenta.router import Router
|
||||
from argenta.command.models import Command
|
||||
from argenta.response import Response
|
||||
from argenta.router import Router
|
||||
|
||||
from ..utils import get_time_of_pre_cycle_setup
|
||||
from ..registry import benchmark
|
||||
from .entity import benchmarks
|
||||
|
||||
|
||||
@benchmark(type_="pre_cycle_setup", description="With no aliases")
|
||||
def benchmark_no_aliases() -> float:
|
||||
@benchmarks.register(type_="pre_cycle_setup", description="With no aliases")
|
||||
def benchmark_no_aliases() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
router = Router()
|
||||
|
||||
@@ -33,12 +32,11 @@ def benchmark_no_aliases() -> float:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
execution_time = get_time_of_pre_cycle_setup(app)
|
||||
return execution_time
|
||||
app._pre_cycle_setup()
|
||||
|
||||
|
||||
@benchmark(type_="pre_cycle_setup", description="With few aliases (6 total)")
|
||||
def benchmark_few_aliases() -> float:
|
||||
@benchmarks.register(type_="pre_cycle_setup", description="With few aliases (6 total)")
|
||||
def benchmark_few_aliases() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
router = Router()
|
||||
|
||||
@@ -55,12 +53,11 @@ def benchmark_few_aliases() -> float:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
execution_time = get_time_of_pre_cycle_setup(app)
|
||||
return execution_time
|
||||
app._pre_cycle_setup()
|
||||
|
||||
|
||||
@benchmark(type_="pre_cycle_setup", description="With many aliases (15 total)")
|
||||
def benchmark_many_aliases() -> float:
|
||||
@benchmarks.register(type_="pre_cycle_setup", description="With many aliases (15 total)")
|
||||
def benchmark_with_many_aliases() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
router = Router()
|
||||
|
||||
@@ -77,12 +74,11 @@ def benchmark_many_aliases() -> float:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
execution_time = get_time_of_pre_cycle_setup(app)
|
||||
return execution_time
|
||||
app._pre_cycle_setup()
|
||||
|
||||
|
||||
@benchmark(type_="pre_cycle_setup", description="With very many aliases (60 total)")
|
||||
def benchmark_very_many_aliases() -> float:
|
||||
@benchmarks.register(type_="pre_cycle_setup", description="With very many aliases (60 total)")
|
||||
def benchmark_very_many_aliases() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
router = Router()
|
||||
|
||||
@@ -99,12 +95,11 @@ def benchmark_very_many_aliases() -> float:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
execution_time = get_time_of_pre_cycle_setup(app)
|
||||
return execution_time
|
||||
app._pre_cycle_setup()
|
||||
|
||||
|
||||
@benchmark(type_="pre_cycle_setup", description="With extreme aliases (300 total)")
|
||||
def benchmark_extreme_aliases() -> float:
|
||||
@benchmarks.register(type_="pre_cycle_setup", description="With extreme aliases (300 total)")
|
||||
def benchmark_extreme_aliases() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
router = Router()
|
||||
|
||||
@@ -121,5 +116,4 @@ def benchmark_extreme_aliases() -> float:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
execution_time = get_time_of_pre_cycle_setup(app)
|
||||
return execution_time
|
||||
app._pre_cycle_setup()
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
__all__ = [
|
||||
"benchmark_few_routers",
|
||||
"benchmark_many_routers",
|
||||
"benchmark_many_commands_per_router",
|
||||
"benchmark_many_aliases_per_command",
|
||||
"benchmark_extreme_routers"
|
||||
]
|
||||
|
||||
from argenta import App
|
||||
from argenta.command.models import Command
|
||||
from argenta.response import Response
|
||||
from argenta.router import Router
|
||||
|
||||
from .entity import benchmarks
|
||||
|
||||
|
||||
@benchmarks.register(type_="validate_routers_for_collisions", description="With few routers (3 routers, 1 command each)")
|
||||
def benchmark_few_routers() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
|
||||
for i in range(3):
|
||||
router = Router()
|
||||
|
||||
@router.command(Command(f'cmd{i}'))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
app._setup_system_router()
|
||||
app._validate_routers_for_collisions()
|
||||
|
||||
|
||||
@benchmarks.register(type_="validate_routers_for_collisions", description="With many routers (10 routers, 1 command each)")
|
||||
def benchmark_many_routers() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
|
||||
for i in range(10):
|
||||
router = Router()
|
||||
|
||||
@router.command(Command(f'cmd{i}'))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
app._setup_system_router()
|
||||
app._validate_routers_for_collisions()
|
||||
|
||||
|
||||
@benchmarks.register(type_="validate_routers_for_collisions", description="With many commands per router (3 routers, 10 commands each)")
|
||||
def benchmark_many_commands_per_router() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
|
||||
for i in range(3):
|
||||
router = Router()
|
||||
|
||||
for j in range(10):
|
||||
@router.command(Command(f'cmd{i}_{j}'))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
app._setup_system_router()
|
||||
app._validate_routers_for_collisions()
|
||||
|
||||
|
||||
@benchmarks.register(type_="validate_routers_for_collisions", description="With many aliases (3 routers, 5 commands, 10 aliases each)")
|
||||
def benchmark_many_aliases_per_command() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
|
||||
for i in range(3):
|
||||
router = Router()
|
||||
|
||||
for j in range(5):
|
||||
@router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(10)}))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
app._setup_system_router()
|
||||
app._validate_routers_for_collisions()
|
||||
|
||||
|
||||
@benchmarks.register(type_="validate_routers_for_collisions", description="Extreme (20 routers, 10 commands, 20 aliases each)")
|
||||
def benchmark_extreme_routers() -> None:
|
||||
app = App(override_system_messages=True)
|
||||
|
||||
for i in range(20):
|
||||
router = Router()
|
||||
|
||||
for j in range(10):
|
||||
@router.command(Command(f'cmd{i}_{j}', aliases={f'alias{i}_{j}_{k}' for k in range(20)}))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
app._setup_system_router()
|
||||
app._validate_routers_for_collisions()
|
||||
@@ -0,0 +1,179 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from importlib.metadata import version
|
||||
from pathlib import Path
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from argenta.command import Flag, PossibleValues, Flags
|
||||
from argenta.command.flag import ValidationStatus
|
||||
from argenta.command.models import Command
|
||||
from argenta.response import Response
|
||||
from argenta.router import Router
|
||||
from .benchmarks.core.models import BenchmarkGroupResult
|
||||
from .benchmarks.entity import benchmarks as registered_benchmarks
|
||||
from .services.report_table_generator import ReportTableGenerator
|
||||
from .services.system_info_reader import get_system_info
|
||||
from .services.diagram_generator import DiagramGenerator
|
||||
from .services.release_generator import ReleaseGenerator
|
||||
|
||||
console = Console()
|
||||
router = Router(title="Metrics commands:", disable_redirect_stdout=True)
|
||||
|
||||
POSITIVE_INTEGER_PATTERN = re.compile(r"^[1-9]\d*$")
|
||||
|
||||
|
||||
@router.command(
|
||||
Command(
|
||||
"run-all",
|
||||
description="Print all benchmarks results",
|
||||
flags=Flags([
|
||||
Flag('without-gc', possible_values=PossibleValues.NEITHER),
|
||||
Flag('without-system-info', possible_values=PossibleValues.NEITHER)
|
||||
])
|
||||
)
|
||||
)
|
||||
def all_print_handler(response: Response) -> None:
|
||||
report_generator = ReportTableGenerator(get_system_info())
|
||||
|
||||
without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID)
|
||||
if not without_system_info:
|
||||
console.print(report_generator.generate_system_info_header())
|
||||
console.print(report_generator.generate_system_info_table())
|
||||
|
||||
is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID)
|
||||
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(is_gc_disabled=bool(is_gc_disabled))
|
||||
|
||||
for benchmark_group_result in type_grouped_benchmarks:
|
||||
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
|
||||
console.print(report_generator.generate_benchmark_report_table(benchmark_group_result))
|
||||
|
||||
|
||||
@router.command(Command("list-types", description="List all benchmark types"))
|
||||
def list_types_handler(_: Response) -> None:
|
||||
types = registered_benchmarks.get_types()
|
||||
|
||||
if not types:
|
||||
console.print("[yellow]No benchmark types found[/yellow]")
|
||||
return
|
||||
|
||||
console.print("[bold cyan]Available benchmark types:[/bold cyan]\n")
|
||||
for type_ in types:
|
||||
benchmarks_count = len(registered_benchmarks.get_benchmarks_by_type(type_))
|
||||
console.print(f" [green]•[/green] [bold]{type_}[/bold] ({benchmarks_count} benchmarks)")
|
||||
|
||||
|
||||
@router.command(
|
||||
Command(
|
||||
"run-type",
|
||||
description="Run benchmarks by specific type",
|
||||
flags=Flags([
|
||||
Flag('type', possible_values=registered_benchmarks.get_types()),
|
||||
Flag('without-gc', possible_values=PossibleValues.NEITHER),
|
||||
Flag('without-system-info', possible_values=PossibleValues.NEITHER)
|
||||
])
|
||||
)
|
||||
)
|
||||
def run_type_handler(response: Response) -> None:
|
||||
type_flag = response.input_flags.get_flag_by_name("type")
|
||||
|
||||
if not type_flag:
|
||||
console.print("[red]Error: --type flag is required[/red]")
|
||||
console.print("[yellow]Usage: run-type --type <type_name>[/yellow]")
|
||||
return
|
||||
|
||||
benchmark_type = type_flag.input_value
|
||||
|
||||
if not type_flag.status == ValidationStatus.VALID:
|
||||
console.print(f"[red]Error: No benchmarks found for type '{benchmark_type}'[/red]")
|
||||
console.print("\n[yellow]Available types:[/yellow]")
|
||||
types = registered_benchmarks.get_types()
|
||||
for t in types:
|
||||
console.print(f" • {t}")
|
||||
return
|
||||
|
||||
report_generator = ReportTableGenerator(get_system_info())
|
||||
|
||||
without_system_info = response.input_flags.get_flag_by_name("without-system-info", with_status=ValidationStatus.VALID)
|
||||
if not without_system_info:
|
||||
console.print(report_generator.generate_system_info_header())
|
||||
console.print(report_generator.generate_system_info_table())
|
||||
|
||||
is_gc_disabled = response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID, default=False)
|
||||
benchmark_group_result = registered_benchmarks.run_benchmarks_by_type(benchmark_type, is_gc_disabled=bool(is_gc_disabled))
|
||||
|
||||
console.print(report_generator.generate_benchmark_table_header(benchmark_group_result))
|
||||
console.print(report_generator.generate_benchmark_report_table(benchmark_group_result))
|
||||
|
||||
|
||||
@router.command(Command("release-generate", description="Generate release report"))
|
||||
def release_generate_handler(_: Response) -> None:
|
||||
lib_version = version("argenta")
|
||||
|
||||
console.print(f"[cyan]Generating release report for version:[/cyan] [bold]{lib_version}[/bold]")
|
||||
console.print("[dim]Running benchmarks (1000 iterations, GC disabled)...[/dim]\n")
|
||||
|
||||
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(
|
||||
iterations=1000,
|
||||
is_gc_disabled=True
|
||||
)
|
||||
|
||||
release_generator = ReleaseGenerator(lib_version)
|
||||
output_dir = release_generator.generate_release(type_grouped_benchmarks)
|
||||
|
||||
console.print(f"[green]✓[/green] Benchmarks completed. Generating release report...\n")
|
||||
|
||||
for benchmark_group in type_grouped_benchmarks:
|
||||
console.print(f"[cyan]Generated for:[/cyan] [bold]{benchmark_group.type_}[/bold]")
|
||||
console.print(f" [green]✓[/green] {benchmark_group.type_}_comparison.png")
|
||||
console.print(f" [green]✓[/green] {benchmark_group.type_}.json\n")
|
||||
|
||||
console.print(f"[bold green]✓ Release report generated successfully[/bold green]")
|
||||
console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]")
|
||||
|
||||
|
||||
@router.command(
|
||||
Command(
|
||||
"diagrams-generate",
|
||||
description="Generate diagrams for all benchmarks",
|
||||
flags=Flags([
|
||||
Flag('without-gc', possible_values=PossibleValues.NEITHER),
|
||||
Flag('iterations', possible_values=POSITIVE_INTEGER_PATTERN)
|
||||
])
|
||||
)
|
||||
)
|
||||
def diagrams_generate_handler(response: Response) -> None:
|
||||
iterations = 100
|
||||
iterations_flag = response.input_flags.get_flag_by_name("iterations", with_status=ValidationStatus.VALID)
|
||||
if iterations_flag:
|
||||
iterations = int(iterations_flag.input_value)
|
||||
|
||||
is_gc_disabled = bool(response.input_flags.get_flag_by_name("without-gc", with_status=ValidationStatus.VALID))
|
||||
|
||||
console.print("[cyan]Running all benchmarks...[/cyan]")
|
||||
console.print(f"[dim]Iterations: {iterations}, GC Disabled: {is_gc_disabled}[/dim]\n")
|
||||
|
||||
type_grouped_benchmarks: list[BenchmarkGroupResult] = registered_benchmarks.run_benchmarks_grouped_by_type(
|
||||
iterations=iterations,
|
||||
is_gc_disabled=is_gc_disabled
|
||||
)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
output_dir = Path("metrics/reports/diagrams") / timestamp
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
diagram_generator = DiagramGenerator(output_dir)
|
||||
|
||||
console.print(f"[green]✓[/green] Benchmarks completed. Generating diagrams...\n")
|
||||
|
||||
generated_count = 0
|
||||
|
||||
for benchmark_group in type_grouped_benchmarks:
|
||||
console.print(f"[cyan]Generating diagram for:[/cyan] [bold]{benchmark_group.type_}[/bold]")
|
||||
|
||||
comparison_path = diagram_generator.generate_comparison_diagram(benchmark_group)
|
||||
generated_count += 1
|
||||
console.print(f" [green]✓[/green] {comparison_path.name}\n")
|
||||
|
||||
console.print(f"[bold green]✓ Successfully generated {generated_count} diagrams[/bold green]")
|
||||
console.print(f"[cyan]Output directory:[/cyan] [bold]{output_dir}[/bold]")
|
||||
@@ -1,98 +0,0 @@
|
||||
__all__ = [
|
||||
"Benchmark",
|
||||
"Benchmarks",
|
||||
"benchmark"
|
||||
]
|
||||
|
||||
from typing import Callable, ClassVar, overload, override
|
||||
|
||||
BenchmarkAsFunc = Callable[[], float]
|
||||
|
||||
|
||||
class Benchmark:
|
||||
def __init__(
|
||||
self,
|
||||
func: BenchmarkAsFunc,
|
||||
*,
|
||||
type_: str,
|
||||
name: str,
|
||||
description: str,
|
||||
iterations: int
|
||||
) -> None:
|
||||
self.func = func
|
||||
self.type_ = type_
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.iterations = iterations
|
||||
|
||||
def run(self) -> float:
|
||||
return self.func()
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f'Benchmark<{self.type_=}, {self.name=}, {self.description=}, {self.iterations=}>'
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f'Benchmark({self.type_=}, {self.name=}, {self.description=}, {self.iterations=})'
|
||||
|
||||
|
||||
class Benchmarks:
|
||||
_benchmarks: ClassVar[list[Benchmark]] = []
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def register(
|
||||
cls,
|
||||
call: BenchmarkAsFunc,
|
||||
*,
|
||||
type_: str = "",
|
||||
description: str = "",
|
||||
iterations: int = 100,
|
||||
) -> BenchmarkAsFunc:
|
||||
...
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def register(
|
||||
cls,
|
||||
call: None = None,
|
||||
*,
|
||||
type_: str = "",
|
||||
description: str = "",
|
||||
iterations: int = 100,
|
||||
) -> Callable[[BenchmarkAsFunc], BenchmarkAsFunc]:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def register(
|
||||
cls,
|
||||
call: BenchmarkAsFunc | None = None,
|
||||
*,
|
||||
type_: str = "",
|
||||
description: str = "",
|
||||
iterations: int = 100,
|
||||
) -> Callable[[BenchmarkAsFunc], BenchmarkAsFunc] | BenchmarkAsFunc:
|
||||
def decorator(func: BenchmarkAsFunc) -> BenchmarkAsFunc:
|
||||
cls._benchmarks.append(
|
||||
Benchmark(
|
||||
func,
|
||||
type_=type_,
|
||||
name=func.__name__,
|
||||
description=description or f'description for {func.__name__} with {iterations} iterations',
|
||||
iterations=iterations
|
||||
)
|
||||
)
|
||||
return func
|
||||
|
||||
if call is None:
|
||||
return decorator
|
||||
else:
|
||||
return decorator(call)
|
||||
|
||||
@classmethod
|
||||
def get_benchmarks(cls) -> list[Benchmark]:
|
||||
return cls._benchmarks
|
||||
|
||||
|
||||
benchmark = Benchmarks.register
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"type": "finds_appropriate_handler",
|
||||
"iterations": 1000,
|
||||
"gc_disabled": true,
|
||||
"benchmarks": [
|
||||
{
|
||||
"name": "benchmark_simple_command",
|
||||
"description": "Simple command (no flags)",
|
||||
"avg_time": 0.036,
|
||||
"median_time": 0.0354,
|
||||
"std_dev": 0.0087
|
||||
},
|
||||
{
|
||||
"name": "benchmark_command_with_flags",
|
||||
"description": "Command with flags (3 flags)",
|
||||
"avg_time": 0.0557,
|
||||
"median_time": 0.0545,
|
||||
"std_dev": 0.0171
|
||||
},
|
||||
{
|
||||
"name": "benchmark_many_commands",
|
||||
"description": "Many commands (50 commands)",
|
||||
"avg_time": 1.0453,
|
||||
"median_time": 1.0388,
|
||||
"std_dev": 0.0322
|
||||
},
|
||||
{
|
||||
"name": "benchmark_command_with_many_flags",
|
||||
"description": "Command with many flags (20 flags)",
|
||||
"avg_time": 0.1322,
|
||||
"median_time": 0.131,
|
||||
"std_dev": 0.0045
|
||||
},
|
||||
{
|
||||
"name": "benchmark_extreme_router",
|
||||
"description": "Extreme (100 commands, 10 flags each)",
|
||||
"avg_time": 3.2471,
|
||||
"median_time": 3.235,
|
||||
"std_dev": 0.0814
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"type": "flag_validation",
|
||||
"iterations": 1000,
|
||||
"gc_disabled": true,
|
||||
"benchmarks": [
|
||||
{
|
||||
"name": "benchmark_validate_all_single_flag",
|
||||
"description": "Single flag with PossibleValues.ALL",
|
||||
"avg_time": 0.0008,
|
||||
"median_time": 0.0008,
|
||||
"std_dev": 0.0002
|
||||
},
|
||||
{
|
||||
"name": "benchmark_validate_neither_single_flag",
|
||||
"description": "Single flag with PossibleValues.NEITHER",
|
||||
"avg_time": 0.0008,
|
||||
"median_time": 0.0008,
|
||||
"std_dev": 0.0002
|
||||
},
|
||||
{
|
||||
"name": "benchmark_validate_list_small",
|
||||
"description": "List validation (5 possible values)",
|
||||
"avg_time": 0.001,
|
||||
"median_time": 0.0009,
|
||||
"std_dev": 0.0007
|
||||
},
|
||||
{
|
||||
"name": "benchmark_validate_list_large",
|
||||
"description": "List validation (50 possible values)",
|
||||
"avg_time": 0.0079,
|
||||
"median_time": 0.0078,
|
||||
"std_dev": 0.0021
|
||||
},
|
||||
{
|
||||
"name": "benchmark_validate_regex_simple",
|
||||
"description": "Regex validation (simple pattern)",
|
||||
"avg_time": 0.0017,
|
||||
"median_time": 0.0016,
|
||||
"std_dev": 0.0028
|
||||
},
|
||||
{
|
||||
"name": "benchmark_validate_regex_complex",
|
||||
"description": "Regex validation (complex pattern)",
|
||||
"avg_time": 0.0018,
|
||||
"median_time": 0.0016,
|
||||
"std_dev": 0.0051
|
||||
},
|
||||
{
|
||||
"name": "benchmark_validate_multiple_flags_10",
|
||||
"description": "Multiple flags validation (10 flags)",
|
||||
"avg_time": 0.0145,
|
||||
"median_time": 0.0144,
|
||||
"std_dev": 0.0013
|
||||
},
|
||||
{
|
||||
"name": "benchmark_validate_multiple_flags_50",
|
||||
"description": "Multiple flags validation (50 flags)",
|
||||
"avg_time": 0.0661,
|
||||
"median_time": 0.0658,
|
||||
"std_dev": 0.0024
|
||||
},
|
||||
{
|
||||
"name": "benchmark_validate_extreme_100_flags",
|
||||
"description": "Extreme (100 flags with regex validation)",
|
||||
"avg_time": 0.1599,
|
||||
"median_time": 0.1589,
|
||||
"std_dev": 0.0065
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"type": "input_command_parse",
|
||||
"iterations": 1000,
|
||||
"gc_disabled": true,
|
||||
"benchmarks": [
|
||||
{
|
||||
"name": "benchmark_parse_simple_command",
|
||||
"description": "Simple command (no flags)",
|
||||
"avg_time": 0.0096,
|
||||
"median_time": 0.0095,
|
||||
"std_dev": 0.0012
|
||||
},
|
||||
{
|
||||
"name": "benchmark_command_with_few_flags",
|
||||
"description": "Command with few flags (3 flags)",
|
||||
"avg_time": 0.0216,
|
||||
"median_time": 0.0213,
|
||||
"std_dev": 0.0021
|
||||
},
|
||||
{
|
||||
"name": "benchmark_command_with_flags_and_values",
|
||||
"description": "Command with flags and values (5 flags)",
|
||||
"avg_time": 0.06,
|
||||
"median_time": 0.0595,
|
||||
"std_dev": 0.0025
|
||||
},
|
||||
{
|
||||
"name": "benchmark_command_with_mixed_prefixes",
|
||||
"description": "Command with mixed prefixes (-, --, ---)",
|
||||
"avg_time": 0.0542,
|
||||
"median_time": 0.0538,
|
||||
"std_dev": 0.0028
|
||||
},
|
||||
{
|
||||
"name": "benchmark_command_with_long_values",
|
||||
"description": "Command with long values (10 flags)",
|
||||
"avg_time": 0.2092,
|
||||
"median_time": 0.2082,
|
||||
"std_dev": 0.0067
|
||||
},
|
||||
{
|
||||
"name": "benchmark_command_with_quoted_values",
|
||||
"description": "Command with quoted values (5 flags)",
|
||||
"avg_time": 0.0481,
|
||||
"median_time": 0.0477,
|
||||
"std_dev": 0.0023
|
||||
},
|
||||
{
|
||||
"name": "benchmark_extreme_many_flags",
|
||||
"description": "Extreme (50 flags with values)",
|
||||
"avg_time": 0.7907,
|
||||
"median_time": 0.7884,
|
||||
"std_dev": 0.0417
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"type": "most_similar_command",
|
||||
"iterations": 1000,
|
||||
"gc_disabled": true,
|
||||
"benchmarks": [
|
||||
{
|
||||
"name": "benchmark_few_commands",
|
||||
"description": "Few commands (10 commands, no match)",
|
||||
"avg_time": 0.251,
|
||||
"median_time": 0.2488,
|
||||
"std_dev": 0.012
|
||||
},
|
||||
{
|
||||
"name": "benchmark_many_commands_most_similar",
|
||||
"description": "Many commands (50 commands, no match)",
|
||||
"avg_time": 1.1933,
|
||||
"median_time": 1.1878,
|
||||
"std_dev": 0.0305
|
||||
},
|
||||
{
|
||||
"name": "benchmark_many_aliases",
|
||||
"description": "Many aliases (20 commands, 10 aliases each)",
|
||||
"avg_time": 1.2151,
|
||||
"median_time": 1.2124,
|
||||
"std_dev": 0.0282
|
||||
},
|
||||
{
|
||||
"name": "benchmark_partial_match",
|
||||
"description": "Partial match (50 commands, prefix match)",
|
||||
"avg_time": 1.6781,
|
||||
"median_time": 1.6689,
|
||||
"std_dev": 0.0573
|
||||
},
|
||||
{
|
||||
"name": "benchmark_extreme_commands",
|
||||
"description": "Extreme (100 commands, 20 aliases each)",
|
||||
"avg_time": 10.5539,
|
||||
"median_time": 10.5288,
|
||||
"std_dev": 0.1603
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"type": "pre_cycle_setup",
|
||||
"iterations": 1000,
|
||||
"gc_disabled": true,
|
||||
"benchmarks": [
|
||||
{
|
||||
"name": "benchmark_no_aliases",
|
||||
"description": "With no aliases",
|
||||
"avg_time": 7.4799,
|
||||
"median_time": 7.4576,
|
||||
"std_dev": 0.1645
|
||||
},
|
||||
{
|
||||
"name": "benchmark_few_aliases",
|
||||
"description": "With few aliases (6 total)",
|
||||
"avg_time": 7.4135,
|
||||
"median_time": 7.4061,
|
||||
"std_dev": 0.1709
|
||||
},
|
||||
{
|
||||
"name": "benchmark_with_many_aliases",
|
||||
"description": "With many aliases (15 total)",
|
||||
"avg_time": 7.4018,
|
||||
"median_time": 7.3943,
|
||||
"std_dev": 0.1589
|
||||
},
|
||||
{
|
||||
"name": "benchmark_very_many_aliases",
|
||||
"description": "With very many aliases (60 total)",
|
||||
"avg_time": 7.476,
|
||||
"median_time": 7.4575,
|
||||
"std_dev": 0.2156
|
||||
},
|
||||
{
|
||||
"name": "benchmark_extreme_aliases",
|
||||
"description": "With extreme aliases (300 total)",
|
||||
"avg_time": 7.7167,
|
||||
"median_time": 7.706,
|
||||
"std_dev": 0.2052
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"type": "validate_routers_for_collisions",
|
||||
"iterations": 1000,
|
||||
"gc_disabled": true,
|
||||
"benchmarks": [
|
||||
{
|
||||
"name": "benchmark_few_routers",
|
||||
"description": "With few routers (3 routers, 1 command each)",
|
||||
"avg_time": 0.0959,
|
||||
"median_time": 0.0944,
|
||||
"std_dev": 0.0097
|
||||
},
|
||||
{
|
||||
"name": "benchmark_many_routers",
|
||||
"description": "With many routers (10 routers, 1 command each)",
|
||||
"avg_time": 0.2488,
|
||||
"median_time": 0.2467,
|
||||
"std_dev": 0.0081
|
||||
},
|
||||
{
|
||||
"name": "benchmark_many_commands_per_router",
|
||||
"description": "With many commands per router (3 routers, 10 commands each)",
|
||||
"avg_time": 0.6474,
|
||||
"median_time": 0.6401,
|
||||
"std_dev": 0.0304
|
||||
},
|
||||
{
|
||||
"name": "benchmark_many_aliases_per_command",
|
||||
"description": "With many aliases (3 routers, 5 commands, 10 aliases each)",
|
||||
"avg_time": 0.5261,
|
||||
"median_time": 0.5156,
|
||||
"std_dev": 0.0475
|
||||
},
|
||||
{
|
||||
"name": "benchmark_extreme_routers",
|
||||
"description": "Extreme (20 routers, 10 commands, 20 aliases each)",
|
||||
"avg_time": 9.9128,
|
||||
"median_time": 9.9518,
|
||||
"std_dev": 0.2373
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
@@ -0,0 +1,6 @@
|
||||
from .diagram_generator import DiagramGenerator
|
||||
from .report_table_generator import ReportTableGenerator
|
||||
from .system_info_reader import get_system_info
|
||||
from .release_generator import ReleaseGenerator
|
||||
|
||||
__all__ = ["DiagramGenerator", "ReportTableGenerator", "get_system_info", "ReleaseGenerator"]
|
||||
@@ -0,0 +1,110 @@
|
||||
__all__ = ["DiagramGenerator"]
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from ..benchmarks.core.models import BenchmarkGroupResult
|
||||
|
||||
|
||||
class DiagramGenerator:
|
||||
def __init__(self, output_dir: Path | str) -> None:
|
||||
self.output_dir: Path = Path(output_dir) if isinstance(output_dir, str) else output_dir
|
||||
|
||||
matplotlib.use('Agg')
|
||||
plt.style.use('seaborn-v0_8-whitegrid')
|
||||
|
||||
def generate_comparison_diagram(self, benchmark_group: BenchmarkGroupResult) -> Path:
|
||||
results = benchmark_group.benchmark_results
|
||||
sorted_results = sorted(results, key=lambda br: br.avg_time)
|
||||
|
||||
descriptions: list[str] = [br.description for br in sorted_results]
|
||||
avg_times: list[float] = [br.avg_time for br in sorted_results]
|
||||
median_times: list[float] = [br.median_time for br in sorted_results]
|
||||
std_devs: list[float] = [br.std_dev for br in sorted_results]
|
||||
|
||||
max_value = max(
|
||||
max(avg_times) if avg_times else 0,
|
||||
max(median_times) if median_times else 0,
|
||||
max(std_devs) if std_devs else 0
|
||||
)
|
||||
y_limit = max_value / 0.85 if max_value > 0 else 1.0
|
||||
|
||||
items_count = len(descriptions)
|
||||
x_positions: list[int] = list(range(items_count))
|
||||
|
||||
bar_width = 0.25
|
||||
|
||||
x_std_dev = [x - bar_width for x in x_positions]
|
||||
x_avg = [x for x in x_positions]
|
||||
x_median = [x + bar_width for x in x_positions]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(16, 8))
|
||||
fig.patch.set_facecolor('white')
|
||||
|
||||
bars_std = ax.bar(x_std_dev, std_devs, bar_width, label='Std Deviation',
|
||||
color='#2ecc71', alpha=0.9, edgecolor='#27ae60', linewidth=1.5)
|
||||
bars_avg = ax.bar(x_avg, avg_times, bar_width, label='Average Time',
|
||||
color='#3498db', alpha=0.9, edgecolor='#2980b9', linewidth=1.5)
|
||||
bars_median = ax.bar(x_median, median_times, bar_width, label='Median Time',
|
||||
color='#e74c3c', alpha=0.9, edgecolor='#c0392b', linewidth=1.5)
|
||||
|
||||
for bar_group in [bars_std, bars_avg, bars_median]:
|
||||
for bar in bar_group:
|
||||
height = bar.get_height()
|
||||
ax.text(
|
||||
bar.get_x() + bar.get_width() / 2.,
|
||||
height,
|
||||
f'{height:.3f}',
|
||||
ha='center', va='bottom', fontsize=9, fontweight='bold'
|
||||
)
|
||||
|
||||
ax.set_ylabel('Time (ms)', fontsize=14, fontweight='bold', labelpad=10)
|
||||
|
||||
title_text = f'{benchmark_group.type_.replace("_", " ").title()}'
|
||||
metadata_text = f'Iterations: {benchmark_group.iterations} | GC: {"Disabled" if benchmark_group.is_gc_disabled else "Enabled"}'
|
||||
|
||||
ax.text(0.5, 1.08, title_text, transform=ax.transAxes,
|
||||
fontsize=18, fontweight='bold', ha='center', color='#2c3e50')
|
||||
ax.text(0.5, 1.03, metadata_text, transform=ax.transAxes,
|
||||
fontsize=12, ha='center', color='#7f8c8d', style='italic')
|
||||
|
||||
ax.set_xticks(x_positions)
|
||||
ax.set_xticklabels([])
|
||||
|
||||
for i, (pos, desc) in enumerate(zip(x_positions, descriptions)):
|
||||
text_x_pos = pos - bar_width - (bar_width / 2)
|
||||
ax.text(
|
||||
text_x_pos,
|
||||
y_limit * 0.02,
|
||||
desc,
|
||||
rotation=90, va='bottom', ha='right', fontsize=10,
|
||||
color='#2c3e50'
|
||||
)
|
||||
|
||||
ax.set_ylim(0, y_limit)
|
||||
|
||||
legend = ax.legend(loc='upper left', fontsize=12, framealpha=0.95,
|
||||
edgecolor='#34495e', fancybox=True, shadow=True)
|
||||
legend.get_frame().set_facecolor('#ecf0f1')
|
||||
|
||||
ax.grid(axis='y', alpha=0.4, linestyle='--', linewidth=0.8)
|
||||
ax.set_axisbelow(True)
|
||||
|
||||
ax.spines['top'].set_visible(False)
|
||||
ax.spines['right'].set_visible(False)
|
||||
ax.spines['left'].set_color('#7f8c8d')
|
||||
ax.spines['bottom'].set_color('#7f8c8d')
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
filename = f"{benchmark_group.type_}_comparison.png"
|
||||
output_path = self.output_dir / filename
|
||||
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
plt.savefig(output_path, dpi=200, bbox_inches='tight', facecolor='white')
|
||||
plt.close(fig)
|
||||
|
||||
return output_path
|
||||
@@ -0,0 +1,49 @@
|
||||
__all__ = ["ReleaseGenerator"]
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from ..benchmarks.core.models import BenchmarkGroupResult
|
||||
from .diagram_generator import DiagramGenerator
|
||||
|
||||
|
||||
class ReleaseGenerator:
|
||||
def __init__(self, lib_version: str) -> None:
|
||||
self.lib_version = lib_version
|
||||
self.output_dir = Path("metrics/reports/releases") / lib_version
|
||||
|
||||
def generate_release(self, benchmark_groups: list[BenchmarkGroupResult]) -> Path:
|
||||
if self.output_dir.exists():
|
||||
shutil.rmtree(self.output_dir)
|
||||
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for benchmark_group in benchmark_groups:
|
||||
type_dir = self.output_dir / benchmark_group.type_
|
||||
type_dir.mkdir(exist_ok=True)
|
||||
|
||||
diagram_generator = DiagramGenerator(type_dir)
|
||||
diagram_generator.generate_comparison_diagram(benchmark_group)
|
||||
|
||||
json_data = {
|
||||
"type": benchmark_group.type_,
|
||||
"iterations": benchmark_group.iterations,
|
||||
"gc_disabled": benchmark_group.is_gc_disabled,
|
||||
"benchmarks": [
|
||||
{
|
||||
"name": br.name,
|
||||
"description": br.description,
|
||||
"avg_time": br.avg_time,
|
||||
"median_time": br.median_time,
|
||||
"std_dev": br.std_dev
|
||||
}
|
||||
for br in benchmark_group.benchmark_results
|
||||
]
|
||||
}
|
||||
|
||||
json_path = type_dir / f"{benchmark_group.type_}.json"
|
||||
with open(json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(json_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return self.output_dir
|
||||
@@ -0,0 +1,72 @@
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from ..benchmarks.core.models import BenchmarkGroupResult
|
||||
from metrics.services.system_info_reader import SystemInfo
|
||||
|
||||
|
||||
class ReportTableGenerator:
|
||||
def __init__(self, system_info: SystemInfo):
|
||||
self.system_info = system_info
|
||||
self._cached_benchmark_tables: dict[int, Table] = {}
|
||||
self._cached_system_info_table: Table | None = None
|
||||
|
||||
def generate_benchmark_report_table(self, benchmark_group_result: BenchmarkGroupResult) -> Table:
|
||||
if cached_result := self._cached_benchmark_tables.get(id(benchmark_group_result)):
|
||||
return cached_result
|
||||
|
||||
table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
|
||||
table.add_column("Description", style="dim")
|
||||
table.add_column("Avg Time", justify="right", style="bold yellow")
|
||||
table.add_column("Median Time", justify="right", style="bold yellow")
|
||||
table.add_column("Stdev", justify="right", style="bold yellow")
|
||||
|
||||
for benchmark in benchmark_group_result.benchmark_results:
|
||||
table.add_row(
|
||||
benchmark.description,
|
||||
str(benchmark.avg_time),
|
||||
str(benchmark.median_time),
|
||||
str(benchmark.std_dev),
|
||||
)
|
||||
self._cached_benchmark_tables[id(benchmark_group_result)] = table
|
||||
return table
|
||||
|
||||
@staticmethod
|
||||
def generate_benchmark_table_header(benchmark_group_result: BenchmarkGroupResult) -> Panel:
|
||||
header_text = Text(f"TYPE: {benchmark_group_result.type_.upper()} ; "
|
||||
f"ITERATIONS: {benchmark_group_result.iterations} ; "
|
||||
f"GC {"DISABLED" if benchmark_group_result.is_gc_disabled else "ENABLED"} ; "
|
||||
f"ALL TIME IN MS",
|
||||
style="bold magenta")
|
||||
return Panel(header_text, expand=False, border_style="magenta")
|
||||
|
||||
def generate_system_info_table(self) -> Table:
|
||||
if self._cached_system_info_table is not None:
|
||||
return self._cached_system_info_table
|
||||
|
||||
table = Table(show_header=True, header_style="bold cyan", border_style="blue", show_lines=True)
|
||||
table.add_column("Parameter", style="green")
|
||||
table.add_column("Value", style="yellow")
|
||||
|
||||
table.add_row("OS Name", self.system_info.os_info.name)
|
||||
table.add_row("OS Kernel Version", self.system_info.os_info.kernel_version)
|
||||
table.add_row("Architecture", self.system_info.cpu_info.architecture)
|
||||
table.add_row("CPU", self.system_info.cpu_info.name)
|
||||
table.add_row("CPU Physical Cores", str(self.system_info.cpu_info.physical_cores))
|
||||
table.add_row("CPU Logical Cores", str(self.system_info.cpu_info.logical_cores))
|
||||
table.add_row("CPU Max Frequency", str(self.system_info.cpu_info.max_frequency) + ' GHz')
|
||||
table.add_row("Total RAM", str(self.system_info.memory_info.total_ram) + ' GB')
|
||||
table.add_row("Used RAM", str(self.system_info.memory_info.used_ram) + ' GB')
|
||||
table.add_row("Available RAM", str(self.system_info.memory_info.available_ram) + ' GB')
|
||||
table.add_row("Python Version", self.system_info.python_info.version)
|
||||
table.add_row("Python Implementation", self.system_info.python_info.implementation)
|
||||
table.add_row("Python Compiler", self.system_info.python_info.compiler)
|
||||
|
||||
self._cached_system_info_table = table
|
||||
return table
|
||||
|
||||
@staticmethod
|
||||
def generate_system_info_header() -> Panel:
|
||||
header_text = Text("SYSTEM INFO", style="bold magenta")
|
||||
return Panel(header_text, expand=False, border_style="magenta")
|
||||
@@ -0,0 +1,126 @@
|
||||
__all__ = [
|
||||
"SystemInfo",
|
||||
"get_system_info"
|
||||
]
|
||||
|
||||
from dataclasses import dataclass
|
||||
import platform
|
||||
import sys
|
||||
|
||||
import cpuinfo
|
||||
import psutil
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SystemInfo:
|
||||
os_info: OSInfo
|
||||
cpu_info: CPUInfo
|
||||
memory_info: MemoryInfo
|
||||
python_info: PythonInfo
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OSInfo:
|
||||
name: str
|
||||
kernel_version: str
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CPUInfo:
|
||||
name: str
|
||||
architecture: str
|
||||
physical_cores: int
|
||||
logical_cores: int
|
||||
max_frequency: float
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MemoryInfo:
|
||||
total_ram: float # in GB
|
||||
used_ram: float # in GB
|
||||
available_ram: float # in GB
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PythonInfo:
|
||||
version: str
|
||||
implementation: str
|
||||
compiler: str
|
||||
|
||||
|
||||
def get_system_info() -> SystemInfo:
|
||||
os_info = get_os_info()
|
||||
cpu_info = get_cpu_info()
|
||||
memory_info = get_memory_info()
|
||||
python_info = get_python_info()
|
||||
return SystemInfo(
|
||||
os_info=os_info,
|
||||
cpu_info=cpu_info,
|
||||
memory_info=memory_info,
|
||||
python_info=python_info,
|
||||
)
|
||||
|
||||
def get_os_info() -> OSInfo:
|
||||
system = platform.system()
|
||||
|
||||
if system == "Windows":
|
||||
ver = sys.getwindowsversion()
|
||||
kernel_version = f"{ver.major}.{ver.minor}.{ver.build}"
|
||||
|
||||
if ver.build >= 22000:
|
||||
product_name = "Windows 11"
|
||||
else:
|
||||
product_name = "Windows 10"
|
||||
|
||||
return OSInfo(
|
||||
name=product_name,
|
||||
kernel_version=kernel_version,
|
||||
)
|
||||
elif system == "Darwin":
|
||||
return OSInfo(
|
||||
kernel_version=platform.release(),
|
||||
name=f"macOS {platform.mac_ver()[0]}"
|
||||
)
|
||||
else:
|
||||
return OSInfo(
|
||||
kernel_version=platform.release(),
|
||||
name=platform.system()
|
||||
)
|
||||
|
||||
def get_cpu_info() -> CPUInfo:
|
||||
cpu_info = cpuinfo.get_cpu_info()
|
||||
cpu_name = cpu_info["brand_raw"]
|
||||
cpu_architecture = cpu_info["arch"]
|
||||
cpu_physical_cores = psutil.cpu_count(logical=False)
|
||||
cpu_logical_cores = psutil.cpu_count(logical=True)
|
||||
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
cpu_max_frequency = cpu_freq.max
|
||||
|
||||
return CPUInfo(
|
||||
name=cpu_name,
|
||||
architecture=cpu_architecture,
|
||||
physical_cores=cpu_physical_cores,
|
||||
logical_cores=cpu_logical_cores,
|
||||
max_frequency=cpu_max_frequency
|
||||
)
|
||||
|
||||
def get_memory_info() -> MemoryInfo:
|
||||
mem = psutil.virtual_memory()
|
||||
total_ram = round(mem.total / (1024**3), 2)
|
||||
used_ram = round(mem.used / (1024**3), 2)
|
||||
available_ram = round(mem.available / (1024**3), 2)
|
||||
|
||||
return MemoryInfo(
|
||||
total_ram=total_ram,
|
||||
used_ram=used_ram,
|
||||
available_ram=available_ram,
|
||||
)
|
||||
|
||||
def get_python_info() -> PythonInfo:
|
||||
python_version = platform.python_version()
|
||||
python_implementation = platform.python_implementation()
|
||||
python_compiler = platform.python_compiler()
|
||||
return PythonInfo(
|
||||
version=python_version,
|
||||
implementation=python_implementation,
|
||||
compiler=python_compiler
|
||||
)
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
__all__ = [
|
||||
"get_time_of_pre_cycle_setup",
|
||||
"attempts_to_average",
|
||||
"run_benchmark",
|
||||
"BenchmarkResult"
|
||||
]
|
||||
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from argenta import App
|
||||
from metrics.registry import Benchmark
|
||||
|
||||
|
||||
def get_time_of_pre_cycle_setup(app: App) -> float:
|
||||
start = time.perf_counter()
|
||||
with redirect_stdout(io.StringIO()):
|
||||
app._pre_cycle_setup() # pyright: ignore[reportPrivateUsage]
|
||||
end = time.perf_counter()
|
||||
return (end - start) * 1000 # as milliseconds
|
||||
|
||||
|
||||
def attempts_to_average(bench_attempts: list[float], iterations: int) -> Decimal:
|
||||
return Decimal(sum(bench_attempts) / iterations).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BenchmarkResult:
|
||||
type_: str
|
||||
name: str
|
||||
description: str
|
||||
iterations: int
|
||||
avg_time: Decimal
|
||||
|
||||
|
||||
def run_benchmark(benchmark: Benchmark) -> BenchmarkResult:
|
||||
bench_attempts: list[float] = []
|
||||
for _ in range(benchmark.iterations):
|
||||
bench_attempts.append(benchmark.run())
|
||||
avg = attempts_to_average(bench_attempts, benchmark.iterations)
|
||||
return BenchmarkResult(benchmark.type_, benchmark.name, benchmark.description, benchmark.iterations, avg)
|
||||
+14
-10
@@ -1,14 +1,18 @@
|
||||
from argenta.app import AutoCompleter
|
||||
from argenta import App, Command, Response, Router
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_commands: set[str] = {"start", "qwertyu", "stop", "exit"}
|
||||
hist_file: str = "history.txt"
|
||||
app = App(override_system_messages=True)
|
||||
router = Router()
|
||||
|
||||
ac: AutoCompleter = AutoCompleter(autocomplete_button='tab')
|
||||
ac.initial_setup(test_commands)
|
||||
@router.command(Command('command'))
|
||||
def handler(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
while True:
|
||||
inp: str = ac.prompt(">>> ").strip()
|
||||
if inp == "exit":
|
||||
break
|
||||
@router.command(Command('command_other'))
|
||||
def handler2(_res: Response) -> None:
|
||||
pass
|
||||
|
||||
app.include_routers(router)
|
||||
app._pre_cycle_setup()
|
||||
|
||||
assert app._most_similar_command('command_') == 'command'
|
||||
@@ -4,7 +4,7 @@ from argenta.app import DynamicDividingLine
|
||||
|
||||
from .routers import router
|
||||
|
||||
app: App = App(prompt='>>> ', dividing_line=DynamicDividingLine('~'))
|
||||
app: App = App(prompt='>>> ', dividing_line=None)
|
||||
orchestrator: Orchestrator = Orchestrator()
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from prompt_toolkit import HTML
|
||||
|
||||
from argenta import App, Orchestrator
|
||||
from argenta.app import PredefinedMessages
|
||||
from argenta.app import PredefinedMessages, StaticDividingLine, AutoCompleter
|
||||
from argenta.app.dividing_line.models import DynamicDividingLine
|
||||
from mock.mock_app.routers import work_router
|
||||
|
||||
app: App = App(
|
||||
dividing_line=DynamicDividingLine('^'),
|
||||
dividing_line=StaticDividingLine('~')
|
||||
)
|
||||
orchestrator: Orchestrator = Orchestrator()
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
from argenta import Command, Response, Router
|
||||
from argenta.command import Flag, Flags
|
||||
from argenta.command.flag import ValidationStatus
|
||||
|
||||
work_router: Router = Router(title="Base points:", disable_redirect_stdout=True)
|
||||
work_router: Router = Router(title="Base points:")
|
||||
|
||||
|
||||
@work_router.command(
|
||||
Command(
|
||||
"hello",
|
||||
"hello",
|
||||
flags=Flags([
|
||||
Flag("test")
|
||||
]),
|
||||
description="Hello, world!")
|
||||
)
|
||||
def command_help(response: Response):
|
||||
c = input("Enter your name: ")
|
||||
print(f"Hello, {c}!")
|
||||
n = input('sfgdheth')
|
||||
print(f"Hello,{n} {response.input_flags.get_flag_by_name('test', with_status=ValidationStatus.VALID)}")
|
||||
|
||||
+30
-13
@@ -1,9 +1,9 @@
|
||||
[project]
|
||||
name = "argenta"
|
||||
version = "1.1.2"
|
||||
version = "1.2.0"
|
||||
description = "Python library for building modular CLI applications"
|
||||
authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }]
|
||||
requires-python = ">=3.12"
|
||||
requires-python = ">=3.12,<3.15"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
dependencies = [
|
||||
@@ -14,6 +14,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
{include-group = "linters"},
|
||||
{include-group = "typecheckers"},
|
||||
{include-group = "docs"},
|
||||
{include-group = "tests"},
|
||||
"scriv>=1.8.0",
|
||||
]
|
||||
linters = [
|
||||
"isort>=7.0.0",
|
||||
"ruff>=0.12.12",
|
||||
@@ -35,17 +42,14 @@ tests = [
|
||||
"pytest-cov>=7.0.0",
|
||||
"pytest-mock>=3.15.1",
|
||||
]
|
||||
metrics = [
|
||||
"matplotlib>=3.10.8",
|
||||
"psutil>=7.2.1",
|
||||
"py-cpuinfo>=9.0.0",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
".idea",
|
||||
"venv",
|
||||
".git",
|
||||
"poetry.lock",
|
||||
".__pycache__",
|
||||
"tests"
|
||||
]
|
||||
line-length=90
|
||||
line-length=100
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "strict"
|
||||
@@ -68,6 +72,19 @@ omit = [
|
||||
"src/argenta/metrics/*"
|
||||
]
|
||||
|
||||
[tool.scriv]
|
||||
format = "md"
|
||||
output_file = "CHANGELOG.md"
|
||||
fragment_directory = "changelog.d"
|
||||
categories = [
|
||||
"Added",
|
||||
"Changed",
|
||||
"Deprecated",
|
||||
"Removed",
|
||||
"Fixed",
|
||||
]
|
||||
md_header_level = "2"
|
||||
|
||||
[tool.mypy]
|
||||
disable_error_code = "import-untyped"
|
||||
|
||||
@@ -75,5 +92,5 @@ disable_error_code = "import-untyped"
|
||||
line_length=90
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
requires = ["uv_build"]
|
||||
build-backend = "uv_build"
|
||||
|
||||
@@ -3,12 +3,14 @@ __all__ = ["AutoCompleter"]
|
||||
import sys
|
||||
from typing import Callable, Iterable
|
||||
|
||||
from prompt_toolkit import PromptSession, HTML
|
||||
from prompt_toolkit import HTML, PromptSession
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.completion import Completer, Completion, CompleteEvent
|
||||
from prompt_toolkit.completion import (CompleteEvent, Completer, Completion,
|
||||
ThreadedCompleter)
|
||||
from prompt_toolkit.cursor_shapes import CursorShape
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
||||
from prompt_toolkit.history import History, ThreadedHistory, FileHistory, InMemoryHistory
|
||||
from prompt_toolkit.history import FileHistory, History, InMemoryHistory, ThreadedHistory
|
||||
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
|
||||
from prompt_toolkit.lexers import Lexer
|
||||
from prompt_toolkit.styles import Style
|
||||
@@ -97,34 +99,36 @@ class AutoCompleter:
|
||||
|
||||
def _(event: KeyPressEvent) -> None:
|
||||
buff = event.app.current_buffer
|
||||
|
||||
if buff.complete_state:
|
||||
buff.complete_next()
|
||||
else:
|
||||
completions = list(buff.completer.get_completions(buff.document, CompleteEvent()))
|
||||
if len(completions) == 1:
|
||||
buff.apply_completion(completions[0])
|
||||
else:
|
||||
buff.start_completion(select_first=False)
|
||||
return
|
||||
comps_gen = iter(buff.completer.get_completions(buff.document, CompleteEvent()))
|
||||
try:
|
||||
first = next(comps_gen)
|
||||
except StopIteration:
|
||||
return
|
||||
try:
|
||||
_ = next(comps_gen)
|
||||
buff.start_completion(select_first=False)
|
||||
except StopIteration:
|
||||
buff.apply_completion(first)
|
||||
|
||||
kb.add(self.autocomplete_button)(_)
|
||||
|
||||
history: InMemoryHistory | ThreadedHistory
|
||||
|
||||
if self.history_filename:
|
||||
history = ThreadedHistory(FileHistory(self.history_filename))
|
||||
else:
|
||||
history = InMemoryHistory()
|
||||
|
||||
style = Style.from_dict({'valid': '#00ff00', 'invalid': '#ff0000'})
|
||||
|
||||
self._session = PromptSession(
|
||||
history=history,
|
||||
completer=HistoryCompleter(history, all_commands),
|
||||
completer=ThreadedCompleter(HistoryCompleter(history, all_commands)),
|
||||
complete_while_typing=False,
|
||||
key_bindings=kb,
|
||||
auto_suggest=AutoSuggestFromHistory() if self.auto_suggestions else None,
|
||||
style=style if self.command_highlighting else style,
|
||||
style=style if self.command_highlighting else None,
|
||||
lexer=CommandLexer(all_commands) if self.command_highlighting else None,
|
||||
)
|
||||
|
||||
@@ -134,5 +138,6 @@ class AutoCompleter:
|
||||
if self._session is None:
|
||||
raise RuntimeError("Call initial_setup() before using prompt()")
|
||||
return self._session.prompt(
|
||||
HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text
|
||||
HTML(prompt_text) if isinstance(prompt_text, str) else prompt_text,
|
||||
cursor=CursorShape.BLINKING_BEAM
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
from rich.markup import escape
|
||||
|
||||
from argenta.app.presentation.renderers import Renderer
|
||||
from argenta.app.protocols import (DescriptionMessageGenerator, EmptyCommandHandler,
|
||||
MostSimilarCommandGetter, NonStandardBehaviorHandler,
|
||||
Printer)
|
||||
from argenta.command import InputCommand
|
||||
from argenta.response.entity import Response
|
||||
|
||||
|
||||
class BehaviorHandlersFabric:
|
||||
def __init__(
|
||||
self,
|
||||
printer: Printer,
|
||||
renderer: Renderer,
|
||||
most_similar_command_getter: MostSimilarCommandGetter,
|
||||
) -> None:
|
||||
self._printer = printer
|
||||
self._renderer = renderer
|
||||
self._most_similar_command_getter = most_similar_command_getter
|
||||
|
||||
def generate_incorrect_input_syntax_handler(self) -> NonStandardBehaviorHandler[str]:
|
||||
return lambda raw_command: self._printer(
|
||||
self._renderer.render_text_for_incorrect_input_syntax_handler(
|
||||
raw_command=escape(raw_command)
|
||||
)
|
||||
)
|
||||
|
||||
def generate_repeated_input_flags_handler(self) -> NonStandardBehaviorHandler[str]:
|
||||
return lambda raw_command: self._printer(
|
||||
self._renderer.render_text_for_repeated_input_flags_handler(
|
||||
raw_command=escape(raw_command)
|
||||
)
|
||||
)
|
||||
|
||||
def generate_empty_input_command_handler(self) -> EmptyCommandHandler:
|
||||
return lambda: self._printer(self._renderer.render_text_for_empty_input_command_handler())
|
||||
|
||||
def generate_unknown_command_handler(self) -> NonStandardBehaviorHandler[InputCommand]:
|
||||
def unknown_command_handler(command: InputCommand) -> None:
|
||||
command_trigger: str = command.trigger
|
||||
most_similar_command_trigger: str | None = self._most_similar_command_getter(command_trigger)
|
||||
self._printer(
|
||||
self._renderer.render_text_for_unknown_command_handler(
|
||||
command_trigger=command_trigger,
|
||||
most_similar_command_trigger=most_similar_command_trigger
|
||||
)
|
||||
)
|
||||
return unknown_command_handler
|
||||
|
||||
def generate_exit_command_handler(self, farewell_message: str) -> NonStandardBehaviorHandler[Response]:
|
||||
return lambda _: self._printer(farewell_message)
|
||||
|
||||
def generate_description_message_generator(self) -> DescriptionMessageGenerator:
|
||||
return lambda command, description: self._renderer.render_text_for_description_message_generator(
|
||||
command=command,
|
||||
description=description
|
||||
)
|
||||
|
||||
|
||||
class BehaviorHandlersSettersMixin:
|
||||
def __init__(
|
||||
self,
|
||||
description_message_generator: DescriptionMessageGenerator,
|
||||
incorrect_input_syntax_handler: NonStandardBehaviorHandler[str],
|
||||
repeated_input_flags_handler: NonStandardBehaviorHandler[str],
|
||||
empty_input_command_handler: EmptyCommandHandler,
|
||||
unknown_command_handler: NonStandardBehaviorHandler[InputCommand],
|
||||
exit_command_handler: NonStandardBehaviorHandler[Response]
|
||||
):
|
||||
self._description_message_generator: DescriptionMessageGenerator = description_message_generator
|
||||
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = incorrect_input_syntax_handler
|
||||
self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = repeated_input_flags_handler
|
||||
self._empty_input_command_handler: EmptyCommandHandler = empty_input_command_handler
|
||||
self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = unknown_command_handler
|
||||
self._exit_command_handler: NonStandardBehaviorHandler[Response] = exit_command_handler
|
||||
|
||||
def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None:
|
||||
self._description_message_generator = _
|
||||
|
||||
def set_incorrect_input_syntax_handler(self, _: NonStandardBehaviorHandler[str], /) -> None:
|
||||
self._incorrect_input_syntax_handler = _
|
||||
|
||||
def set_repeated_input_flags_handler(self, _: NonStandardBehaviorHandler[str], /) -> None:
|
||||
self._repeated_input_flags_handler = _
|
||||
|
||||
def set_unknown_command_handler(self, _: NonStandardBehaviorHandler[InputCommand], /) -> None:
|
||||
self._unknown_command_handler = _
|
||||
|
||||
def set_empty_command_handler(self, _: EmptyCommandHandler, /) -> None:
|
||||
"""
|
||||
Public. Sets the handler for empty commands when entering a command
|
||||
:param _: handler for empty commands when entering a command
|
||||
:return: None
|
||||
"""
|
||||
self._empty_input_command_handler = _
|
||||
|
||||
def set_exit_command_handler(self, _: NonStandardBehaviorHandler[Response], /) -> None:
|
||||
"""
|
||||
Public. Sets the handler for exit command when entering a command
|
||||
:param _: handler for exit command when entering a command
|
||||
:return: None
|
||||
"""
|
||||
self._exit_command_handler = _
|
||||
@@ -41,9 +41,9 @@ class StaticDividingLine(BaseDividingLine):
|
||||
:return: full line of dividing line as str
|
||||
"""
|
||||
if is_override:
|
||||
return f"\n{self.length * self.get_unit_part()}\n"
|
||||
return self.length * self.get_unit_part()
|
||||
else:
|
||||
return f"\n[dim]{self.length * self.get_unit_part()}[/dim]\n"
|
||||
return f"[dim]{self.length * self.get_unit_part()}[/dim]"
|
||||
|
||||
|
||||
class DynamicDividingLine(BaseDividingLine):
|
||||
@@ -63,6 +63,6 @@ class DynamicDividingLine(BaseDividingLine):
|
||||
:return: full line of dividing line as str
|
||||
"""
|
||||
if is_override:
|
||||
return f"\n{length * self.get_unit_part()}\n"
|
||||
return length * self.get_unit_part()
|
||||
else:
|
||||
return f"\n[dim]{self.get_unit_part() * length}[/dim]\n"
|
||||
return f"[dim]{self.get_unit_part() * length}[/dim]"
|
||||
|
||||
+105
-365
@@ -1,246 +1,94 @@
|
||||
__all__ = ["App"]
|
||||
|
||||
import io
|
||||
import re
|
||||
from contextlib import redirect_stdout
|
||||
from typing import Callable, Never, TypeAlias
|
||||
import difflib
|
||||
from typing import Never, TypeAlias
|
||||
|
||||
from art import text2art
|
||||
from prompt_toolkit import HTML
|
||||
from rich.console import Console
|
||||
from rich.markup import escape
|
||||
|
||||
from argenta.app.autocompleter import AutoCompleter
|
||||
from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric,
|
||||
BehaviorHandlersSettersMixin)
|
||||
from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine
|
||||
from argenta.app.protocols import (
|
||||
DescriptionMessageGenerator,
|
||||
EmptyCommandHandler,
|
||||
NonStandardBehaviorHandler,
|
||||
Printer,
|
||||
)
|
||||
from argenta.app.presentation.renderers import PlainRenderer, Renderer, RichRenderer
|
||||
from argenta.app.presentation.viewers import Viewer
|
||||
from argenta.app.protocols import Printer
|
||||
from argenta.app.registered_routers.entity import RegisteredRouters
|
||||
from argenta.command.exceptions import (
|
||||
InputCommandException,
|
||||
RepeatedInputFlagsException,
|
||||
UnprocessedInputFlagException,
|
||||
)
|
||||
from argenta.router.exceptions import RepeatedAliasNameException, RepeatedTriggerNameException
|
||||
from argenta.command.exceptions import (InputCommandException,
|
||||
RepeatedInputFlagsException,
|
||||
UnprocessedInputFlagException)
|
||||
from argenta.command.models import Command, InputCommand
|
||||
from argenta.response import Response
|
||||
from argenta.router import Router
|
||||
from argenta.router.exceptions import (RepeatedAliasNameException,
|
||||
RepeatedTriggerNameException)
|
||||
|
||||
Matches: TypeAlias = list[str] | list[Never]
|
||||
|
||||
_ANSI_ESCAPE_RE: re.Pattern[str] = re.compile(r"\u001b\[[0-9;]*m")
|
||||
|
||||
|
||||
class BaseApp:
|
||||
class BaseApp(BehaviorHandlersSettersMixin):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
prompt: str | HTML,
|
||||
prompt: str,
|
||||
initial_message: str,
|
||||
farewell_message: str,
|
||||
exit_command: Command,
|
||||
system_router_title: str,
|
||||
dividing_line: StaticDividingLine | DynamicDividingLine,
|
||||
dividing_line: StaticDividingLine | DynamicDividingLine | None,
|
||||
repeat_command_groups_printing: bool,
|
||||
override_system_messages: bool,
|
||||
autocompleter: AutoCompleter,
|
||||
print_func: Printer,
|
||||
printer: Printer,
|
||||
) -> None:
|
||||
self._prompt: str | HTML = prompt
|
||||
self._print_func: Printer = print_func
|
||||
self._prompt: str = prompt
|
||||
self._printer: Printer = printer
|
||||
self._exit_command: Command = exit_command
|
||||
self._dividing_line: StaticDividingLine | DynamicDividingLine = dividing_line
|
||||
self._dividing_line: StaticDividingLine | DynamicDividingLine | None = dividing_line
|
||||
self._repeat_command_groups_printing: bool = repeat_command_groups_printing
|
||||
self._override_system_messages: bool = override_system_messages
|
||||
self._autocompleter: AutoCompleter = autocompleter
|
||||
self.system_router: Router = Router(title=system_router_title)
|
||||
self._system_router: Router = Router(title=system_router_title)
|
||||
|
||||
self._farewell_message: str = farewell_message
|
||||
self._initial_message: str = initial_message
|
||||
|
||||
self._stdout_buffer: io.StringIO = io.StringIO()
|
||||
|
||||
self._description_message_gen: DescriptionMessageGenerator = (
|
||||
lambda command, description: f"{command} *=*=* {description}"
|
||||
)
|
||||
self.registered_routers: RegisteredRouters = RegisteredRouters()
|
||||
self._messages_on_startup: list[str] = []
|
||||
|
||||
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = (
|
||||
lambda _: print_func(f"Incorrect flag syntax: {_}")
|
||||
self._renderer: Renderer = PlainRenderer() if self._override_system_messages else RichRenderer()
|
||||
|
||||
self._viewer: Viewer = Viewer(
|
||||
printer=self._printer,
|
||||
renderer=self._renderer,
|
||||
dividing_line=self._dividing_line,
|
||||
override_system_messages=self._override_system_messages,
|
||||
)
|
||||
self._repeated_input_flags_handler: NonStandardBehaviorHandler[str] = (
|
||||
lambda _: print_func(f"Repeated input flags: {_}")
|
||||
)
|
||||
self._empty_input_command_handler: EmptyCommandHandler = lambda: print_func(
|
||||
"Empty input command"
|
||||
)
|
||||
self._unknown_command_handler: NonStandardBehaviorHandler[InputCommand] = (
|
||||
lambda _: print_func(f"Unknown command: {_.trigger}")
|
||||
)
|
||||
self._exit_command_handler: NonStandardBehaviorHandler[Response] = (
|
||||
lambda _: print_func(self._farewell_message)
|
||||
self._handlers_fabric: BehaviorHandlersFabric = BehaviorHandlersFabric(
|
||||
printer=self._printer,
|
||||
renderer=self._renderer,
|
||||
most_similar_command_getter=self._most_similar_command
|
||||
)
|
||||
|
||||
def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None:
|
||||
"""
|
||||
Public. Sets the output pattern of the available commands
|
||||
:param _: output pattern of the available commands
|
||||
:return: None
|
||||
"""
|
||||
self._description_message_gen = _
|
||||
self._initial_message: str = self._renderer.render_initial_message(initial_message)
|
||||
self._farewell_message: str = self._renderer.render_farewell_message(farewell_message)
|
||||
|
||||
def set_incorrect_input_syntax_handler(
|
||||
self, _: NonStandardBehaviorHandler[str], /
|
||||
) -> None:
|
||||
"""
|
||||
Public. Sets the handler for incorrect flags when entering a command
|
||||
:param _: handler for incorrect flags when entering a command
|
||||
:return: None
|
||||
"""
|
||||
self._incorrect_input_syntax_handler = _
|
||||
|
||||
def set_repeated_input_flags_handler(
|
||||
self, _: NonStandardBehaviorHandler[str], /
|
||||
) -> None:
|
||||
"""
|
||||
Public. Sets the handler for repeated flags when entering a command
|
||||
:param _: handler for repeated flags when entering a command
|
||||
:return: None
|
||||
"""
|
||||
self._repeated_input_flags_handler = _
|
||||
|
||||
def set_unknown_command_handler(
|
||||
self, _: NonStandardBehaviorHandler[InputCommand], /
|
||||
) -> None:
|
||||
"""
|
||||
Public. Sets the handler for unknown commands when entering a command
|
||||
:param _: handler for unknown commands when entering a command
|
||||
:return: None
|
||||
"""
|
||||
self._unknown_command_handler = _
|
||||
|
||||
def set_empty_command_handler(self, _: EmptyCommandHandler, /) -> None:
|
||||
"""
|
||||
Public. Sets the handler for empty commands when entering a command
|
||||
:param _: handler for empty commands when entering a command
|
||||
:return: None
|
||||
"""
|
||||
self._empty_input_command_handler = _
|
||||
|
||||
def set_exit_command_handler(
|
||||
self, _: NonStandardBehaviorHandler[Response], /
|
||||
) -> None:
|
||||
"""
|
||||
Public. Sets the handler for exit command when entering a command
|
||||
:param _: handler for exit command when entering a command
|
||||
:return: None
|
||||
"""
|
||||
self._exit_command_handler = _
|
||||
|
||||
def _print_command_group_description(self) -> None:
|
||||
"""
|
||||
Private. Prints the description of the available commands
|
||||
:return: None
|
||||
"""
|
||||
for registered_router in self.registered_routers:
|
||||
self._print_func(registered_router.title)
|
||||
for command_handler in registered_router.command_handlers:
|
||||
handled_command = command_handler.handled_command
|
||||
self._print_func(
|
||||
self._description_message_gen(
|
||||
handled_command.trigger,
|
||||
handled_command.description,
|
||||
)
|
||||
)
|
||||
self._print_func("")
|
||||
|
||||
def _print_framed_text(self, text: str) -> None:
|
||||
"""
|
||||
Private. Outputs text by framing it in a static or dynamic split strip
|
||||
:param text: framed text
|
||||
:return: None
|
||||
"""
|
||||
if isinstance(self._dividing_line, DynamicDividingLine):
|
||||
clear_text = _ANSI_ESCAPE_RE.sub("", text)
|
||||
max_length_line = max([len(line) for line in clear_text.split("\n")])
|
||||
max_length_line = (
|
||||
max_length_line
|
||||
if 10 <= max_length_line <= 80
|
||||
else 80
|
||||
if max_length_line > 80
|
||||
else 10
|
||||
)
|
||||
|
||||
self._print_func(
|
||||
self._dividing_line.get_full_dynamic_line(
|
||||
length=max_length_line, is_override=self._override_system_messages
|
||||
)
|
||||
)
|
||||
print(text.strip("\n"))
|
||||
self._print_func(
|
||||
self._dividing_line.get_full_dynamic_line(
|
||||
length=max_length_line, is_override=self._override_system_messages
|
||||
)
|
||||
)
|
||||
|
||||
elif isinstance(self._dividing_line, StaticDividingLine): # pyright: ignore[reportUnnecessaryIsInstance]
|
||||
self._print_func(
|
||||
self._dividing_line.get_full_static_line(
|
||||
is_override=self._override_system_messages
|
||||
)
|
||||
)
|
||||
print(text.strip("\n"))
|
||||
self._print_func(
|
||||
self._dividing_line.get_full_static_line(
|
||||
is_override=self._override_system_messages
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
super().__init__(
|
||||
description_message_generator = self._handlers_fabric.generate_description_message_generator(),
|
||||
incorrect_input_syntax_handler = self._handlers_fabric.generate_incorrect_input_syntax_handler(),
|
||||
repeated_input_flags_handler = self._handlers_fabric.generate_repeated_input_flags_handler(),
|
||||
empty_input_command_handler = self._handlers_fabric.generate_empty_input_command_handler(),
|
||||
unknown_command_handler = self._handlers_fabric.generate_unknown_command_handler(),
|
||||
exit_command_handler = self._handlers_fabric.generate_exit_command_handler(self._farewell_message)
|
||||
)
|
||||
|
||||
def _is_exit_command(self, command: InputCommand) -> bool:
|
||||
"""
|
||||
Private. Checks if the given command is an exit command
|
||||
:param command: command to check
|
||||
:return: is it an exit command or not as bool
|
||||
"""
|
||||
trigger = command.trigger
|
||||
exit_trigger = self._exit_command.trigger
|
||||
if trigger.lower() == exit_trigger.lower():
|
||||
return True
|
||||
elif trigger.lower() in [x.lower() for x in self._exit_command.aliases]:
|
||||
return True
|
||||
return False
|
||||
|
||||
if not self._system_router.command_handlers.get_command_handler_by_trigger(command.trigger.lower()):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _is_unknown_command(self, input_command: InputCommand) -> bool:
|
||||
if not self.registered_routers.get_router_by_trigger(input_command.trigger.lower()):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _capture_stdout(self, func: Callable[[], None]) -> str:
|
||||
"""
|
||||
Private. Captures stdout from a function call using a reusable buffer
|
||||
:param func: function to execute with captured stdout
|
||||
:return: captured stdout as string
|
||||
"""
|
||||
self._stdout_buffer.seek(0)
|
||||
self._stdout_buffer.truncate(0)
|
||||
with redirect_stdout(self._stdout_buffer):
|
||||
func()
|
||||
return self._stdout_buffer.getvalue()
|
||||
|
||||
def _error_handler(self, error: InputCommandException, raw_command: str) -> None:
|
||||
"""
|
||||
Private. Handles parsing errors of the entered command
|
||||
:param error: error being handled
|
||||
:param raw_command: the raw input command
|
||||
:return: None
|
||||
"""
|
||||
if isinstance(error, UnprocessedInputFlagException):
|
||||
self._incorrect_input_syntax_handler(raw_command)
|
||||
elif isinstance(error, RepeatedInputFlagsException):
|
||||
@@ -248,172 +96,100 @@ class BaseApp:
|
||||
else:
|
||||
self._empty_input_command_handler()
|
||||
|
||||
def _setup_system_router(self) -> None:
|
||||
"""
|
||||
Private. Sets up system router
|
||||
:return: None
|
||||
"""
|
||||
|
||||
@self.system_router.command(self._exit_command)
|
||||
def _(response: Response) -> None:
|
||||
self._exit_command_handler(response)
|
||||
|
||||
self.registered_routers.add_registered_router(self.system_router)
|
||||
|
||||
def _validate_routers_for_collisions(self) -> None:
|
||||
"""
|
||||
Private. Validates that there are no trigger/alias collisions between routers
|
||||
:return: None
|
||||
:raises: RepeatedTriggerNameException or RepeatedAliasNameException if collision detected
|
||||
"""
|
||||
|
||||
all_triggers: set[str] = set()
|
||||
all_aliases: set[str] = set()
|
||||
|
||||
seen_names: set[str] = set()
|
||||
|
||||
for router_entity in self.registered_routers:
|
||||
union_units: set[str] = all_triggers | all_aliases
|
||||
trigger_collisions: set[str] = union_units & router_entity.triggers
|
||||
if trigger_collisions:
|
||||
if not seen_names.isdisjoint(router_entity.triggers):
|
||||
raise RepeatedTriggerNameException()
|
||||
|
||||
alias_collisions: set[str] = union_units & router_entity.aliases
|
||||
|
||||
alias_collisions = seen_names.intersection(router_entity.aliases)
|
||||
if alias_collisions:
|
||||
raise RepeatedAliasNameException(alias_collisions)
|
||||
|
||||
all_triggers.update(router_entity.triggers)
|
||||
all_aliases.update(router_entity.aliases)
|
||||
|
||||
seen_names.update(router_entity.triggers)
|
||||
seen_names.update(router_entity.aliases)
|
||||
|
||||
def _most_similar_command(self, unknown_command: str) -> str | None:
|
||||
all_commands = self.registered_routers.get_triggers()
|
||||
matches = difflib.get_close_matches(unknown_command, all_commands, n=1)
|
||||
return matches[0] if matches else None
|
||||
|
||||
matches_startswith_unknown_command: Matches = sorted(
|
||||
cmd for cmd in all_commands if cmd.startswith(unknown_command)
|
||||
)
|
||||
matches_startswith_cmd: Matches = sorted(
|
||||
cmd for cmd in all_commands if unknown_command.startswith(cmd)
|
||||
)
|
||||
def _setup_system_router(self) -> None:
|
||||
@self._system_router.command(self._exit_command)
|
||||
def _(response: Response) -> None:
|
||||
self._exit_command_handler(response)
|
||||
|
||||
matches: Matches = matches_startswith_unknown_command or matches_startswith_cmd
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
elif len(matches) > 1:
|
||||
return sorted(matches, key=lambda cmd: len(cmd))[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def _setup_default_view(self) -> None:
|
||||
"""
|
||||
Private. Sets up default app view
|
||||
:return: None
|
||||
"""
|
||||
self._prompt = f"<gray><b>{self._prompt}</b></gray>"
|
||||
self._initial_message = (
|
||||
"\n" + f"[bold red]{text2art(self._initial_message, font='tarty1')}" + "\n"
|
||||
)
|
||||
self._farewell_message = (
|
||||
"[bold red]\n\n"
|
||||
+ str(text2art(self._farewell_message, font="chanky")) # pyright: ignore[reportUnknownArgumentType]
|
||||
+ "\n[/bold red]\n"
|
||||
+ "[red i]github.com/koloideal/Argenta[/red i] | [red bold i]made by kolo[/red bold i]\n"
|
||||
)
|
||||
self._description_message_gen = lambda command, description: (
|
||||
f"[bold red]{escape('[' + command + ']')}[/bold red] "
|
||||
f"[blue dim]*=*=*[/blue dim] "
|
||||
f"[bold yellow italic]{escape(description)}"
|
||||
)
|
||||
self._incorrect_input_syntax_handler = lambda raw_command: self._print_func(
|
||||
f"[red bold]Incorrect flag syntax: {escape(raw_command)}"
|
||||
)
|
||||
self._repeated_input_flags_handler = lambda raw_command: self._print_func(
|
||||
f"[red bold]Repeated input flags: {escape(raw_command)}"
|
||||
)
|
||||
self._empty_input_command_handler = lambda: self._print_func(
|
||||
"[red bold]Empty input command"
|
||||
)
|
||||
|
||||
def unknown_command_handler(command: InputCommand) -> None:
|
||||
cmd_trg: str = command.trigger
|
||||
mst_sim_cmd: str | None = self._most_similar_command(cmd_trg)
|
||||
first_part_of_text = (
|
||||
f"[red]Unknown command:[/red] [blue]{escape(cmd_trg)}[/blue]"
|
||||
)
|
||||
second_part_of_text = (
|
||||
("[red], most similar:[/red] " + ("[blue]" + mst_sim_cmd + "[/blue]"))
|
||||
if mst_sim_cmd
|
||||
else ""
|
||||
)
|
||||
self._print_func(first_part_of_text + second_part_of_text)
|
||||
|
||||
self._unknown_command_handler = unknown_command_handler
|
||||
self.registered_routers.add_registered_router(self._system_router)
|
||||
|
||||
def _pre_cycle_setup(self) -> None:
|
||||
"""
|
||||
Private. Configures various aspects of the application before the start of the cycle
|
||||
:return: None
|
||||
"""
|
||||
self._setup_system_router()
|
||||
self._validate_routers_for_collisions()
|
||||
|
||||
self._autocompleter.initial_setup(self.registered_routers.get_triggers())
|
||||
|
||||
self._print_func(self._initial_message)
|
||||
|
||||
for message in self._messages_on_startup:
|
||||
self._print_func(message)
|
||||
if self._messages_on_startup:
|
||||
print("\n")
|
||||
self._viewer.view_messages_on_startup(self._messages_on_startup)
|
||||
|
||||
if not self._repeat_command_groups_printing:
|
||||
self._print_command_group_description()
|
||||
self._viewer.view_command_groups_description(self._description_message_generator, self.registered_routers)
|
||||
|
||||
def _process_exist_and_valid_command(self, input_command: InputCommand) -> None:
|
||||
processing_router = self.registered_routers.get_router_by_trigger(input_command.trigger.lower())
|
||||
|
||||
|
||||
if not processing_router:
|
||||
raise RuntimeError(f"Router for '{input_command.trigger}' not found. Panic!")
|
||||
|
||||
if processing_router.disable_redirect_stdout:
|
||||
dividing_line_unit_part: str = self._dividing_line.get_unit_part()
|
||||
self._print_func(
|
||||
StaticDividingLine(dividing_line_unit_part).get_full_static_line(
|
||||
is_override=self._override_system_messages
|
||||
self._viewer.view_framed_text_from_generator(
|
||||
output_text_generator=lambda: processing_router.finds_appropriate_handler(input_command),
|
||||
is_stdout_redirected_by_router=processing_router.is_redirect_stdout_disabled
|
||||
)
|
||||
|
||||
def _run_polling(self) -> None:
|
||||
self._viewer.view_initial_message(self._initial_message)
|
||||
self._pre_cycle_setup()
|
||||
while True:
|
||||
if self._repeat_command_groups_printing:
|
||||
self._viewer.view_command_groups_description(self._description_message_generator, self.registered_routers)
|
||||
|
||||
print() # pre-prompt gap
|
||||
raw_command: str = self._autocompleter.prompt(self._renderer.render_prompt(self._prompt))
|
||||
print() # post-prompt gap
|
||||
|
||||
try:
|
||||
input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
|
||||
except InputCommandException as error: # noqa F841
|
||||
self._viewer.view_framed_text_from_generator(
|
||||
output_text_generator=lambda: self._error_handler(error, raw_command) # noqa
|
||||
)
|
||||
)
|
||||
processing_router.finds_appropriate_handler(input_command)
|
||||
self._print_func(
|
||||
StaticDividingLine(dividing_line_unit_part).get_full_static_line(
|
||||
is_override=self._override_system_messages
|
||||
continue
|
||||
|
||||
if self._is_unknown_command(input_command):
|
||||
self._viewer.view_framed_text_from_generator(
|
||||
output_text_generator=lambda: self._unknown_command_handler(input_command)
|
||||
)
|
||||
)
|
||||
else:
|
||||
stdout_result = self._capture_stdout(
|
||||
lambda: processing_router.finds_appropriate_handler(input_command)
|
||||
)
|
||||
self._print_framed_text(stdout_result)
|
||||
continue
|
||||
|
||||
if self._is_exit_command(input_command):
|
||||
self._system_router.finds_appropriate_handler(input_command)
|
||||
return
|
||||
|
||||
AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine
|
||||
DEFAULT_DIVIDING_LINE: StaticDividingLine = StaticDividingLine()
|
||||
|
||||
DEFAULT_PRINT_FUNC: Printer = Console().print
|
||||
DEFAULT_AUTOCOMPLETER: AutoCompleter = AutoCompleter()
|
||||
DEFAULT_EXIT_COMMAND: Command = Command("q", description="Exit command")
|
||||
self._process_exist_and_valid_command(input_command)
|
||||
|
||||
|
||||
class App(BaseApp):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
prompt: str | HTML = ">>> ",
|
||||
initial_message: str = "Argenta\n",
|
||||
farewell_message: str = "\nSee you\n",
|
||||
exit_command: Command = DEFAULT_EXIT_COMMAND,
|
||||
prompt: str = ">>> ",
|
||||
initial_message: str = "Argenta",
|
||||
farewell_message: str = "See you",
|
||||
exit_command: Command = Command("q", description="Exit command"),
|
||||
system_router_title: str = "System points:",
|
||||
dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE,
|
||||
dividing_line: StaticDividingLine | DynamicDividingLine | None = None,
|
||||
repeat_command_groups_printing: bool = False,
|
||||
override_system_messages: bool = False,
|
||||
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
|
||||
print_func: Printer = DEFAULT_PRINT_FUNC,
|
||||
autocompleter: AutoCompleter | None = None,
|
||||
printer: Printer = Console().print,
|
||||
) -> None:
|
||||
"""
|
||||
Public. The essence of the application itself.
|
||||
@@ -427,7 +203,7 @@ class App(BaseApp):
|
||||
: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 autocompleter: the entity of the autocompleter
|
||||
:param print_func: system messages text output function
|
||||
:param printer: system messages text output function
|
||||
:return: None
|
||||
"""
|
||||
super().__init__(
|
||||
@@ -439,45 +215,9 @@ class App(BaseApp):
|
||||
dividing_line=dividing_line,
|
||||
repeat_command_groups_printing=repeat_command_groups_printing,
|
||||
override_system_messages=override_system_messages,
|
||||
autocompleter=autocompleter,
|
||||
print_func=print_func,
|
||||
autocompleter=autocompleter or AutoCompleter(),
|
||||
printer=printer,
|
||||
)
|
||||
if not self._override_system_messages:
|
||||
self._setup_default_view()
|
||||
|
||||
def run_polling(self) -> None:
|
||||
"""
|
||||
Private. Starts the user input processing cycle
|
||||
:return: None
|
||||
"""
|
||||
self._pre_cycle_setup()
|
||||
while True:
|
||||
if self._repeat_command_groups_printing:
|
||||
self._print_command_group_description()
|
||||
|
||||
raw_command: str = self._autocompleter.prompt(self._prompt)
|
||||
|
||||
try:
|
||||
input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
|
||||
except InputCommandException as error: # noqa F841
|
||||
stderr_result = self._capture_stdout(
|
||||
lambda: self._error_handler(error, raw_command) # noqa F821
|
||||
)
|
||||
self._print_framed_text(stderr_result)
|
||||
continue
|
||||
|
||||
if self._is_exit_command(input_command):
|
||||
self.system_router.finds_appropriate_handler(input_command)
|
||||
return
|
||||
|
||||
if self._is_unknown_command(input_command):
|
||||
stdout_res = self._capture_stdout(
|
||||
lambda: self._unknown_command_handler(input_command)
|
||||
)
|
||||
self._print_framed_text(stdout_res)
|
||||
continue
|
||||
|
||||
self._process_exist_and_valid_command(input_command)
|
||||
|
||||
def include_router(self, router: Router) -> None:
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .renderers import PlainRenderer, Renderer, RichRenderer
|
||||
from .viewers import Viewer
|
||||
|
||||
__all__ = ["Renderer", "RichRenderer", "PlainRenderer", "Viewer"]
|
||||
@@ -0,0 +1,182 @@
|
||||
from typing import Iterable, Protocol
|
||||
|
||||
from art import text2art
|
||||
|
||||
from argenta.app.protocols import DescriptionMessageGenerator
|
||||
from argenta.app.registered_routers.entity import RegisteredRouters
|
||||
|
||||
|
||||
class Renderer(Protocol):
|
||||
@staticmethod
|
||||
def render_prompt(
|
||||
text: str
|
||||
) -> str: ...
|
||||
@staticmethod
|
||||
def render_initial_message(
|
||||
text: str
|
||||
) -> str: ...
|
||||
@staticmethod
|
||||
def render_farewell_message(
|
||||
text: str
|
||||
) -> str: ...
|
||||
@staticmethod
|
||||
def render_messages_on_startup(
|
||||
messages: Iterable[str]
|
||||
) -> str: ...
|
||||
@staticmethod
|
||||
def render_text_for_description_message_generator(
|
||||
command: str,
|
||||
description: str
|
||||
) -> str: ...
|
||||
@staticmethod
|
||||
def render_command_groups_description(
|
||||
description_message_generator: DescriptionMessageGenerator,
|
||||
registered_routers: RegisteredRouters
|
||||
) -> str: ...
|
||||
@staticmethod
|
||||
def render_text_for_incorrect_input_syntax_handler(
|
||||
raw_command: str
|
||||
) -> str: ...
|
||||
@staticmethod
|
||||
def render_text_for_repeated_input_flags_handler(
|
||||
raw_command: str
|
||||
) -> str: ...
|
||||
@staticmethod
|
||||
def render_text_for_empty_input_command_handler() -> str: ...
|
||||
@staticmethod
|
||||
def render_text_for_unknown_command_handler(
|
||||
command_trigger: str,
|
||||
most_similar_command_trigger: str | None
|
||||
) -> str: ...
|
||||
|
||||
|
||||
class RichRenderer(Renderer):
|
||||
@staticmethod
|
||||
def render_prompt(text: str) -> str:
|
||||
return f"<gray><b>{text}</b></gray>"
|
||||
|
||||
@staticmethod
|
||||
def render_initial_message(text: str) -> str:
|
||||
return f"[bold red]{text2art(text, font='tarty1')}[/bold red]"
|
||||
|
||||
@staticmethod
|
||||
def render_farewell_message(text: str) -> str:
|
||||
return (
|
||||
"[bold red]"
|
||||
+ str(text2art(text, font="chanky"))
|
||||
+ "[/bold red]\n"
|
||||
+ "[red i]https://github.com/koloideal/Argenta[/red i] | [red bold i]made by kolo[/red bold i]"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def render_text_for_description_message_generator(command: str, description: str) -> str:
|
||||
return (
|
||||
f"[bold red]<{command}>[/bold red] "
|
||||
f"[blue dim]*=*=*[/blue dim] "
|
||||
f"[bold yellow italic]{description}[/bold yellow italic]"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def render_messages_on_startup(messages: Iterable[str]) -> str:
|
||||
return "\n" + "\n".join(messages)
|
||||
|
||||
@staticmethod
|
||||
def render_command_groups_description(
|
||||
description_message_generator: DescriptionMessageGenerator,
|
||||
registered_routers: RegisteredRouters
|
||||
) -> str:
|
||||
command_groups_description = ""
|
||||
for registered_router in registered_routers:
|
||||
command_groups_description += "\n\n" + registered_router.title
|
||||
for command_handler in registered_router.command_handlers:
|
||||
handled_command = command_handler.handled_command
|
||||
command_groups_description += '\n' + description_message_generator(
|
||||
handled_command.trigger,
|
||||
handled_command.description,
|
||||
)
|
||||
return command_groups_description
|
||||
|
||||
@staticmethod
|
||||
def render_text_for_incorrect_input_syntax_handler(raw_command: str) -> str:
|
||||
return f"[red bold]Incorrect flag syntax: {raw_command}[/red bold]"
|
||||
|
||||
@staticmethod
|
||||
def render_text_for_repeated_input_flags_handler(raw_command: str) -> str:
|
||||
return f"[red bold]Repeated input flags: {raw_command}[/red bold]"
|
||||
|
||||
@staticmethod
|
||||
def render_text_for_empty_input_command_handler() -> str:
|
||||
return "[red bold]Empty input command[/red bold]"
|
||||
|
||||
@staticmethod
|
||||
def render_text_for_unknown_command_handler(
|
||||
command_trigger: str,
|
||||
most_similar_command_trigger: str | None
|
||||
) -> str:
|
||||
return (
|
||||
f"[red]Unknown command:[/red] [blue]{command_trigger}[/blue]"
|
||||
+ (f"[red], most similar:[/red] [blue]{most_similar_command_trigger}[/blue]"
|
||||
if most_similar_command_trigger else "")
|
||||
)
|
||||
|
||||
|
||||
class PlainRenderer(Renderer):
|
||||
@staticmethod
|
||||
def render_prompt(text: str) -> str:
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def render_initial_message(text: str) -> str:
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def render_farewell_message(text: str) -> str:
|
||||
return f"\n{text} | https://github.com/koloideal/Argenta | made by kolo"
|
||||
|
||||
@staticmethod
|
||||
def render_text_for_description_message_generator(command: str, description: str) -> str:
|
||||
return f"{command} *=*=* {description}"
|
||||
|
||||
@staticmethod
|
||||
def render_messages_on_startup(messages: Iterable[str]) -> str:
|
||||
return "\n" + "\n".join(messages)
|
||||
|
||||
@staticmethod
|
||||
def render_command_groups_description(
|
||||
description_message_generator: DescriptionMessageGenerator,
|
||||
registered_routers: RegisteredRouters,
|
||||
) -> str:
|
||||
command_groups_description = ""
|
||||
for registered_router in registered_routers:
|
||||
command_groups_description += "\n\n" + registered_router.title
|
||||
for command_handler in registered_router.command_handlers:
|
||||
handled_command = command_handler.handled_command
|
||||
command_groups_description += "\n" + description_message_generator(
|
||||
handled_command.trigger,
|
||||
handled_command.description,
|
||||
)
|
||||
return command_groups_description
|
||||
|
||||
@staticmethod
|
||||
def render_text_for_incorrect_input_syntax_handler(raw_command: str) -> str:
|
||||
return f"Incorrect flag syntax: {raw_command}"
|
||||
|
||||
@staticmethod
|
||||
def render_text_for_repeated_input_flags_handler(raw_command: str) -> str:
|
||||
return f"Repeated input flags: {raw_command}"
|
||||
|
||||
@staticmethod
|
||||
def render_text_for_empty_input_command_handler() -> str:
|
||||
return "Empty input command"
|
||||
|
||||
@staticmethod
|
||||
def render_text_for_unknown_command_handler(
|
||||
command_trigger: str,
|
||||
most_similar_command_trigger: str | None
|
||||
) -> str:
|
||||
return (
|
||||
f"Unknown command: {command_trigger}"
|
||||
+ (f", most similar: {most_similar_command_trigger}"
|
||||
if most_similar_command_trigger else "")
|
||||
)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
__all__ = ["Viewer"]
|
||||
|
||||
import re
|
||||
from contextlib import redirect_stdout
|
||||
from io import StringIO
|
||||
from typing import Callable, Iterable, TypeAlias
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from argenta.app import DynamicDividingLine, StaticDividingLine
|
||||
from argenta.app.presentation.renderers import Renderer
|
||||
from argenta.app.protocols import DescriptionMessageGenerator, Printer
|
||||
from argenta.app.registered_routers.entity import RegisteredRouters
|
||||
|
||||
AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine | None
|
||||
|
||||
|
||||
class Viewer:
|
||||
ANSI_ESCAPE_RE: re.Pattern[str] = re.compile(r"\u001b\[[0-9;]*m")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
printer: Printer,
|
||||
renderer: Renderer,
|
||||
dividing_line: AVAILABLE_DIVIDING_LINES,
|
||||
override_system_messages: bool
|
||||
):
|
||||
self._printer = printer
|
||||
self._renderer = renderer
|
||||
self._dividing_line = dividing_line
|
||||
self._override_system_messages = override_system_messages
|
||||
self._stdout_buffer: StringIO = StringIO()
|
||||
|
||||
def _capture_stdout(self, func: Callable[[], None]) -> str:
|
||||
self._stdout_buffer.seek(0)
|
||||
self._stdout_buffer.truncate(0)
|
||||
with redirect_stdout(self._stdout_buffer):
|
||||
func()
|
||||
return self._stdout_buffer.getvalue()
|
||||
|
||||
def view_messages_on_startup(self, messages: Iterable[str]) -> None:
|
||||
self._printer(self._renderer.render_messages_on_startup(messages))
|
||||
|
||||
def view_command_groups_description(
|
||||
self,
|
||||
description_message_generator: DescriptionMessageGenerator,
|
||||
registered_routers: RegisteredRouters
|
||||
) -> None:
|
||||
self._printer(
|
||||
self._renderer.render_command_groups_description(
|
||||
description_message_generator,
|
||||
registered_routers
|
||||
)
|
||||
)
|
||||
|
||||
def view_initial_message(self, initial_message: str) -> None:
|
||||
self._printer(initial_message)
|
||||
|
||||
def view_framed_text_from_generator(
|
||||
self,
|
||||
output_text_generator: Callable[[], None],
|
||||
is_stdout_redirected_by_router: bool = False,
|
||||
) -> None:
|
||||
match (self._dividing_line, is_stdout_redirected_by_router):
|
||||
case (None, bool()):
|
||||
output_text_generator()
|
||||
case (DynamicDividingLine(), False):
|
||||
stdout_result = self._capture_stdout(
|
||||
lambda: output_text_generator()
|
||||
)
|
||||
clear_text = self.ANSI_ESCAPE_RE.sub("", stdout_result)
|
||||
max_length_line = max([len(line) for line in clear_text.split("\n")])
|
||||
max_length_line = (
|
||||
max_length_line
|
||||
if 10 <= max_length_line <= 100
|
||||
else 100
|
||||
if max_length_line > 100
|
||||
else 10
|
||||
)
|
||||
dynamic_dividing_line_as_str: str = self._dividing_line.get_full_dynamic_line(
|
||||
length=max_length_line, is_override=self._override_system_messages
|
||||
)
|
||||
self._printer(dynamic_dividing_line_as_str + "\n")
|
||||
self._printer(Text.from_ansi(stdout_result.strip("\n")).markup)
|
||||
self._printer('\n' + dynamic_dividing_line_as_str)
|
||||
|
||||
case (StaticDividingLine() as dividing_line, bool()) | (DynamicDividingLine() as dividing_line, True):
|
||||
static_dividing_line_as_str: str = StaticDividingLine(dividing_line.get_unit_part()).get_full_static_line(
|
||||
is_override=self._override_system_messages
|
||||
)
|
||||
self._printer(static_dividing_line_as_str + '\n')
|
||||
output_text_generator()
|
||||
self._printer('\n' + static_dividing_line_as_str)
|
||||
case _:
|
||||
raise NotImplementedError(f"Dividing line with type {self._dividing_line} is not implemented")
|
||||
@@ -1,10 +1,17 @@
|
||||
__all__ = ["NonStandardBehaviorHandler", "EmptyCommandHandler", "Printer", "DescriptionMessageGenerator", "HandlerFunc"]
|
||||
__all__ = [
|
||||
"NonStandardBehaviorHandler",
|
||||
"EmptyCommandHandler",
|
||||
"MostSimilarCommandGetter",
|
||||
"Printer",
|
||||
"DescriptionMessageGenerator",
|
||||
"HandlerFunc",
|
||||
]
|
||||
|
||||
from typing import Any, Protocol, TypeVar
|
||||
|
||||
from typing import ParamSpec, Protocol, TypeVar
|
||||
from argenta.response import Response
|
||||
|
||||
T = TypeVar("T", contravariant=True)
|
||||
P = ParamSpec("P")
|
||||
T = TypeVar("T", contravariant=True)
|
||||
|
||||
|
||||
class NonStandardBehaviorHandler(Protocol[T]):
|
||||
@@ -22,11 +29,16 @@ class Printer(Protocol):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class MostSimilarCommandGetter(Protocol):
|
||||
def __call__(self, _unknown_trigger: str, /) -> str | None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DescriptionMessageGenerator(Protocol):
|
||||
def __call__(self, _command: str, _description: str, /) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HandlerFunc(Protocol):
|
||||
def __call__(self, response: Response) -> None:
|
||||
def __call__(self, response: Response, /, *args: Any, **kwargs: Any) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from argenta.command.flag import Flag as Flag
|
||||
from argenta.command.flag import Flags as Flags
|
||||
from argenta.command.flag import InputFlag as InputFlag
|
||||
from argenta.command.flag import InputFlags as InputFlags
|
||||
from argenta.command.flag import PossibleValues as PossibleValues
|
||||
from argenta.command.flag.defaults import PredefinedFlags as PredefinedFlags
|
||||
from argenta.command.flag.models import Flags as Flags
|
||||
from argenta.command.flag.models import InputFlags as InputFlags
|
||||
from argenta.command.models import Command as Command
|
||||
from argenta.command.models import InputCommand as InputCommand
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from argenta.command.flag.flags.models import Flags as Flags
|
||||
from argenta.command.flag.flags.models import InputFlags as InputFlags
|
||||
from argenta.command.flag.models import Flag as Flag
|
||||
from argenta.command.flag.models import Flags as Flags
|
||||
from argenta.command.flag.models import InputFlag as InputFlag
|
||||
from argenta.command.flag.models import InputFlags as InputFlags
|
||||
from argenta.command.flag.models import PossibleValues as PossibleValues
|
||||
from argenta.command.flag.models import ValidationStatus as ValidationStatus
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from argenta.command.flag.flags.models import Flags as Flags
|
||||
from argenta.command.flag.flags.models import InputFlags as InputFlags
|
||||
@@ -1,107 +0,0 @@
|
||||
__all__ = ["Flags", "InputFlags"]
|
||||
|
||||
from collections.abc import Iterator
|
||||
from typing import Generic, TypeVar, override
|
||||
|
||||
from argenta.command.flag.models import Flag, InputFlag
|
||||
|
||||
FlagType = TypeVar("FlagType")
|
||||
|
||||
|
||||
class BaseFlags(Generic[FlagType]):
|
||||
def __init__(self, flags: list[FlagType] | None = None) -> None:
|
||||
"""
|
||||
Public. A model that combines the registered flags
|
||||
:param flags: the flags that will be registered
|
||||
:return: None
|
||||
"""
|
||||
self.flags: list[FlagType] = flags if flags else []
|
||||
|
||||
def add_flag(self, flag: FlagType) -> None:
|
||||
"""
|
||||
Public. Adds a flag to the list of flags
|
||||
:param flag: flag to add
|
||||
:return: None
|
||||
"""
|
||||
self.flags.append(flag)
|
||||
|
||||
def add_flags(self, flags: list[FlagType]) -> None:
|
||||
"""
|
||||
Public. Adds a list of flags to the list of flags
|
||||
:param flags: list of flags to add
|
||||
:return: None
|
||||
"""
|
||||
self.flags.extend(flags)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.flags)
|
||||
|
||||
def __iter__(self) -> Iterator[FlagType]:
|
||||
return iter(self.flags)
|
||||
|
||||
def __getitem__(self, flag_index: int) -> FlagType:
|
||||
return self.flags[flag_index]
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.flags)
|
||||
|
||||
|
||||
class Flags(BaseFlags[Flag]):
|
||||
def get_flag_by_name(self, name: str) -> Flag | None:
|
||||
"""
|
||||
Public. Returns the flag entity by its name or None if not found
|
||||
:param name: the name of the flag to get
|
||||
:return: entity of the flag or None
|
||||
"""
|
||||
return next((flag for flag in self.flags if flag.name == name), None)
|
||||
|
||||
@override
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Flags):
|
||||
return False
|
||||
|
||||
if len(self.flags) != len(other.flags):
|
||||
return False
|
||||
|
||||
flag_pairs: zip[tuple[Flag, Flag]] = zip(self.flags, other.flags)
|
||||
return all(s_flag == o_flag for s_flag, o_flag in flag_pairs)
|
||||
|
||||
def __contains__(self, flag_to_check: object) -> bool:
|
||||
if isinstance(flag_to_check, Flag):
|
||||
for flag in self.flags:
|
||||
if flag == flag_to_check:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
|
||||
class InputFlags(BaseFlags[InputFlag]):
|
||||
def get_flag_by_name(self, name: str) -> InputFlag | None:
|
||||
"""
|
||||
Public. Returns the flag entity by its name or None if not found
|
||||
:param name: the name of the flag to get
|
||||
:return: entity of the flag or None
|
||||
"""
|
||||
return next((flag for flag in self.flags if flag.name == name), None)
|
||||
|
||||
@override
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, InputFlags):
|
||||
return False
|
||||
|
||||
if len(self.flags) != len(other.flags):
|
||||
return False
|
||||
|
||||
paired_flags: zip[tuple[InputFlag, InputFlag]] = zip(self.flags, other.flags)
|
||||
|
||||
return all(my_flag == other_flag for my_flag, other_flag in paired_flags)
|
||||
|
||||
def __contains__(self, ingressable_item: object) -> bool:
|
||||
if isinstance(ingressable_item, InputFlag):
|
||||
for flag in self.flags:
|
||||
if flag == ingressable_item:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
raise TypeError
|
||||
@@ -1,8 +1,8 @@
|
||||
__all__ = ["PossibleValues", "ValidationStatus", "Flag", "InputFlag"]
|
||||
__all__ = ["PossibleValues", "ValidationStatus", "Flag", "InputFlag", "InputFlags", "Flags"]
|
||||
|
||||
from enum import Enum
|
||||
from re import Pattern
|
||||
from typing import Literal, override
|
||||
from typing import Any, Container, Generic, Iterator, Literal, TypeVar, override
|
||||
|
||||
PREFIX_TYPE = Literal["-", "--", "---"]
|
||||
|
||||
@@ -24,7 +24,7 @@ class Flag:
|
||||
name: str,
|
||||
*,
|
||||
prefix: PREFIX_TYPE = "--",
|
||||
possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
|
||||
possible_values: Container[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
|
||||
) -> None:
|
||||
"""
|
||||
Public. The entity of the flag being registered for subsequent processing
|
||||
@@ -35,7 +35,7 @@ class Flag:
|
||||
"""
|
||||
self.name: str = name
|
||||
self.prefix: PREFIX_TYPE = prefix
|
||||
self.possible_values: list[str] | Pattern[str] | PossibleValues = possible_values
|
||||
self.possible_values: Container[str] | Pattern[str] | PossibleValues = possible_values
|
||||
|
||||
def validate_input_flag_value(self, input_flag_value: str) -> bool:
|
||||
"""
|
||||
@@ -91,7 +91,7 @@ class InputFlag:
|
||||
Public. The entity of the flag of the entered command
|
||||
:param name: the name of the input flag
|
||||
:param prefix: the prefix of the input flag
|
||||
:param value: the value of the input flag
|
||||
:param input_value: the value of the input flag
|
||||
:return: None
|
||||
"""
|
||||
self.name: str = name
|
||||
@@ -122,3 +122,115 @@ class InputFlag:
|
||||
return self.name == other.name
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
FlagType = TypeVar("FlagType")
|
||||
|
||||
|
||||
class BaseFlags(Generic[FlagType]):
|
||||
def __init__(self, flags: list[FlagType] | None = None) -> None:
|
||||
"""
|
||||
Public. A model that combines the registered flags
|
||||
:param flags: the flags that will be registered
|
||||
:return: None
|
||||
"""
|
||||
self.flags: list[FlagType] = flags if flags else []
|
||||
|
||||
def add_flag(self, flag: FlagType) -> None:
|
||||
"""
|
||||
Public. Adds a flag to the list of flags
|
||||
:param flag: flag to add
|
||||
:return: None
|
||||
"""
|
||||
self.flags.append(flag)
|
||||
|
||||
def add_flags(self, flags: list[FlagType]) -> None:
|
||||
"""
|
||||
Public. Adds a list of flags to the list of flags
|
||||
:param flags: list of flags to add
|
||||
:return: None
|
||||
"""
|
||||
self.flags.extend(flags)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.flags)
|
||||
|
||||
def __iter__(self) -> Iterator[FlagType]:
|
||||
return iter(self.flags)
|
||||
|
||||
def __getitem__(self, flag_index: int) -> FlagType:
|
||||
return self.flags[flag_index]
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.flags)
|
||||
|
||||
|
||||
class Flags(BaseFlags[Flag]):
|
||||
def get_flag_by_name(self, name: str) -> Flag | None:
|
||||
"""
|
||||
Public. Returns the flag entity by its name or None if not found
|
||||
:param name: the name of the flag to get
|
||||
:return: entity of the flag or None
|
||||
"""
|
||||
return next((flag for flag in self.flags if flag.name == name), None)
|
||||
|
||||
@override
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Flags):
|
||||
return False
|
||||
|
||||
if len(self.flags) != len(other.flags):
|
||||
return False
|
||||
|
||||
flag_pairs: Iterator[tuple[Flag, Flag]] = zip(self.flags, other.flags)
|
||||
return all(s_flag == o_flag for s_flag, o_flag in flag_pairs)
|
||||
|
||||
def __contains__(self, flag_to_check: object) -> bool:
|
||||
if isinstance(flag_to_check, Flag):
|
||||
for flag in self.flags:
|
||||
if flag == flag_to_check:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
|
||||
class InputFlags(BaseFlags[InputFlag]):
|
||||
def get_flag_by_name(
|
||||
self,
|
||||
name: str,
|
||||
with_status: ValidationStatus | None = None,
|
||||
default: Any = None
|
||||
) -> InputFlag | None:
|
||||
"""
|
||||
Public. Returns the flag entity by its name or None if not found
|
||||
:param default:
|
||||
:param with_status:
|
||||
:param name: the name of the flag to get
|
||||
:return: entity of the flag or None
|
||||
"""
|
||||
if with_status is None:
|
||||
return next((flag for flag in self.flags if flag.name == name), default)
|
||||
else:
|
||||
return next((flag for flag in self.flags if flag.name == name and flag.status == with_status), default)
|
||||
|
||||
@override
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, InputFlags):
|
||||
return False
|
||||
|
||||
if len(self.flags) != len(other.flags):
|
||||
return False
|
||||
|
||||
paired_flags: Iterator[tuple[InputFlag, InputFlag]] = zip(self.flags, other.flags)
|
||||
|
||||
return all(my_flag == other_flag for my_flag, other_flag in paired_flags)
|
||||
|
||||
def __contains__(self, ingressable_item: object) -> bool:
|
||||
if isinstance(ingressable_item, InputFlag):
|
||||
for flag in self.flags:
|
||||
if flag == ingressable_item:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
__all__ = ["Command", "InputCommand"]
|
||||
|
||||
import shlex
|
||||
from typing import Literal, Never, Self, cast
|
||||
from typing import Iterable, Literal, Never, Self, cast
|
||||
|
||||
from argenta.command.exceptions import (
|
||||
EmptyInputCommandException,
|
||||
RepeatedInputFlagsException,
|
||||
UnprocessedInputFlagException,
|
||||
)
|
||||
from argenta.command.flag.flags.models import Flags, InputFlags
|
||||
from argenta.command import Flags, InputFlags
|
||||
from argenta.command.exceptions import (EmptyInputCommandException,
|
||||
RepeatedInputFlagsException,
|
||||
UnprocessedInputFlagException)
|
||||
from argenta.command.flag.models import Flag, InputFlag, ValidationStatus
|
||||
|
||||
ParseFlagsResult = tuple[InputFlags, str | None, str | None]
|
||||
@@ -16,10 +14,6 @@ ParseResult = tuple[str, InputFlags]
|
||||
|
||||
MIN_FLAG_PREFIX: str = "-"
|
||||
PREFIX_TYPE = Literal["-", "--", "---"]
|
||||
DEFAULT_WITHOUT_FLAGS: Flags = Flags()
|
||||
DEFAULT_WITHOUT_ALIASES: set[Never] = set()
|
||||
|
||||
DEFAULT_WITHOUT_INPUT_FLAGS: InputFlags = InputFlags()
|
||||
|
||||
|
||||
class Command:
|
||||
@@ -28,8 +22,8 @@ class Command:
|
||||
trigger: str,
|
||||
*,
|
||||
description: str = "Some useful command",
|
||||
flags: Flag | Flags = DEFAULT_WITHOUT_FLAGS,
|
||||
aliases: set[str] | set[Never] = DEFAULT_WITHOUT_ALIASES,
|
||||
flags: Flag | Flags | None = None,
|
||||
aliases: Iterable[str] | None = None,
|
||||
):
|
||||
"""
|
||||
Public. The command that can and should be registered in the Router
|
||||
@@ -38,11 +32,16 @@ class Command:
|
||||
:param flags: processed commands
|
||||
:param aliases: string synonyms for the main trigger
|
||||
"""
|
||||
pretty_flags = flags if isinstance(flags, Flags) else Flags([flags])
|
||||
pretty_flags: Flags = (
|
||||
flags if isinstance(flags, Flags)
|
||||
else Flags([flags])
|
||||
if flags is not None
|
||||
else Flags()
|
||||
)
|
||||
self.registered_flags: Flags = pretty_flags
|
||||
self.trigger: str = trigger
|
||||
self.description: str = description
|
||||
self.aliases: set[str] | set[Never] = aliases
|
||||
self.aliases: Iterable[str] | Iterable[Never] = aliases or set()
|
||||
|
||||
self._paired_string_entity_flag: dict[str, Flag] = {
|
||||
flag.string_entity: flag for flag in pretty_flags
|
||||
@@ -68,7 +67,7 @@ class InputCommand:
|
||||
self,
|
||||
trigger: str,
|
||||
*,
|
||||
input_flags: InputFlag | InputFlags = DEFAULT_WITHOUT_INPUT_FLAGS,
|
||||
input_flags: InputFlag | InputFlags | None = None,
|
||||
):
|
||||
"""
|
||||
Private. The model of the input command, after parsing
|
||||
@@ -81,6 +80,8 @@ class InputCommand:
|
||||
input_flags
|
||||
if isinstance(input_flags, InputFlags)
|
||||
else InputFlags([input_flags])
|
||||
if input_flags is not None
|
||||
else InputFlags()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -90,7 +91,14 @@ class InputCommand:
|
||||
:param raw_command: raw input command
|
||||
:return: model of the input command, after parsing as InputCommand
|
||||
"""
|
||||
tokens = shlex.split(raw_command)
|
||||
lexer = shlex.shlex(raw_command, posix=True)
|
||||
lexer.whitespace_split = True
|
||||
lexer.commenters = ""
|
||||
|
||||
try:
|
||||
tokens = list(lexer)
|
||||
except ValueError as e:
|
||||
raise UnprocessedInputFlagException from e
|
||||
|
||||
if not tokens:
|
||||
raise EmptyInputCommandException
|
||||
|
||||
@@ -39,4 +39,4 @@ class Orchestrator:
|
||||
)
|
||||
setup_dishka(app, container, auto_inject=self._auto_inject_handlers)
|
||||
|
||||
app.run_polling()
|
||||
app._run_polling()
|
||||
|
||||
@@ -2,7 +2,7 @@ __all__ = ["Response"]
|
||||
|
||||
from dishka import Container
|
||||
|
||||
from argenta.command.flag.flags.models import InputFlags
|
||||
from argenta.command import InputFlags
|
||||
from argenta.response.status import ResponseStatus
|
||||
|
||||
EMPTY_INPUT_FLAGS: InputFlags = InputFlags()
|
||||
|
||||
@@ -6,9 +6,8 @@ from typing import Callable
|
||||
from rich.console import Console
|
||||
|
||||
from argenta.app.protocols import HandlerFunc
|
||||
from argenta.command import Command, InputCommand
|
||||
from argenta.command import Command, InputCommand, InputFlags
|
||||
from argenta.command.flag import ValidationStatus
|
||||
from argenta.command.flag.flags import InputFlags
|
||||
from argenta.response import Response, ResponseStatus
|
||||
from argenta.router.command_handler.entity import CommandHandler, CommandHandlers
|
||||
from argenta.router.exceptions import (RepeatedAliasNameException,
|
||||
@@ -36,7 +35,7 @@ class Router:
|
||||
:return: None
|
||||
"""
|
||||
self.title: str = title
|
||||
self.disable_redirect_stdout: bool = disable_redirect_stdout
|
||||
self.is_redirect_stdout_disabled: bool = disable_redirect_stdout
|
||||
|
||||
self.command_handlers: CommandHandlers = CommandHandlers()
|
||||
self.aliases: set[str] = set()
|
||||
@@ -57,7 +56,7 @@ class Router:
|
||||
self._update_routing_keys(redefined_command)
|
||||
|
||||
def decorator(func: HandlerFunc) -> HandlerFunc:
|
||||
_validate_func_args(func)
|
||||
self._validate_func_args(func)
|
||||
self.command_handlers.add_handler(CommandHandler(func, redefined_command))
|
||||
return func
|
||||
|
||||
@@ -117,7 +116,7 @@ class Router:
|
||||
handle_command = command_handler.handled_command
|
||||
if handle_command.registered_flags.flags:
|
||||
if input_command_flags.flags:
|
||||
response: Response = _structuring_input_flags(handle_command, input_command_flags)
|
||||
response: Response = self._structuring_input_flags(handle_command, input_command_flags)
|
||||
command_handler.handling(response)
|
||||
else:
|
||||
response = Response(ResponseStatus.ALL_FLAGS_VALID)
|
||||
@@ -134,53 +133,53 @@ class Router:
|
||||
response = Response(ResponseStatus.ALL_FLAGS_VALID)
|
||||
command_handler.handling(response)
|
||||
|
||||
@staticmethod
|
||||
def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response:
|
||||
"""
|
||||
Private. Validates flags of input command
|
||||
:param handled_command: entity of the handled command
|
||||
:param input_flags:
|
||||
:return: entity of response as Response
|
||||
"""
|
||||
invalid_value_flags, undefined_flags = False, False
|
||||
|
||||
def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response:
|
||||
"""
|
||||
Private. Validates flags of input command
|
||||
:param handled_command: entity of the handled command
|
||||
:param input_flags:
|
||||
:return: entity of response as Response
|
||||
"""
|
||||
invalid_value_flags, undefined_flags = False, False
|
||||
for flag in input_flags:
|
||||
flag_status: ValidationStatus = handled_command.validate_input_flag(flag)
|
||||
flag.status = flag_status
|
||||
if flag_status == ValidationStatus.INVALID:
|
||||
invalid_value_flags = True
|
||||
elif flag_status == ValidationStatus.UNDEFINED:
|
||||
undefined_flags = True
|
||||
|
||||
for flag in input_flags:
|
||||
flag_status: ValidationStatus = handled_command.validate_input_flag(flag)
|
||||
flag.status = flag_status
|
||||
if flag_status == ValidationStatus.INVALID:
|
||||
invalid_value_flags = True
|
||||
elif flag_status == ValidationStatus.UNDEFINED:
|
||||
undefined_flags = True
|
||||
|
||||
status = ResponseStatus.from_flags(
|
||||
has_invalid_value_flags=invalid_value_flags,
|
||||
has_undefined_flags=undefined_flags
|
||||
)
|
||||
|
||||
return Response(status=status, input_flags=input_flags)
|
||||
|
||||
|
||||
def _validate_func_args(func: HandlerFunc) -> None:
|
||||
"""
|
||||
Private. Validates the arguments of the handler
|
||||
:param func: entity of the handler func
|
||||
:return: None if func is valid else raise exception
|
||||
"""
|
||||
transferred_args = getfullargspec(func).args
|
||||
if len(transferred_args) == 0:
|
||||
raise RequiredArgumentNotPassedException()
|
||||
|
||||
response_arg: str = transferred_args[0]
|
||||
func_annotations: dict[str, None] = get_annotations(func)
|
||||
|
||||
response_arg_annotation = func_annotations.get(response_arg)
|
||||
|
||||
if response_arg_annotation is not None and response_arg_annotation is not Response:
|
||||
source_line: int = getsourcelines(func)[1]
|
||||
Console().print(
|
||||
f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint '
|
||||
+ f"of argument([green]{response_arg}[/green]) passed to the handler must be [/i][bold blue]{Response}[/bold blue],"
|
||||
+ f" [i]but[/i] [bold blue]{response_arg_annotation}[/bold blue] [i]is specified[/i]",
|
||||
highlight=False,
|
||||
status = ResponseStatus.from_flags(
|
||||
has_invalid_value_flags=invalid_value_flags,
|
||||
has_undefined_flags=undefined_flags
|
||||
)
|
||||
|
||||
return Response(status=status, input_flags=input_flags)
|
||||
|
||||
@staticmethod
|
||||
def _validate_func_args(func: HandlerFunc) -> None:
|
||||
"""
|
||||
Private. Validates the arguments of the handler
|
||||
:param func: entity of the handler func
|
||||
:return: None if func is valid else raise exception
|
||||
"""
|
||||
transferred_args = getfullargspec(func).args
|
||||
if len(transferred_args) == 0:
|
||||
raise RequiredArgumentNotPassedException()
|
||||
|
||||
response_arg: str = transferred_args[0]
|
||||
func_annotations: dict[str, None] = get_annotations(func)
|
||||
|
||||
response_arg_annotation = func_annotations.get(response_arg)
|
||||
|
||||
if response_arg_annotation is not None and response_arg_annotation is not Response:
|
||||
source_line: int = getsourcelines(func)[1]
|
||||
Console().print(
|
||||
f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint '
|
||||
+ f"of argument([green]{response_arg}[/green]) passed to the handler must be [/i][bold blue]{Response}[/bold blue],"
|
||||
+ f" [i]but[/i] [bold blue]{response_arg_annotation}[/bold blue] [i]is specified[/i]",
|
||||
highlight=False,
|
||||
)
|
||||
|
||||
@@ -5,8 +5,7 @@ from collections.abc import Iterator
|
||||
import pytest
|
||||
|
||||
from argenta import App, Orchestrator, Router
|
||||
from argenta.command import Command, PredefinedFlags
|
||||
from argenta.command.flag.flags.models import Flags
|
||||
from argenta.command import Command, PredefinedFlags, Flags
|
||||
from argenta.command.flag.models import ValidationStatus
|
||||
from argenta.response import Response
|
||||
|
||||
@@ -36,7 +35,7 @@ def test_empty_input_triggers_empty_command_handler(monkeypatch: pytest.MonkeyPa
|
||||
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||
print('test command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
app.set_empty_command_handler(lambda: print('Empty input command'))
|
||||
orchestrator.start_polling(app)
|
||||
@@ -62,7 +61,7 @@ def test_unknown_command_triggers_unknown_command_handler(monkeypatch: pytest.Mo
|
||||
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||
print('test command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
|
||||
orchestrator.start_polling(app)
|
||||
@@ -83,7 +82,7 @@ def test_mixed_valid_and_unknown_commands_handled_correctly(monkeypatch: pytest.
|
||||
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||
print('test command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
|
||||
orchestrator.start_polling(app)
|
||||
@@ -108,7 +107,7 @@ def test_multiple_commands_with_unknown_command_in_between(monkeypatch: pytest.M
|
||||
def test1(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||
print('more command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}'))
|
||||
orchestrator.start_polling(app)
|
||||
@@ -136,7 +135,7 @@ def test_unregistered_flag_without_value_is_accessible(monkeypatch: pytest.Monke
|
||||
if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED:
|
||||
print(f'test command with undefined flag: {undefined_flag.string_entity}')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -160,7 +159,7 @@ def test_unregistered_flag_with_value_is_accessible(monkeypatch: pytest.MonkeyPa
|
||||
else:
|
||||
raise
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -183,7 +182,7 @@ def test_registered_and_unregistered_flags_coexist(monkeypatch: pytest.MonkeyPat
|
||||
if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED:
|
||||
print(f'connecting to host with flag: {undefined_flag.string_entity} {undefined_flag.input_value}')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -208,7 +207,7 @@ def test_flag_without_value_triggers_incorrect_syntax_handler(monkeypatch: pytes
|
||||
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||
print('test command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"'))
|
||||
orchestrator.start_polling(app)
|
||||
@@ -234,7 +233,7 @@ def test_repeated_flags_trigger_repeated_flags_handler(monkeypatch: pytest.Monke
|
||||
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||
print('test command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"'))
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -5,9 +5,8 @@ from collections.abc import Iterator
|
||||
import pytest
|
||||
|
||||
from argenta import App, Orchestrator, Router
|
||||
from argenta.command import Command, PredefinedFlags
|
||||
from argenta.command import Command, PredefinedFlags, Flags
|
||||
from argenta.command.flag import Flag
|
||||
from argenta.command.flag.flags import Flags
|
||||
from argenta.command.flag.models import PossibleValues, ValidationStatus
|
||||
from argenta.response import Response
|
||||
|
||||
@@ -37,7 +36,7 @@ def test_simple_command_executes_successfully(monkeypatch: pytest.MonkeyPatch, c
|
||||
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||
print('test command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -61,7 +60,7 @@ def test_two_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, caps
|
||||
def test2(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||
print('some command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -89,7 +88,7 @@ def test_three_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, ca
|
||||
def test2(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||
print('more command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -117,7 +116,7 @@ def test_custom_flag_without_value_is_recognized(monkeypatch: pytest.MonkeyPatch
|
||||
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||
print(f'\nhelp for {valid_flag.name} flag\n')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -140,7 +139,7 @@ def test_custom_flag_with_regex_validation_accepts_valid_value(monkeypatch: pyte
|
||||
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||
print(f'flag value for {valid_flag.name} flag : {valid_flag.input_value}')
|
||||
|
||||
app = App(override_system_messages=True, repeat_command_groups_printing=True, print_func=print)
|
||||
app = App(override_system_messages=True, repeat_command_groups_printing=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -168,7 +167,7 @@ def test_predefined_short_help_flag_is_recognized(monkeypatch: pytest.MonkeyPatc
|
||||
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||
print(f'help for {valid_flag.name} flag')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -191,7 +190,7 @@ def test_predefined_info_flag_is_recognized(monkeypatch: pytest.MonkeyPatch, cap
|
||||
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||
print('info about test command')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -214,7 +213,7 @@ def test_predefined_host_flag_with_value_is_recognized(monkeypatch: pytest.Monke
|
||||
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||
print(f'connecting to host {valid_flag.input_value}')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
@@ -243,7 +242,7 @@ def test_two_predefined_flags_are_recognized_together(monkeypatch: pytest.Monkey
|
||||
if (host_flag and host_flag.status == ValidationStatus.VALID) and (port_flag and port_flag.status == ValidationStatus.VALID):
|
||||
print(f'connecting to host {host_flag.input_value} and port {port_flag.input_value}')
|
||||
|
||||
app = App(override_system_messages=True, print_func=print)
|
||||
app = App(override_system_messages=True, printer=print)
|
||||
app.include_router(router)
|
||||
orchestrator.start_polling(app)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import pytest
|
||||
from pytest import CaptureFixture
|
||||
|
||||
from argenta.app import App
|
||||
from argenta.app.dividing_line import DynamicDividingLine, StaticDividingLine
|
||||
from argenta.app.protocols import DescriptionMessageGenerator, NonStandardBehaviorHandler
|
||||
from argenta.command.models import Command, InputCommand
|
||||
from argenta.response import Response
|
||||
@@ -18,26 +17,31 @@ from argenta.router import Router
|
||||
|
||||
def test_default_exit_command_lowercase_q_is_recognized() -> None:
|
||||
app = App()
|
||||
app._setup_system_router()
|
||||
assert app._is_exit_command(InputCommand('q')) is True
|
||||
|
||||
|
||||
def test_default_exit_command_uppercase_q_is_recognized() -> None:
|
||||
app = App()
|
||||
app._setup_system_router()
|
||||
assert app._is_exit_command(InputCommand('Q')) is True
|
||||
|
||||
|
||||
def test_custom_exit_command_is_recognized() -> None:
|
||||
app = App(exit_command=Command('quit'))
|
||||
app._setup_system_router()
|
||||
assert app._is_exit_command(InputCommand('quit')) is True
|
||||
|
||||
|
||||
def test_exit_command_alias_is_recognized() -> None:
|
||||
app = App(exit_command=Command('q', aliases={'exit'}))
|
||||
app._setup_system_router()
|
||||
assert app._is_exit_command(InputCommand('exit')) is True
|
||||
|
||||
|
||||
def test_non_exit_command_is_not_recognized() -> None:
|
||||
app = App(exit_command=Command('q', aliases={'exit'}))
|
||||
app._setup_system_router()
|
||||
assert app._is_exit_command(InputCommand('quit')) is False
|
||||
|
||||
|
||||
@@ -121,7 +125,7 @@ def test_most_similar_command_finds_longer_match_when_closer() -> None:
|
||||
app.include_routers(router)
|
||||
app._pre_cycle_setup()
|
||||
|
||||
assert app._most_similar_command('command_') == 'command_other'
|
||||
assert app._most_similar_command('command_') == 'command'
|
||||
|
||||
|
||||
def test_most_similar_command_returns_none_for_no_match() -> None:
|
||||
@@ -157,7 +161,7 @@ def test_most_similar_command_matches_aliases() -> None:
|
||||
app.include_routers(router)
|
||||
app._pre_cycle_setup()
|
||||
|
||||
assert app._most_similar_command('othe') == 'other_name'
|
||||
assert app._most_similar_command('other_') == 'other_name'
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -291,48 +295,6 @@ def test_pre_cycle_setup_prints_startup_messages(capsys: CaptureFixture[str]) ->
|
||||
assert 'some message' in stdout.out
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests for framed text printing
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_print_framed_text_with_static_dividing_line(capsys: CaptureFixture[str]) -> None:
|
||||
app = App(override_system_messages=True, dividing_line=StaticDividingLine(length=5))
|
||||
app._print_framed_text('test')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert '\n-----\n\ntest\n\n-----\n' in captured.out
|
||||
|
||||
|
||||
def test_print_framed_text_with_dynamic_dividing_line_short_text(capsys: CaptureFixture[str]) -> None:
|
||||
app = App(override_system_messages=True, dividing_line=DynamicDividingLine())
|
||||
app._print_framed_text('some long test')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert '\n--------------\n\nsome long test\n\n--------------\n' in captured.out
|
||||
|
||||
|
||||
def test_print_framed_text_with_dynamic_dividing_line_long_text(capsys: CaptureFixture[str]) -> None:
|
||||
app = App(override_system_messages=True, dividing_line=DynamicDividingLine())
|
||||
app._print_framed_text('test as test as test')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert '\n' + '-'*20 + '\n\ntest as test as test\n\n' + '-'*20 + '\n' in captured.out
|
||||
|
||||
|
||||
def test_print_framed_text_with_unsupported_dividing_line_raises_error() -> None:
|
||||
class OtherDividingLine:
|
||||
pass
|
||||
|
||||
app = App(override_system_messages=True, dividing_line=OtherDividingLine()) # pyright: ignore[reportArgumentType]
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
app._print_framed_text('some long test')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests for handler configuration
|
||||
# ============================================================================
|
||||
@@ -343,7 +305,7 @@ def test_set_description_message_pattern_stores_generator() -> None:
|
||||
descr_gen: DescriptionMessageGenerator = lambda command, description: command + '-+-' + description
|
||||
app.set_description_message_pattern(descr_gen)
|
||||
|
||||
assert app._description_message_gen is descr_gen
|
||||
assert app._description_message_generator is descr_gen
|
||||
|
||||
|
||||
def test_set_exit_command_handler_stores_handler() -> None:
|
||||
@@ -354,22 +316,6 @@ def test_set_exit_command_handler_stores_handler() -> None:
|
||||
assert app._exit_command_handler is handler
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests for default view setup
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def test_setup_default_view_formats_prompt() -> None:
|
||||
app = App(prompt='>>')
|
||||
assert app._prompt == '<gray><b>>></b></gray>'
|
||||
|
||||
|
||||
def test_setup_default_view_sets_default_unknown_command_handler() -> None:
|
||||
app = App()
|
||||
app._setup_default_view()
|
||||
assert app._unknown_command_handler(InputCommand('nonexists')) is None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests for command processing
|
||||
# ============================================================================
|
||||
@@ -672,11 +618,17 @@ def test_app_handlers_work_with_multiple_routers() -> None:
|
||||
|
||||
app.set_unknown_command_handler(custom_handler)
|
||||
|
||||
# Both commands should be known
|
||||
assert not app._is_unknown_command(InputCommand('cmd1'))
|
||||
assert not app._is_unknown_command(InputCommand('cmd2'))
|
||||
|
||||
# Unknown command should trigger handler
|
||||
assert app._is_unknown_command(InputCommand('unknown'))
|
||||
app._unknown_command_handler(InputCommand('unknown'))
|
||||
assert call_tracker['called']
|
||||
|
||||
|
||||
def test_process_exist_and_valid_command_raises_runtime_error_when_router_not_found() -> None:
|
||||
app = App()
|
||||
app._pre_cycle_setup()
|
||||
|
||||
with pytest.raises(RuntimeError, match="Router for 'nonexistent' not found. Panic!"):
|
||||
app._process_exist_and_valid_command(InputCommand('nonexistent'))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user