mirror of
https://github.com/koloideal/Argenta.git
synced 2026-06-10 18:15:28 +03:00
@@ -1,5 +1,7 @@
|
|||||||
#### joe made this: http://goel.io/joe
|
#### joe made this: http://goel.io/joe
|
||||||
|
|
||||||
|
metrics/reports/diagrams
|
||||||
|
|
||||||
#### python ####
|
#### python ####
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__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 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(
|
orchestrator = Orchestrator(
|
||||||
arg_parser=arg_parser,
|
arg_parser=arg_parser,
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if arg_parser.parsed_argspace.get_by_name('dev'):
|
if arg_parser.parsed_argspace.get_by_name("dev"):
|
||||||
orchestrator.start_polling(App(initial_message='ArgentaDev'))
|
orchestrator.start_polling(App(initial_message="ArgentaDev"))
|
||||||
else:
|
else:
|
||||||
orchestrator.start_polling(App())
|
orchestrator.start_polling(App())
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ arguments = [
|
|||||||
ValueArgument("port", help="Server port", is_required=True),
|
ValueArgument("port", help="Server port", is_required=True),
|
||||||
]
|
]
|
||||||
|
|
||||||
argparser = ArgParser(
|
argparser = ArgParser(processed_args=arguments, name="WebServer", description="Simple web server")
|
||||||
processed_args=arguments,
|
|
||||||
name="WebServer",
|
|
||||||
description="Simple web server"
|
|
||||||
)
|
|
||||||
|
|
||||||
app = App()
|
app = App()
|
||||||
orchestrator = Orchestrator(argparser)
|
orchestrator = Orchestrator(argparser)
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
config_arg = argspace.get_by_name("config")
|
from argenta import Response, Router
|
||||||
if config_arg:
|
from argenta.di import FromDishka
|
||||||
|
from argenta.orchestrator.argparser import ArgSpace
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
@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}")
|
print(f"Config path: {config_arg.value}")
|
||||||
|
|
||||||
verbose_arg = argspace.get_by_name("verbose")
|
verbose_arg = argspace.get_by_name("verbose")
|
||||||
if verbose_arg and verbose_arg.value:
|
if verbose_arg and verbose_arg.value:
|
||||||
print("Verbose mode enabled")
|
print("Verbose mode enabled")
|
||||||
|
|
||||||
unknown_arg = argspace.get_by_name("nonexistent")
|
unknown_arg = argspace.get_by_name("nonexistent")
|
||||||
if unknown_arg is None:
|
if unknown_arg is None:
|
||||||
print("Argument not found")
|
print("Argument not found")
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
from argenta.orchestrator.argparser import ArgParser, ValueArgument
|
from argenta.orchestrator.argparser import ArgParser, ValueArgument
|
||||||
|
|
||||||
# Create arguments
|
# Create arguments
|
||||||
config_arg = ValueArgument(
|
config_arg = ValueArgument("config", help="Path to configuration file", default="config.yaml")
|
||||||
"config",
|
|
||||||
help="Path to configuration file",
|
|
||||||
default="config.yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
log_level_arg = ValueArgument(
|
log_level_arg = ValueArgument(
|
||||||
"log-level",
|
"log-level",
|
||||||
help="Logging level",
|
help="Logging level",
|
||||||
possible_values=["DEBUG", "INFO", "WARNING", "ERROR"],
|
possible_values=["DEBUG", "INFO", "WARNING", "ERROR"],
|
||||||
default="INFO"
|
default="INFO",
|
||||||
)
|
)
|
||||||
|
|
||||||
host_arg = ValueArgument(
|
host_arg = ValueArgument("host", help="Server host address", is_required=True)
|
||||||
"host",
|
|
||||||
help="Server host address",
|
|
||||||
is_required=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register in ArgParser
|
# Register in ArgParser
|
||||||
parser = ArgParser(
|
parser = ArgParser(
|
||||||
processed_args=[config_arg, log_level_arg, host_arg],
|
processed_args=[config_arg, log_level_arg, host_arg],
|
||||||
name="MyApp",
|
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
|
from argenta.orchestrator.argparser import ArgParser, BooleanArgument
|
||||||
|
|
||||||
# Create boolean arguments
|
# Create boolean arguments
|
||||||
verbose_arg = BooleanArgument(
|
verbose_arg = BooleanArgument("verbose", help="Enable verbose output")
|
||||||
"verbose",
|
debug_arg = BooleanArgument("debug", help="Enable debug mode")
|
||||||
help="Enable verbose output"
|
no_cache_arg = BooleanArgument("no-cache", help="Disable caching")
|
||||||
)
|
|
||||||
|
|
||||||
debug_arg = BooleanArgument(
|
|
||||||
"debug",
|
|
||||||
help="Enable debug mode"
|
|
||||||
)
|
|
||||||
|
|
||||||
no_cache_arg = BooleanArgument(
|
|
||||||
"no-cache",
|
|
||||||
help="Disable caching"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register in ArgParser
|
# Register in ArgParser
|
||||||
parser = ArgParser(
|
parser = ArgParser(processed_args=[verbose_arg, debug_arg, no_cache_arg], name="MyApp")
|
||||||
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 = Router(title="System")
|
||||||
|
|
||||||
@router.command(Command(
|
|
||||||
|
@router.command(
|
||||||
|
Command(
|
||||||
"shutdown",
|
"shutdown",
|
||||||
description="Shutdown the system",
|
description="Shutdown the system",
|
||||||
aliases=["poweroff", "halt", "stop"]
|
aliases=["poweroff", "halt", "stop"]
|
||||||
))
|
)
|
||||||
|
)
|
||||||
def handle_shutdown(response: Response):
|
def handle_shutdown(response: Response):
|
||||||
print("Shutting down the system...")
|
print("Shutting down the system...")
|
||||||
@@ -3,7 +3,7 @@ from argenta.command import Flag
|
|||||||
verbose_flag = Flag(name="verbose", prefix="--")
|
verbose_flag = Flag(name="verbose", prefix="--")
|
||||||
short_flag = Flag(name="v", prefix="-")
|
short_flag = Flag(name="v", prefix="-")
|
||||||
|
|
||||||
# Debug view
|
# Debug presentation
|
||||||
print(repr(verbose_flag)) # Flag<prefix=--, name=verbose>
|
print(repr(verbose_flag)) # Flag<prefix=--, name=verbose>
|
||||||
print(repr(short_flag)) # Flag<prefix=-, name=v>
|
print(repr(short_flag)) # Flag<prefix=-, name=v>
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ from argenta.command.flag import ValidationStatus
|
|||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
@router.command(Command(
|
@router.command(Command("deploy", flags=Flag("verbose", possible_values=PossibleValues.NEITHER)))
|
||||||
"deploy",
|
|
||||||
flags=Flag("verbose", possible_values=PossibleValues.NEITHER)
|
|
||||||
))
|
|
||||||
def deploy_handler(response: Response):
|
def deploy_handler(response: Response):
|
||||||
# Check for toggle flag presence
|
# Check for toggle flag presence
|
||||||
verbose_flag = response.input_flags.get_flag_by_name("verbose")
|
verbose_flag = response.input_flags.get_flag_by_name("verbose")
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ router = Router(title="Example")
|
|||||||
Command(
|
Command(
|
||||||
"example",
|
"example",
|
||||||
description="Example command with flags",
|
description="Example command with flags",
|
||||||
flags=Flags([
|
flags=Flags([Flag("name"), Flag("age")]),
|
||||||
Flag("name"),
|
|
||||||
Flag("age")
|
|
||||||
]),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def example_handler(response: Response):
|
def example_handler(response: Response):
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ router = Router(title="Get Flag Example")
|
|||||||
Command(
|
Command(
|
||||||
"config",
|
"config",
|
||||||
description="Configure settings",
|
description="Configure settings",
|
||||||
flags=Flags([
|
flags=Flags([Flag("host"), Flag("port"), Flag("debug")]),
|
||||||
Flag("host"),
|
|
||||||
Flag("port"),
|
|
||||||
Flag("debug")
|
|
||||||
]),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def config_handler(response: Response):
|
def config_handler(response: Response):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from argenta import Command, Response, Router
|
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")
|
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
|
# Create InputFlags collection
|
||||||
flags = InputFlags()
|
flags = InputFlags()
|
||||||
|
|
||||||
# Create several flags
|
# Create several flags
|
||||||
flag1 = InputFlag(
|
flag1 = InputFlag(name="option1", prefix="--", input_value="value1", status=ValidationStatus.VALID)
|
||||||
name="option1", prefix="--", input_value="value1", status=ValidationStatus.VALID
|
|
||||||
)
|
|
||||||
|
|
||||||
flag2 = InputFlag(
|
flag2 = InputFlag(name="option2", prefix="--", input_value="value2", status=ValidationStatus.VALID)
|
||||||
name="option2", prefix="--", input_value="value2", status=ValidationStatus.VALID
|
|
||||||
)
|
|
||||||
|
|
||||||
flag3 = InputFlag(
|
flag3 = InputFlag(name="option3", prefix="---", input_value="value3", status=ValidationStatus.VALID)
|
||||||
name="option3", prefix="---", input_value="value3", status=ValidationStatus.VALID
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add all flags in one call
|
# Add all flags in one call
|
||||||
flags.add_flags([flag1, flag2, flag3])
|
flags.add_flags([flag1, flag2, flag3])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from argenta.command.flag import InputFlag, ValidationStatus
|
from argenta.command.flag import InputFlag, ValidationStatus
|
||||||
from argenta.command.flag.flags.models import InputFlags
|
from argenta.command import InputFlags
|
||||||
|
|
||||||
# Create first collection
|
# Create first collection
|
||||||
flags1 = InputFlags(
|
flags1 = InputFlags(
|
||||||
@@ -26,12 +26,8 @@ flags3 = InputFlags(
|
|||||||
)
|
)
|
||||||
|
|
||||||
print(f"flags1 == flags2: {flags1 == flags2}") # True (same names)
|
print(f"flags1 == flags2: {flags1 == flags2}") # True (same names)
|
||||||
print(
|
print(f"flags1 == flags3: {flags1 == flags3}") # True (same names, values are not considered)
|
||||||
f"flags1 == flags3: {flags1 == flags3}"
|
|
||||||
) # True (same names, values are not considered)
|
|
||||||
|
|
||||||
# Different collections
|
# Different collections
|
||||||
flags4 = InputFlags(
|
flags4 = InputFlags([InputFlag(name="flag3", input_value="value3", status=ValidationStatus.VALID)])
|
||||||
[InputFlag(name="flag3", input_value="value3", status=ValidationStatus.VALID)]
|
|
||||||
)
|
|
||||||
print(f"flags1 == flags4: {flags1 == flags4}") # False (different flags)
|
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")
|
router = Router("Calculator")
|
||||||
|
|
||||||
operations = {
|
operations = {"mul": operator.mul, "sub": operator.sub, "add": operator.add}
|
||||||
'mul': operator.mul,
|
|
||||||
'sub': operator.sub,
|
|
||||||
'add': operator.add
|
|
||||||
}
|
|
||||||
|
|
||||||
@router.command(
|
@router.command(
|
||||||
Command(
|
Command(
|
||||||
@@ -22,7 +19,9 @@ operations = {
|
|||||||
[
|
[
|
||||||
Flag("a", possible_values=re.compile(r"^\d{,5}$")), # First number
|
Flag("a", possible_values=re.compile(r"^\d{,5}$")), # First number
|
||||||
Flag("b", possible_values=re.compile(r"^\d{,5}$")), # Second 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=">> ",
|
prompt=">> ",
|
||||||
initial_message="Simple App",
|
initial_message="Simple App",
|
||||||
farewell_message="Goodbye!",
|
farewell_message="Goodbye!",
|
||||||
repeat_command_groups_printing=False
|
repeat_command_groups_printing=False,
|
||||||
)
|
)
|
||||||
orchestrator = Orchestrator()
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
@@ -15,11 +15,7 @@ main_router = Router(title="Main commands")
|
|||||||
|
|
||||||
|
|
||||||
# 3. Define command and its handler
|
# 3. Define command and its handler
|
||||||
@main_router.command(Command(
|
@main_router.command(Command("hello", description="Prints greeting message", flags=Flag("name")))
|
||||||
"hello",
|
|
||||||
description="Prints greeting message",
|
|
||||||
flags=Flag("name")
|
|
||||||
))
|
|
||||||
def hello_handler(response: Response):
|
def hello_handler(response: Response):
|
||||||
"""This handler will be called for 'hello' command."""
|
"""This handler will be called for 'hello' command."""
|
||||||
name = response.input_flags.get_flag_by_name("name")
|
name = response.input_flags.get_flag_by_name("name")
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from argenta import Command, Response, Router
|
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 argenta.di import FromDishka
|
||||||
|
|
||||||
from .repository import Priority, Task, TaskRepository
|
from .repository import Priority, Task, TaskRepository
|
||||||
@@ -9,14 +10,18 @@ from .repository import Priority, Task, TaskRepository
|
|||||||
router = Router(title="Task Manager")
|
router = Router(title="Task Manager")
|
||||||
|
|
||||||
|
|
||||||
@router.command(Command(
|
@router.command(
|
||||||
|
Command(
|
||||||
"add-task",
|
"add-task",
|
||||||
description="Add a new task",
|
description="Add a new task",
|
||||||
flags=Flags([
|
flags=Flags(
|
||||||
|
[
|
||||||
Flag("description"),
|
Flag("description"),
|
||||||
Flag("priority", possible_values=["low", "medium", "high"]),
|
Flag("priority", possible_values=["low", "medium", "high"]),
|
||||||
]),
|
]
|
||||||
))
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
def add_task(response: Response, repo: FromDishka[TaskRepository]):
|
def add_task(response: Response, repo: FromDishka[TaskRepository]):
|
||||||
description_flag = response.input_flags.get_flag_by_name("description")
|
description_flag = response.input_flags.get_flag_by_name("description")
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from argenta.di import FromDishka
|
|||||||
|
|
||||||
router = Router(title="Authentication")
|
router = Router(title="Authentication")
|
||||||
|
|
||||||
|
|
||||||
def authenticate_user(username: str) -> str:
|
def authenticate_user(username: str) -> str:
|
||||||
return f"token_for_{username}"
|
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(
|
Command(
|
||||||
"process",
|
"process",
|
||||||
description="Process with flags",
|
description="Process with flags",
|
||||||
flags=Flags([
|
flags=Flags([Flag("format", possible_values=["json", "xml"]), Flag("verbose")]),
|
||||||
Flag("format", possible_values=["json", "xml"]),
|
|
||||||
Flag("verbose")
|
|
||||||
]),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def process_handler(response: Response):
|
def process_handler(response: Response):
|
||||||
|
|||||||
@@ -8,22 +8,21 @@ from argenta import App, Orchestrator, Router, Command, Response
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def patched_argv():
|
def patched_argv():
|
||||||
with patch.object(sys, 'argv', ['program.py']):
|
with patch.object(sys, "argv", ["program.py"]):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def test_input_incorrect_command(capsys: CaptureFixture[str]):
|
def test_input_incorrect_command(capsys: CaptureFixture[str]):
|
||||||
router = Router()
|
router = Router()
|
||||||
orchestrator = Orchestrator()
|
orchestrator = Orchestrator()
|
||||||
|
|
||||||
@router.command(Command('test'))
|
@router.command(Command("test"))
|
||||||
def test(response: Response) -> None:
|
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.include_router(router)
|
||||||
app.set_unknown_command_handler(
|
app.set_unknown_command_handler(lambda command: print(f"Unknown command: {command.trigger}"))
|
||||||
lambda command: print(f'Unknown command: {command.trigger}')
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch("builtins.input", side_effect=["help", "q"]):
|
with patch("builtins.input", side_effect=["help", "q"]):
|
||||||
orchestrator.start_polling(app)
|
orchestrator.start_polling(app)
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ class Service:
|
|||||||
def hello(self) -> str:
|
def hello(self) -> str:
|
||||||
return "world"
|
return "world"
|
||||||
|
|
||||||
|
|
||||||
def get_service() -> Service:
|
def get_service() -> Service:
|
||||||
return Service()
|
return Service()
|
||||||
|
|
||||||
|
|
||||||
router = Router(title="DI")
|
router = Router(title="DI")
|
||||||
|
|
||||||
|
|
||||||
@router.command("HELLO")
|
@router.command("HELLO")
|
||||||
def hello(response: Response, service: FromDishka[Service]) -> None:
|
def hello(response: Response, service: FromDishka[Service]) -> None:
|
||||||
print(f"hello {service.hello()}")
|
print(f"hello {service.hello()}")
|
||||||
@@ -37,6 +39,6 @@ def test_hello_uses_service():
|
|||||||
|
|
||||||
# Call handler
|
# Call handler
|
||||||
with redirect_stdout(io.StringIO()) as stdout:
|
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()
|
assert "hello world" in stdout.getvalue()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from argenta.command import InputCommand
|
|||||||
|
|
||||||
router = Router(title="Demo")
|
router = Router(title="Demo")
|
||||||
|
|
||||||
|
|
||||||
@router.command(Command("PING", description="Ping command"))
|
@router.command(Command("PING", description="Ping command"))
|
||||||
def ping(response: Response):
|
def ping(response: Response):
|
||||||
print("PONG")
|
print("PONG")
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ Argenta предназначена для создания приложений,
|
|||||||
|
|
||||||
root/contributing
|
root/contributing
|
||||||
root/code_of_conduct
|
root/code_of_conduct
|
||||||
|
root/metrics
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:hidden:
|
:hidden:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Argenta \n"
|
"Project-Id-Version: Argenta \n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
@@ -29,32 +29,32 @@ msgid ""
|
|||||||
"взаимодействие с пользователем, координируя работу всех компонентов: "
|
"взаимодействие с пользователем, координируя работу всех компонентов: "
|
||||||
"роутеров, обработчиков и системных сообщений."
|
"роутеров, обработчиков и системных сообщений."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"The ``App`` object is the core of your console application. It handles "
|
"The ``App`` object is the implementations of your console application. It"
|
||||||
"configuration, lifecycle management, command processing, and user "
|
" handles configuration, lifecycle management, command processing, and "
|
||||||
"interaction, coordinating the work of all components: routers, handlers, "
|
"user interaction, coordinating the work of all components: routers, "
|
||||||
"and system messages."
|
"handlers, and system messages."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:11
|
#: ../../root/api/app/index.rst:11
|
||||||
msgid "Инициализация"
|
msgid "Инициализация"
|
||||||
msgstr "Initialization"
|
msgstr "Initialization"
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:37
|
#: ../../root/api/app/index.rst:31
|
||||||
msgid "Создаёт и настраивает экземпляр приложения."
|
msgid "Создаёт и настраивает экземпляр приложения."
|
||||||
msgstr "Creates and configures an application instance."
|
msgstr "Creates and configures an application instance."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:39
|
#: ../../root/api/app/index.rst:33
|
||||||
msgid "``prompt``: Приглашение к вводу, отображаемое перед каждой командой."
|
msgid "``prompt``: Приглашение к вводу, отображаемое перед каждой командой."
|
||||||
msgstr "``prompt``: Input prompt displayed before each command."
|
msgstr "``prompt``: Input prompt displayed before each command."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:40
|
#: ../../root/api/app/index.rst:34
|
||||||
msgid "``initial_message``: Сообщение, выводимое при запуске приложения."
|
msgid "``initial_message``: Сообщение, выводимое при запуске приложения."
|
||||||
msgstr "``initial_message``: Message displayed when the application starts."
|
msgstr "``initial_message``: Message displayed when the application starts."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:41
|
#: ../../root/api/app/index.rst:35
|
||||||
msgid "``farewell_message``: Сообщение, выводимое при выходе из приложения."
|
msgid "``farewell_message``: Сообщение, выводимое при выходе из приложения."
|
||||||
msgstr "``farewell_message``: Message displayed when exiting the application."
|
msgstr "``farewell_message``: Message displayed when exiting the application."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:42
|
#: ../../root/api/app/index.rst:36
|
||||||
msgid ""
|
msgid ""
|
||||||
"``exit_command``: Команда, которая маркируется как триггер для выхода из "
|
"``exit_command``: Команда, которая маркируется как триггер для выхода из "
|
||||||
"приложения."
|
"приложения."
|
||||||
@@ -62,7 +62,7 @@ msgstr ""
|
|||||||
"``exit_command``: Command that is marked as a trigger for exiting the "
|
"``exit_command``: Command that is marked as a trigger for exiting the "
|
||||||
"application."
|
"application."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:43
|
#: ../../root/api/app/index.rst:37
|
||||||
msgid ""
|
msgid ""
|
||||||
"``system_router_title``: Заголовок для системного роутера (содержит "
|
"``system_router_title``: Заголовок для системного роутера (содержит "
|
||||||
"команду выхода)."
|
"команду выхода)."
|
||||||
@@ -70,7 +70,7 @@ msgstr ""
|
|||||||
"``system_router_title``: Title for the system router (contains the exit "
|
"``system_router_title``: Title for the system router (contains the exit "
|
||||||
"command)."
|
"command)."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:44
|
#: ../../root/api/app/index.rst:38
|
||||||
msgid ""
|
msgid ""
|
||||||
"``dividing_line``: Тип разделительной линии (``StaticDividingLine`` или "
|
"``dividing_line``: Тип разделительной линии (``StaticDividingLine`` или "
|
||||||
"``DynamicDividingLine``)."
|
"``DynamicDividingLine``)."
|
||||||
@@ -78,7 +78,7 @@ msgstr ""
|
|||||||
"``dividing_line``: Type of dividing line (``StaticDividingLine`` or "
|
"``dividing_line``: Type of dividing line (``StaticDividingLine`` or "
|
||||||
"``DynamicDividingLine``)."
|
"``DynamicDividingLine``)."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:45
|
#: ../../root/api/app/index.rst:39
|
||||||
msgid ""
|
msgid ""
|
||||||
"``repeat_command_groups_printing``: Если ``True``, список доступных "
|
"``repeat_command_groups_printing``: Если ``True``, список доступных "
|
||||||
"команд выводится перед каждым вводом."
|
"команд выводится перед каждым вводом."
|
||||||
@@ -86,7 +86,7 @@ msgstr ""
|
|||||||
"``repeat_command_groups_printing``: If ``True``, the list of available "
|
"``repeat_command_groups_printing``: If ``True``, the list of available "
|
||||||
"commands is displayed before each input."
|
"commands is displayed before each input."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:46
|
#: ../../root/api/app/index.rst:40
|
||||||
msgid ""
|
msgid ""
|
||||||
"``override_system_messages``: Если ``True``, стандартное форматирование "
|
"``override_system_messages``: Если ``True``, стандартное форматирование "
|
||||||
"(цвета, ASCII-арт) отключается."
|
"(цвета, ASCII-арт) отключается."
|
||||||
@@ -94,7 +94,7 @@ msgstr ""
|
|||||||
"``override_system_messages``: If ``True``, standard formatting (colors, "
|
"``override_system_messages``: If ``True``, standard formatting (colors, "
|
||||||
"ASCII art) is disabled."
|
"ASCII art) is disabled."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:47
|
#: ../../root/api/app/index.rst:41
|
||||||
msgid ""
|
msgid ""
|
||||||
"``autocompleter``: Экземпляр класса :ref:`AutoCompleter "
|
"``autocompleter``: Экземпляр класса :ref:`AutoCompleter "
|
||||||
"<root_api_app_autocompleter>`, отвечающий за автодополнение команд."
|
"<root_api_app_autocompleter>`, отвечающий за автодополнение команд."
|
||||||
@@ -103,29 +103,28 @@ msgstr ""
|
|||||||
"<root_api_app_autocompleter>` class responsible for command "
|
"<root_api_app_autocompleter>` class responsible for command "
|
||||||
"autocompletion."
|
"autocompletion."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:48
|
#: ../../root/api/app/index.rst:42
|
||||||
msgid ""
|
#, fuzzy
|
||||||
"``print_func``: Функция для вывода всех системных сообщений (по умолчанию"
|
msgid "``printer``: Функция для вывода всех системных сообщений."
|
||||||
" ``rich.Console().print``)."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"``print_func``: Function for outputting all system messages (defaults to "
|
"``print_func``: Function for outputting all system messages (defaults to "
|
||||||
"``rich.Console().print``)."
|
"``rich.Console().print``)."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:53
|
#: ../../root/api/app/index.rst:47
|
||||||
msgid ""
|
msgid ""
|
||||||
"В приложениях на Argenta регистр вводимых команд не важен, проверка на "
|
"В приложениях на Argenta регистр вводимых команд не важен, проверка на "
|
||||||
"существование и роутинг команд производится на основании триггеров, "
|
"существование и роутинг команд производится на основании триггеров, "
|
||||||
"приведённых к нижнему регистру."
|
"приведённых к нижнему регистру."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"In applications on Argenta, the case of the entered commands is not important, checking for the "
|
"In applications on Argenta, the case of the entered commands is not "
|
||||||
" existence and routing of commands is performed based on triggers "
|
"important, checking for the existence and routing of commands is "
|
||||||
"reduced to lowercase."
|
"performed based on triggers reduced to lowercase."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:56
|
#: ../../root/api/app/index.rst:50
|
||||||
msgid "Основные методы"
|
msgid "Основные методы"
|
||||||
msgstr "Main Methods"
|
msgstr "Main Methods"
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:60
|
#: ../../root/api/app/index.rst:54
|
||||||
msgid ""
|
msgid ""
|
||||||
"Регистрирует роутер в приложении. Все команды из этого роутера становятся"
|
"Регистрирует роутер в приложении. Все команды из этого роутера становятся"
|
||||||
" доступными для вызова."
|
" доступными для вызова."
|
||||||
@@ -137,19 +136,19 @@ msgstr ""
|
|||||||
msgid "Parameters"
|
msgid "Parameters"
|
||||||
msgstr "Parameters"
|
msgstr "Parameters"
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:62
|
#: ../../root/api/app/index.rst:56
|
||||||
msgid "Экземпляр ``Router`` для регистрации."
|
msgid "Экземпляр ``Router`` для регистрации."
|
||||||
msgstr "``Router`` instance to register."
|
msgstr "``Router`` instance to register."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:66
|
#: ../../root/api/app/index.rst:60
|
||||||
msgid "Регистрирует несколько роутеров одновременно."
|
msgid "Регистрирует несколько роутеров одновременно."
|
||||||
msgstr "Registers multiple routers simultaneously."
|
msgstr "Registers multiple routers simultaneously."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:68
|
#: ../../root/api/app/index.rst:62
|
||||||
msgid "Последовательность экземпляров ``Router`` для регистрации."
|
msgid "Последовательность экземпляров ``Router`` для регистрации."
|
||||||
msgstr "Sequence of ``Router`` instances to register."
|
msgstr "Sequence of ``Router`` instances to register."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:72
|
#: ../../root/api/app/index.rst:66
|
||||||
msgid ""
|
msgid ""
|
||||||
"Добавляет текстовое сообщение, которое выводится при запуске приложения "
|
"Добавляет текстовое сообщение, которое выводится при запуске приложения "
|
||||||
"после ``initial_message``."
|
"после ``initial_message``."
|
||||||
@@ -157,11 +156,11 @@ msgstr ""
|
|||||||
"Adds a text message that is displayed when the application starts after "
|
"Adds a text message that is displayed when the application starts after "
|
||||||
"``initial_message``."
|
"``initial_message``."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:74
|
#: ../../root/api/app/index.rst:68
|
||||||
msgid "Строка с сообщением."
|
msgid "Строка с сообщением."
|
||||||
msgstr "String with the message."
|
msgstr "String with the message."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:77
|
#: ../../root/api/app/index.rst:71
|
||||||
msgid ""
|
msgid ""
|
||||||
"Для вывода стандартных сообщений можно использовать готовые шаблоны из "
|
"Для вывода стандартных сообщений можно использовать готовые шаблоны из "
|
||||||
":ref:`PredefinedMessages <root_api_predefined_messages>`."
|
":ref:`PredefinedMessages <root_api_predefined_messages>`."
|
||||||
@@ -169,11 +168,11 @@ msgstr ""
|
|||||||
"For outputting standard messages, you can use ready-made templates from "
|
"For outputting standard messages, you can use ready-made templates from "
|
||||||
":ref:`PredefinedMessages <root_api_predefined_messages>`."
|
":ref:`PredefinedMessages <root_api_predefined_messages>`."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:82
|
#: ../../root/api/app/index.rst:76
|
||||||
msgid "Методы установки обработчиков"
|
msgid "Методы установки обработчиков"
|
||||||
msgstr "Handler Setup Methods"
|
msgstr "Handler Setup Methods"
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:84
|
#: ../../root/api/app/index.rst:78
|
||||||
msgid ""
|
msgid ""
|
||||||
"``App`` позволяет настраивать реакцию на различные события, такие как "
|
"``App`` позволяет настраивать реакцию на различные события, такие как "
|
||||||
"ошибки ввода или неизвестные команды."
|
"ошибки ввода или неизвестные команды."
|
||||||
@@ -181,7 +180,7 @@ msgstr ""
|
|||||||
"``App`` allows you to configure responses to various events, such as "
|
"``App`` allows you to configure responses to various events, such as "
|
||||||
"input errors or unknown commands."
|
"input errors or unknown commands."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:87
|
#: ../../root/api/app/index.rst:81
|
||||||
msgid ""
|
msgid ""
|
||||||
"Подробнее об исключениях и их обработке в соответствующем :ref:`разделе "
|
"Подробнее об исключениях и их обработке в соответствующем :ref:`разделе "
|
||||||
"документации <root_error_handling>`."
|
"документации <root_error_handling>`."
|
||||||
@@ -189,59 +188,59 @@ msgstr ""
|
|||||||
"For more details on exceptions and their handling, see the corresponding "
|
"For more details on exceptions and their handling, see the corresponding "
|
||||||
":ref:`documentation section <root_error_handling>`."
|
":ref:`documentation section <root_error_handling>`."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:93
|
#: ../../root/api/app/index.rst:87
|
||||||
msgid "Устанавливает шаблон для форматирования описания команды."
|
msgid "Устанавливает шаблон для форматирования описания команды."
|
||||||
msgstr "Sets the template for formatting command descriptions."
|
msgstr "Sets the template for formatting command descriptions."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:95
|
#: ../../root/api/app/index.rst:89
|
||||||
msgid "Обработчик принимает триггер команды (``str``) и её описание (``str``)."
|
msgid "Обработчик принимает триггер команды (``str``) и её описание (``str``)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"The handler accepts the command trigger (``str``) and its description "
|
"The handler accepts the command trigger (``str``) and its description "
|
||||||
"(``str``)."
|
"(``str``)."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:101
|
#: ../../root/api/app/index.rst:95
|
||||||
msgid "Устанавливает обработчик при некорректном введённом синтаксисе флагов."
|
msgid "Устанавливает обработчик при некорректном введённом синтаксисе флагов."
|
||||||
msgstr "Sets the handler for incorrect flag syntax input."
|
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 "Обработчик принимает строку, введённую пользователем."
|
msgid "Обработчик принимает строку, введённую пользователем."
|
||||||
msgstr "The handler accepts the string entered by the user."
|
msgstr "The handler accepts the string entered by the user."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:109
|
#: ../../root/api/app/index.rst:103
|
||||||
msgid "Устанавливает обработчик при повторяющихся флагах в введённой команде."
|
msgid "Устанавливает обработчик при повторяющихся флагах в введённой команде."
|
||||||
msgstr "Sets the handler for duplicate flags in the entered command."
|
msgstr "Sets the handler for duplicate flags in the entered command."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:117
|
#: ../../root/api/app/index.rst:111
|
||||||
msgid "Устанавливает обработчик при вводе неизвестной команды."
|
msgid "Устанавливает обработчик при вводе неизвестной команды."
|
||||||
msgstr "Sets the handler for entering an unknown command."
|
msgstr "Sets the handler for entering an unknown command."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:119
|
#: ../../root/api/app/index.rst:113
|
||||||
msgid "Обработчик принимает объект ``InputCommand`` - объект введённой команды."
|
msgid "Обработчик принимает объект ``InputCommand`` - объект введённой команды."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"The handler accepts an ``InputCommand`` object - the entered command "
|
"The handler accepts an ``InputCommand`` object - the entered command "
|
||||||
"object."
|
"object."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:125
|
#: ../../root/api/app/index.rst:119
|
||||||
msgid "Устанавливает обработчик при вводе пустой строки."
|
msgid "Устанавливает обработчик при вводе пустой строки."
|
||||||
msgstr "Sets the handler for entering an empty string."
|
msgstr "Sets the handler for entering an empty string."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:127
|
#: ../../root/api/app/index.rst:121
|
||||||
msgid "Обработчик не принимает аргументов."
|
msgid "Обработчик не принимает аргументов."
|
||||||
msgstr "The handler accepts no arguments."
|
msgstr "The handler accepts no arguments."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:133
|
#: ../../root/api/app/index.rst:127
|
||||||
msgid "Переопределяет стандартное поведение при вызове команды выхода."
|
msgid "Переопределяет стандартное поведение при вызове команды выхода."
|
||||||
msgstr "Overrides the default behavior when the exit command is invoked."
|
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``."
|
msgid "Обработчик принимает объект ``Response``."
|
||||||
msgstr "The handler accepts a ``Response`` object."
|
msgstr "The handler accepts a ``Response`` object."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:148
|
#: ../../root/api/app/index.rst:142
|
||||||
msgid "PredefinedMessages"
|
msgid "PredefinedMessages"
|
||||||
msgstr "PredefinedMessages"
|
msgstr "PredefinedMessages"
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:150
|
#: ../../root/api/app/index.rst:144
|
||||||
msgid ""
|
msgid ""
|
||||||
"``PredefinedMessages`` — это контейнер, содержащий набор готовых к "
|
"``PredefinedMessages`` — это контейнер, содержащий набор готовых к "
|
||||||
"использованию сообщений. Они отформатированы с использованием синтаксиса "
|
"использованию сообщений. Они отформатированы с использованием синтаксиса "
|
||||||
@@ -252,31 +251,31 @@ msgstr ""
|
|||||||
"messages. They are formatted using ``rich`` syntax and are intended for "
|
"messages. They are formatted using ``rich`` syntax and are intended for "
|
||||||
"displaying standard information, such as usage hints."
|
"displaying standard information, such as usage hints."
|
||||||
|
|
||||||
#: ../../root/api/app/index.rst:152
|
#: ../../root/api/app/index.rst:146
|
||||||
msgid "Рекомендуется использовать их при старте приложения."
|
msgid "Рекомендуется использовать их при старте приложения."
|
||||||
msgstr "It is recommended to use them when starting the application."
|
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]``"
|
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]``"
|
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>``"
|
msgid "Отображается как: ``Usage: <command> <flags>``"
|
||||||
msgstr "Displayed as: ``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]``"
|
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]``"
|
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``"
|
msgid "Отображается как: ``Help: <command> --help``"
|
||||||
msgstr "Displayed as: ``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>``"
|
msgid "Строка: ``[b dim]Autocomplete[/b dim]: [i]<part>[/i] [bold]<tab>``"
|
||||||
msgstr "String: ``[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>``"
|
msgid "Отображается как: ``Autocomplete: <part> <tab>``"
|
||||||
msgstr "Displayed as: ``Autocomplete: <part> <tab>``"
|
msgstr "Displayed as: ``Autocomplete: <part> <tab>``"
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Argenta \n"
|
"Project-Id-Version: Argenta \n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
@@ -30,10 +30,11 @@ msgid ""
|
|||||||
"позволяет получать внешнюю конфигурацию в момент старта (например, путь к"
|
"позволяет получать внешнюю конфигурацию в момент старта (например, путь к"
|
||||||
" файлу настроек, флаги отладки или режим запуска)."
|
" файлу настроек, флаги отладки или режим запуска)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"``ArgParser`` is designed for processing **command-line arguments** passed to the "
|
"``ArgParser`` is designed for processing **command-line arguments** "
|
||||||
"application at startup. It's important not to confuse them with flags that the user "
|
"passed to the application at startup. It's important not to confuse them "
|
||||||
"enters in interactive mode. ``ArgParser`` allows receiving external configuration at "
|
"with flags that the user enters in interactive mode. ``ArgParser`` allows"
|
||||||
"startup (e.g., path to settings file, debug flags, or launch mode)."
|
" receiving external configuration at startup (e.g., path to settings "
|
||||||
|
"file, debug flags, or launch mode)."
|
||||||
|
|
||||||
#: ../../root/api/orchestrator/argparser.rst:11
|
#: ../../root/api/orchestrator/argparser.rst:11
|
||||||
msgid "Инициализация"
|
msgid "Инициализация"
|
||||||
@@ -81,8 +82,9 @@ msgid ""
|
|||||||
"экземпляр ``ArgParser``, атрибут ``parsed_argspace`` будет содержать "
|
"экземпляр ``ArgParser``, атрибут ``parsed_argspace`` будет содержать "
|
||||||
"пустой ``ArgSpace``."
|
"пустой ``ArgSpace``."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Before initializing ``Orchestrator``, to whose constructor an ``ArgParser`` instance "
|
"Before initializing ``Orchestrator``, to whose constructor an "
|
||||||
"was passed, the ``parsed_argspace`` attribute will contain an empty ``ArgSpace``."
|
"``ArgParser`` instance was passed, the ``parsed_argspace`` attribute will"
|
||||||
|
" contain an empty ``ArgSpace``."
|
||||||
|
|
||||||
#: ../../root/api/orchestrator/argparser.rst:40
|
#: ../../root/api/orchestrator/argparser.rst:40
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -90,8 +92,9 @@ msgid ""
|
|||||||
"``Orchestrator``, поэтому использовать ``parsed_argspace`` "
|
"``Orchestrator``, поэтому использовать ``parsed_argspace`` "
|
||||||
"**целесообразно только после** этого."
|
"**целесообразно только после** этого."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Parsing and validation of arguments occur during ``Orchestrator`` initialization, "
|
"Parsing and validation of arguments occur during ``Orchestrator`` "
|
||||||
"so using ``parsed_argspace`` is **advisable only after** that."
|
"initialization, so using ``parsed_argspace`` is **advisable only after** "
|
||||||
|
"that."
|
||||||
|
|
||||||
#: ../../root/api/orchestrator/argparser.rst:45
|
#: ../../root/api/orchestrator/argparser.rst:45
|
||||||
msgid "Лучшие практики"
|
msgid "Лучшие практики"
|
||||||
@@ -104,9 +107,10 @@ msgid ""
|
|||||||
"``ArgSpace`` через DI. Подробнее см. :ref:`здесь "
|
"``ArgSpace`` через DI. Подробнее см. :ref:`здесь "
|
||||||
"<root_dependency_injection>`."
|
"<root_dependency_injection>`."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Using the ``parsed_argspace`` attribute is recommended only during the application "
|
"Using the ``parsed_argspace`` attribute is recommended only during the "
|
||||||
"setup phase. In handlers, the best practice is to obtain ``ArgSpace`` through DI. "
|
"application setup phase. In handlers, the best practice is to obtain "
|
||||||
"For more details, see :ref:`here <root_dependency_injection>`."
|
"``ArgSpace`` through DI. For more details, see :ref:`here "
|
||||||
|
"<root_dependency_injection>`."
|
||||||
|
|
||||||
#: ../../root/api/orchestrator/argparser.rst:49
|
#: ../../root/api/orchestrator/argparser.rst:49
|
||||||
msgid "**Пример использования:**"
|
msgid "**Пример использования:**"
|
||||||
@@ -129,8 +133,8 @@ msgid ""
|
|||||||
"При работе с аргументами командной строки стандартный ``ArgumentParser`` "
|
"При работе с аргументами командной строки стандартный ``ArgumentParser`` "
|
||||||
"автоматически обрабатывает следующие ситуации:"
|
"автоматически обрабатывает следующие ситуации:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"When working with command-line arguments, the standard ``ArgumentParser`` "
|
"When working with command-line arguments, the standard ``ArgumentParser``"
|
||||||
"automatically handles the following situations:"
|
" automatically handles the following situations:"
|
||||||
|
|
||||||
#: ../../root/api/orchestrator/argparser.rst:63
|
#: ../../root/api/orchestrator/argparser.rst:63
|
||||||
msgid "**Отсутствие обязательного аргумента:**"
|
msgid "**Отсутствие обязательного аргумента:**"
|
||||||
@@ -149,6 +153,12 @@ msgid ""
|
|||||||
"При использовании аргумента с ``is_deprecated=True`` выводится "
|
"При использовании аргумента с ``is_deprecated=True`` выводится "
|
||||||
"предупреждение, но выполнение продолжается:"
|
"предупреждение, но выполнение продолжается:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"When using an argument with ``is_deprecated=True``, a warning is displayed, "
|
"When using an argument with ``is_deprecated=True``, a warning is "
|
||||||
"but execution continues:"
|
"displayed, but execution continues:"
|
||||||
|
|
||||||
|
#: ../../root/api/orchestrator/argparser.rst:90
|
||||||
|
msgid ""
|
||||||
|
"Параметр поддерживается начиная с версии CPython 3.13, если версия ниже, "
|
||||||
|
"то параметр будет игнорироваться."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -579,7 +579,7 @@ msgstr ""
|
|||||||
msgid ""
|
msgid ""
|
||||||
"Откройте `127.0.0.1:8000` в браузере, чтобы просмотреть сгенерированную "
|
"Откройте `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
|
#: ../../root/contributing.rst:233
|
||||||
msgid ""
|
msgid ""
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ msgstr "How Does It Work?"
|
|||||||
|
|
||||||
#: ../../root/dependency_injection.rst:51
|
#: ../../root/dependency_injection.rst:51
|
||||||
msgid "В основе DI в Argenta лежат **провайдеры** и **контейнер**."
|
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
|
#: ../../root/dependency_injection.rst:53
|
||||||
msgid ""
|
msgid ""
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ msgstr ""
|
|||||||
"The main purpose of flags is to provide a way to change the command's "
|
"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: "
|
"logic without reworking it. A command can operate in several modes: "
|
||||||
"standard, verbose, debug, or simplified. Flags switch these modes on user"
|
"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
|
#: ../../root/flags.rst:17
|
||||||
msgid "Опциональность и удобство"
|
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 ""
|
msgstr ""
|
||||||
"Project-Id-Version: Argenta \n"
|
"Project-Id-Version: Argenta \n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
@@ -90,20 +90,20 @@ msgstr "Output Customization"
|
|||||||
#: ../../root/overriding_formatting.rst:32
|
#: ../../root/overriding_formatting.rst:32
|
||||||
msgid ""
|
msgid ""
|
||||||
"Для полной замены логики вывода текста в конструкторе ``App`` "
|
"Для полной замены логики вывода текста в конструкторе ``App`` "
|
||||||
"предусмотрен параметр ``print_func``."
|
"предусмотрен параметр ``printer``."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"For complete replacement of text output logic, the ``App`` constructor "
|
"For complete replacement of text output logic, the ``App`` constructor "
|
||||||
"provides the ``print_func`` parameter."
|
"provides the ``printer`` parameter."
|
||||||
|
|
||||||
#: ../../root/overriding_formatting.rst:34
|
#: ../../root/overriding_formatting.rst:34
|
||||||
msgid ""
|
msgid ""
|
||||||
"**print_func**: ``Callable[[str], None]`` Этот параметр позволяет "
|
"**printer**: ``Callable[[str], None]`` Этот параметр позволяет передать "
|
||||||
"передать любую вызываемую сущность (например, функцию), которая будет "
|
"любую вызываемую сущность (например, функцию), которая будет "
|
||||||
"использоваться для вывода всех системных сообщений. По умолчанию это "
|
"использоваться для вывода всех системных сообщений. По умолчанию это "
|
||||||
"``rich.console.Console().print``. Вы можете передать сюда свою функцию, "
|
"``rich.console.Console().print``. Вы можете передать сюда свою функцию, "
|
||||||
"чтобы, например, логировать вывод в файл или отправлять его по сети."
|
"чтобы, например, логировать вывод в файл или отправлять его по сети."
|
||||||
msgstr ""
|
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"
|
"any callable entity (for example, a function) that will be used to output"
|
||||||
" all system messages. By default, this is "
|
" all system messages. By default, this is "
|
||||||
"``rich.console.Console().print``. You can pass your own function here to,"
|
"``rich.console.Console().print``. You can pass your own function here to,"
|
||||||
|
|||||||
+13
-19
@@ -13,26 +13,20 @@ App
|
|||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:linenos:
|
:linenos:
|
||||||
|
|
||||||
AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine
|
def __init__(
|
||||||
DEFAULT_DIVIDING_LINE: StaticDividingLine = StaticDividingLine()
|
self,
|
||||||
|
*,
|
||||||
DEFAULT_PRINT_FUNC: Printer = Console().print
|
prompt: str = ">>> ",
|
||||||
DEFAULT_AUTOCOMPLETER: AutoCompleter = AutoCompleter()
|
initial_message: str = "Argenta",
|
||||||
DEFAULT_EXIT_COMMAND: Command = Command("Q", description="Exit command")
|
farewell_message: str = "See you",
|
||||||
|
exit_command: Command = Command("q", description="Exit command"),
|
||||||
.. code-block:: python
|
system_router_title: str = "System points:",
|
||||||
:linenos:
|
dividing_line: StaticDividingLine | DynamicDividingLine | None = None,
|
||||||
|
|
||||||
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,
|
repeat_command_groups_printing: bool = False,
|
||||||
override_system_messages: bool = False,
|
override_system_messages: bool = False,
|
||||||
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
|
autocompleter: AutoCompleter | None = None,
|
||||||
print_func: Printer = DEFAULT_PRINT_FUNC) -> None
|
printer: Printer = Console().print,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
Создаёт и настраивает экземпляр приложения.
|
Создаёт и настраивает экземпляр приложения.
|
||||||
|
|
||||||
@@ -45,7 +39,7 @@ App
|
|||||||
* ``repeat_command_groups_printing``: Если ``True``, список доступных команд выводится перед каждым вводом.
|
* ``repeat_command_groups_printing``: Если ``True``, список доступных команд выводится перед каждым вводом.
|
||||||
* ``override_system_messages``: Если ``True``, стандартное форматирование (цвета, ASCII-арт) отключается.
|
* ``override_system_messages``: Если ``True``, стандартное форматирование (цвета, ASCII-арт) отключается.
|
||||||
* ``autocompleter``: Экземпляр класса :ref:`AutoCompleter <root_api_app_autocompleter>`, отвечающий за автодополнение команд.
|
* ``autocompleter``: Экземпляр класса :ref:`AutoCompleter <root_api_app_autocompleter>`, отвечающий за автодополнение команд.
|
||||||
* ``print_func``: Функция для вывода всех системных сообщений (по умолчанию ``rich.Console().print``).
|
* ``printer``: Функция для вывода всех системных сообщений.
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@@ -84,3 +84,8 @@ ArgParser
|
|||||||
|
|
||||||
$ python app.py --old-param value
|
$ python app.py --old-param value
|
||||||
Warning: argument --old-param is deprecated
|
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``. Вы можете передать сюда свою функцию, чтобы, например, логировать вывод в файл или отправлять его по сети.
|
Этот параметр позволяет передать любую вызываемую сущность (например, функцию), которая будет использоваться для вывода всех системных сообщений. По умолчанию это ``rich.console.Console().print``. Вы можете передать сюда свою функцию, чтобы, например, логировать вывод в файл или отправлять его по сети.
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
from .benchmarks import *
|
|
||||||
+10
-40
@@ -1,48 +1,18 @@
|
|||||||
from concurrent.futures import ProcessPoolExecutor
|
from argenta import App, Orchestrator, Command
|
||||||
import os
|
from argenta.app import DynamicDividingLine
|
||||||
|
from .handlers import router
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
app = App(initial_message="metrics", exit_command=Command('exit', aliases=['quit']))
|
||||||
console = Console()
|
orchestrator = Orchestrator()
|
||||||
all_benchmarks: list[Benchmark] = Benchmarks.get_benchmarks()
|
|
||||||
|
|
||||||
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]] = {}
|
def main() -> None:
|
||||||
|
app.include_router(router)
|
||||||
for result in results:
|
app.set_description_message_pattern(
|
||||||
type_paired_benchmarks.setdefault(result.type_, []).append(result)
|
lambda command, description: f'[bold cyan]▸[/bold cyan] [bold white]{command}[/bold white] [dim]│[/dim] [yellow italic]{description}[/yellow italic]'
|
||||||
|
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
|
orchestrator.start_polling(app)
|
||||||
console.print(table)
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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__ = [
|
__all__ = [
|
||||||
"benchmark_no_aliases",
|
"benchmark_no_aliases",
|
||||||
"benchmark_many_aliases",
|
"benchmark_with_many_aliases",
|
||||||
"benchmark_few_aliases",
|
"benchmark_few_aliases",
|
||||||
"benchmark_extreme_aliases",
|
"benchmark_extreme_aliases",
|
||||||
"benchmark_very_many_aliases"
|
"benchmark_very_many_aliases"
|
||||||
]
|
]
|
||||||
|
|
||||||
from argenta import App
|
from argenta import App
|
||||||
from argenta.router import Router
|
|
||||||
from argenta.command.models import Command
|
from argenta.command.models import Command
|
||||||
from argenta.response import Response
|
from argenta.response import Response
|
||||||
|
from argenta.router import Router
|
||||||
|
|
||||||
from ..utils import get_time_of_pre_cycle_setup
|
from .entity import benchmarks
|
||||||
from ..registry import benchmark
|
|
||||||
|
|
||||||
|
|
||||||
@benchmark(type_="pre_cycle_setup", description="With no aliases")
|
@benchmarks.register(type_="pre_cycle_setup", description="With no aliases")
|
||||||
def benchmark_no_aliases() -> float:
|
def benchmark_no_aliases() -> None:
|
||||||
app = App(override_system_messages=True)
|
app = App(override_system_messages=True)
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -33,12 +32,11 @@ def benchmark_no_aliases() -> float:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
execution_time = get_time_of_pre_cycle_setup(app)
|
app._pre_cycle_setup()
|
||||||
return execution_time
|
|
||||||
|
|
||||||
|
|
||||||
@benchmark(type_="pre_cycle_setup", description="With few aliases (6 total)")
|
@benchmarks.register(type_="pre_cycle_setup", description="With few aliases (6 total)")
|
||||||
def benchmark_few_aliases() -> float:
|
def benchmark_few_aliases() -> None:
|
||||||
app = App(override_system_messages=True)
|
app = App(override_system_messages=True)
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -55,12 +53,11 @@ def benchmark_few_aliases() -> float:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
execution_time = get_time_of_pre_cycle_setup(app)
|
app._pre_cycle_setup()
|
||||||
return execution_time
|
|
||||||
|
|
||||||
|
|
||||||
@benchmark(type_="pre_cycle_setup", description="With many aliases (15 total)")
|
@benchmarks.register(type_="pre_cycle_setup", description="With many aliases (15 total)")
|
||||||
def benchmark_many_aliases() -> float:
|
def benchmark_with_many_aliases() -> None:
|
||||||
app = App(override_system_messages=True)
|
app = App(override_system_messages=True)
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -77,12 +74,11 @@ def benchmark_many_aliases() -> float:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
execution_time = get_time_of_pre_cycle_setup(app)
|
app._pre_cycle_setup()
|
||||||
return execution_time
|
|
||||||
|
|
||||||
|
|
||||||
@benchmark(type_="pre_cycle_setup", description="With very many aliases (60 total)")
|
@benchmarks.register(type_="pre_cycle_setup", description="With very many aliases (60 total)")
|
||||||
def benchmark_very_many_aliases() -> float:
|
def benchmark_very_many_aliases() -> None:
|
||||||
app = App(override_system_messages=True)
|
app = App(override_system_messages=True)
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -99,12 +95,11 @@ def benchmark_very_many_aliases() -> float:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
execution_time = get_time_of_pre_cycle_setup(app)
|
app._pre_cycle_setup()
|
||||||
return execution_time
|
|
||||||
|
|
||||||
|
|
||||||
@benchmark(type_="pre_cycle_setup", description="With extreme aliases (300 total)")
|
@benchmarks.register(type_="pre_cycle_setup", description="With extreme aliases (300 total)")
|
||||||
def benchmark_extreme_aliases() -> float:
|
def benchmark_extreme_aliases() -> None:
|
||||||
app = App(override_system_messages=True)
|
app = App(override_system_messages=True)
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -121,5 +116,4 @@ def benchmark_extreme_aliases() -> float:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
execution_time = get_time_of_pre_cycle_setup(app)
|
app._pre_cycle_setup()
|
||||||
return execution_time
|
|
||||||
|
|||||||
@@ -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__":
|
app = App(override_system_messages=True)
|
||||||
test_commands: set[str] = {"start", "qwertyu", "stop", "exit"}
|
router = Router()
|
||||||
hist_file: str = "history.txt"
|
|
||||||
|
|
||||||
ac: AutoCompleter = AutoCompleter(autocomplete_button='tab')
|
@router.command(Command('command'))
|
||||||
ac.initial_setup(test_commands)
|
def handler(_res: Response) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
while True:
|
@router.command(Command('command_other'))
|
||||||
inp: str = ac.prompt(">>> ").strip()
|
def handler2(_res: Response) -> None:
|
||||||
if inp == "exit":
|
pass
|
||||||
break
|
|
||||||
|
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
|
from .routers import router
|
||||||
|
|
||||||
app: App = App(prompt='>>> ', dividing_line=DynamicDividingLine('~'))
|
app: App = App(prompt='>>> ', dividing_line=None)
|
||||||
orchestrator: Orchestrator = Orchestrator()
|
orchestrator: Orchestrator = Orchestrator()
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
from prompt_toolkit import HTML
|
||||||
|
|
||||||
from argenta import App, Orchestrator
|
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 argenta.app.dividing_line.models import DynamicDividingLine
|
||||||
from mock.mock_app.routers import work_router
|
from mock.mock_app.routers import work_router
|
||||||
|
|
||||||
app: App = App(
|
app: App = App(
|
||||||
dividing_line=DynamicDividingLine('^'),
|
dividing_line=StaticDividingLine('~')
|
||||||
)
|
)
|
||||||
orchestrator: Orchestrator = Orchestrator()
|
orchestrator: Orchestrator = Orchestrator()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from argenta import Command, Response, Router
|
from argenta import Command, Response, Router
|
||||||
from argenta.command import Flag, Flags
|
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(
|
@work_router.command(
|
||||||
@@ -13,5 +14,5 @@ work_router: Router = Router(title="Base points:", disable_redirect_stdout=True)
|
|||||||
description="Hello, world!")
|
description="Hello, world!")
|
||||||
)
|
)
|
||||||
def command_help(response: Response):
|
def command_help(response: Response):
|
||||||
c = input("Enter your name: ")
|
n = input('sfgdheth')
|
||||||
print(f"Hello, {c}!")
|
print(f"Hello,{n} {response.input_flags.get_flag_by_name('test', with_status=ValidationStatus.VALID)}")
|
||||||
|
|||||||
+30
-13
@@ -1,9 +1,9 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "argenta"
|
name = "argenta"
|
||||||
version = "1.1.2"
|
version = "1.2.0"
|
||||||
description = "Python library for building modular CLI applications"
|
description = "Python library for building modular CLI applications"
|
||||||
authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }]
|
authors = [{ name = "kolo", email = "kolo.is.main@gmail.com" }]
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12,<3.15"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -14,6 +14,13 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
{include-group = "linters"},
|
||||||
|
{include-group = "typecheckers"},
|
||||||
|
{include-group = "docs"},
|
||||||
|
{include-group = "tests"},
|
||||||
|
"scriv>=1.8.0",
|
||||||
|
]
|
||||||
linters = [
|
linters = [
|
||||||
"isort>=7.0.0",
|
"isort>=7.0.0",
|
||||||
"ruff>=0.12.12",
|
"ruff>=0.12.12",
|
||||||
@@ -35,17 +42,14 @@ tests = [
|
|||||||
"pytest-cov>=7.0.0",
|
"pytest-cov>=7.0.0",
|
||||||
"pytest-mock>=3.15.1",
|
"pytest-mock>=3.15.1",
|
||||||
]
|
]
|
||||||
|
metrics = [
|
||||||
|
"matplotlib>=3.10.8",
|
||||||
|
"psutil>=7.2.1",
|
||||||
|
"py-cpuinfo>=9.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
exclude = [
|
line-length=100
|
||||||
".idea",
|
|
||||||
"venv",
|
|
||||||
".git",
|
|
||||||
"poetry.lock",
|
|
||||||
".__pycache__",
|
|
||||||
"tests"
|
|
||||||
]
|
|
||||||
line-length=90
|
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
typeCheckingMode = "strict"
|
typeCheckingMode = "strict"
|
||||||
@@ -68,6 +72,19 @@ omit = [
|
|||||||
"src/argenta/metrics/*"
|
"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]
|
[tool.mypy]
|
||||||
disable_error_code = "import-untyped"
|
disable_error_code = "import-untyped"
|
||||||
|
|
||||||
@@ -75,5 +92,5 @@ disable_error_code = "import-untyped"
|
|||||||
line_length=90
|
line_length=90
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["uv_build"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "uv_build"
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ __all__ = ["AutoCompleter"]
|
|||||||
import sys
|
import sys
|
||||||
from typing import Callable, Iterable
|
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.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.document import Document
|
||||||
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
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.key_binding import KeyBindings, KeyPressEvent
|
||||||
from prompt_toolkit.lexers import Lexer
|
from prompt_toolkit.lexers import Lexer
|
||||||
from prompt_toolkit.styles import Style
|
from prompt_toolkit.styles import Style
|
||||||
@@ -97,34 +99,36 @@ class AutoCompleter:
|
|||||||
|
|
||||||
def _(event: KeyPressEvent) -> None:
|
def _(event: KeyPressEvent) -> None:
|
||||||
buff = event.app.current_buffer
|
buff = event.app.current_buffer
|
||||||
|
|
||||||
if buff.complete_state:
|
if buff.complete_state:
|
||||||
buff.complete_next()
|
buff.complete_next()
|
||||||
else:
|
return
|
||||||
completions = list(buff.completer.get_completions(buff.document, CompleteEvent()))
|
comps_gen = iter(buff.completer.get_completions(buff.document, CompleteEvent()))
|
||||||
if len(completions) == 1:
|
try:
|
||||||
buff.apply_completion(completions[0])
|
first = next(comps_gen)
|
||||||
else:
|
except StopIteration:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
_ = next(comps_gen)
|
||||||
buff.start_completion(select_first=False)
|
buff.start_completion(select_first=False)
|
||||||
|
except StopIteration:
|
||||||
|
buff.apply_completion(first)
|
||||||
|
|
||||||
kb.add(self.autocomplete_button)(_)
|
kb.add(self.autocomplete_button)(_)
|
||||||
|
|
||||||
history: InMemoryHistory | ThreadedHistory
|
history: InMemoryHistory | ThreadedHistory
|
||||||
|
|
||||||
if self.history_filename:
|
if self.history_filename:
|
||||||
history = ThreadedHistory(FileHistory(self.history_filename))
|
history = ThreadedHistory(FileHistory(self.history_filename))
|
||||||
else:
|
else:
|
||||||
history = InMemoryHistory()
|
history = InMemoryHistory()
|
||||||
|
|
||||||
style = Style.from_dict({'valid': '#00ff00', 'invalid': '#ff0000'})
|
style = Style.from_dict({'valid': '#00ff00', 'invalid': '#ff0000'})
|
||||||
|
|
||||||
self._session = PromptSession(
|
self._session = PromptSession(
|
||||||
history=history,
|
history=history,
|
||||||
completer=HistoryCompleter(history, all_commands),
|
completer=ThreadedCompleter(HistoryCompleter(history, all_commands)),
|
||||||
complete_while_typing=False,
|
complete_while_typing=False,
|
||||||
key_bindings=kb,
|
key_bindings=kb,
|
||||||
auto_suggest=AutoSuggestFromHistory() if self.auto_suggestions else None,
|
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,
|
lexer=CommandLexer(all_commands) if self.command_highlighting else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,5 +138,6 @@ class AutoCompleter:
|
|||||||
if self._session is None:
|
if self._session is None:
|
||||||
raise RuntimeError("Call initial_setup() before using prompt()")
|
raise RuntimeError("Call initial_setup() before using prompt()")
|
||||||
return self._session.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
|
:return: full line of dividing line as str
|
||||||
"""
|
"""
|
||||||
if is_override:
|
if is_override:
|
||||||
return f"\n{self.length * self.get_unit_part()}\n"
|
return self.length * self.get_unit_part()
|
||||||
else:
|
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):
|
class DynamicDividingLine(BaseDividingLine):
|
||||||
@@ -63,6 +63,6 @@ class DynamicDividingLine(BaseDividingLine):
|
|||||||
:return: full line of dividing line as str
|
:return: full line of dividing line as str
|
||||||
"""
|
"""
|
||||||
if is_override:
|
if is_override:
|
||||||
return f"\n{length * self.get_unit_part()}\n"
|
return length * self.get_unit_part()
|
||||||
else:
|
else:
|
||||||
return f"\n[dim]{self.get_unit_part() * length}[/dim]\n"
|
return f"[dim]{self.get_unit_part() * length}[/dim]"
|
||||||
|
|||||||
+97
-357
@@ -1,246 +1,94 @@
|
|||||||
__all__ = ["App"]
|
__all__ = ["App"]
|
||||||
|
|
||||||
import io
|
import difflib
|
||||||
import re
|
from typing import Never, TypeAlias
|
||||||
from contextlib import redirect_stdout
|
|
||||||
from typing import Callable, Never, TypeAlias
|
|
||||||
|
|
||||||
from art import text2art
|
|
||||||
from prompt_toolkit import HTML
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markup import escape
|
|
||||||
|
|
||||||
from argenta.app.autocompleter import AutoCompleter
|
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.dividing_line.models import DynamicDividingLine, StaticDividingLine
|
||||||
from argenta.app.protocols import (
|
from argenta.app.presentation.renderers import PlainRenderer, Renderer, RichRenderer
|
||||||
DescriptionMessageGenerator,
|
from argenta.app.presentation.viewers import Viewer
|
||||||
EmptyCommandHandler,
|
from argenta.app.protocols import Printer
|
||||||
NonStandardBehaviorHandler,
|
|
||||||
Printer,
|
|
||||||
)
|
|
||||||
from argenta.app.registered_routers.entity import RegisteredRouters
|
from argenta.app.registered_routers.entity import RegisteredRouters
|
||||||
from argenta.command.exceptions import (
|
from argenta.command.exceptions import (InputCommandException,
|
||||||
InputCommandException,
|
|
||||||
RepeatedInputFlagsException,
|
RepeatedInputFlagsException,
|
||||||
UnprocessedInputFlagException,
|
UnprocessedInputFlagException)
|
||||||
)
|
|
||||||
from argenta.router.exceptions import RepeatedAliasNameException, RepeatedTriggerNameException
|
|
||||||
from argenta.command.models import Command, InputCommand
|
from argenta.command.models import Command, InputCommand
|
||||||
from argenta.response import Response
|
from argenta.response import Response
|
||||||
from argenta.router import Router
|
from argenta.router import Router
|
||||||
|
from argenta.router.exceptions import (RepeatedAliasNameException,
|
||||||
|
RepeatedTriggerNameException)
|
||||||
|
|
||||||
Matches: TypeAlias = list[str] | list[Never]
|
Matches: TypeAlias = list[str] | list[Never]
|
||||||
|
|
||||||
_ANSI_ESCAPE_RE: re.Pattern[str] = re.compile(r"\u001b\[[0-9;]*m")
|
|
||||||
|
|
||||||
|
class BaseApp(BehaviorHandlersSettersMixin):
|
||||||
class BaseApp:
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
prompt: str | HTML,
|
prompt: str,
|
||||||
initial_message: str,
|
initial_message: str,
|
||||||
farewell_message: str,
|
farewell_message: str,
|
||||||
exit_command: Command,
|
exit_command: Command,
|
||||||
system_router_title: str,
|
system_router_title: str,
|
||||||
dividing_line: StaticDividingLine | DynamicDividingLine,
|
dividing_line: StaticDividingLine | DynamicDividingLine | None,
|
||||||
repeat_command_groups_printing: bool,
|
repeat_command_groups_printing: bool,
|
||||||
override_system_messages: bool,
|
override_system_messages: bool,
|
||||||
autocompleter: AutoCompleter,
|
autocompleter: AutoCompleter,
|
||||||
print_func: Printer,
|
printer: Printer,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._prompt: str | HTML = prompt
|
self._prompt: str = prompt
|
||||||
self._print_func: Printer = print_func
|
self._printer: Printer = printer
|
||||||
self._exit_command: Command = exit_command
|
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._repeat_command_groups_printing: bool = repeat_command_groups_printing
|
||||||
self._override_system_messages: bool = override_system_messages
|
self._override_system_messages: bool = override_system_messages
|
||||||
self._autocompleter: AutoCompleter = autocompleter
|
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.registered_routers: RegisteredRouters = RegisteredRouters()
|
||||||
self._messages_on_startup: list[str] = []
|
self._messages_on_startup: list[str] = []
|
||||||
|
|
||||||
self._incorrect_input_syntax_handler: NonStandardBehaviorHandler[str] = (
|
self._renderer: Renderer = PlainRenderer() if self._override_system_messages else RichRenderer()
|
||||||
lambda _: print_func(f"Incorrect flag syntax: {_}")
|
|
||||||
|
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] = (
|
self._handlers_fabric: BehaviorHandlersFabric = BehaviorHandlersFabric(
|
||||||
lambda _: print_func(f"Repeated input flags: {_}")
|
printer=self._printer,
|
||||||
)
|
renderer=self._renderer,
|
||||||
self._empty_input_command_handler: EmptyCommandHandler = lambda: print_func(
|
most_similar_command_getter=self._most_similar_command
|
||||||
"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)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_description_message_pattern(self, _: DescriptionMessageGenerator, /) -> None:
|
self._initial_message: str = self._renderer.render_initial_message(initial_message)
|
||||||
"""
|
self._farewell_message: str = self._renderer.render_farewell_message(farewell_message)
|
||||||
Public. Sets the output pattern of the available commands
|
|
||||||
:param _: output pattern of the available commands
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self._description_message_gen = _
|
|
||||||
|
|
||||||
def set_incorrect_input_syntax_handler(
|
super().__init__(
|
||||||
self, _: NonStandardBehaviorHandler[str], /
|
description_message_generator = self._handlers_fabric.generate_description_message_generator(),
|
||||||
) -> None:
|
incorrect_input_syntax_handler = self._handlers_fabric.generate_incorrect_input_syntax_handler(),
|
||||||
"""
|
repeated_input_flags_handler = self._handlers_fabric.generate_repeated_input_flags_handler(),
|
||||||
Public. Sets the handler for incorrect flags when entering a command
|
empty_input_command_handler = self._handlers_fabric.generate_empty_input_command_handler(),
|
||||||
:param _: handler for incorrect flags when entering a command
|
unknown_command_handler = self._handlers_fabric.generate_unknown_command_handler(),
|
||||||
:return: None
|
exit_command_handler = self._handlers_fabric.generate_exit_command_handler(self._farewell_message)
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
def _is_exit_command(self, command: InputCommand) -> bool:
|
def _is_exit_command(self, command: InputCommand) -> bool:
|
||||||
"""
|
if not self._system_router.command_handlers.get_command_handler_by_trigger(command.trigger.lower()):
|
||||||
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
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def _is_unknown_command(self, input_command: InputCommand) -> bool:
|
def _is_unknown_command(self, input_command: InputCommand) -> bool:
|
||||||
if not self.registered_routers.get_router_by_trigger(input_command.trigger.lower()):
|
if not self.registered_routers.get_router_by_trigger(input_command.trigger.lower()):
|
||||||
return True
|
return True
|
||||||
return False
|
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:
|
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):
|
if isinstance(error, UnprocessedInputFlagException):
|
||||||
self._incorrect_input_syntax_handler(raw_command)
|
self._incorrect_input_syntax_handler(raw_command)
|
||||||
elif isinstance(error, RepeatedInputFlagsException):
|
elif isinstance(error, RepeatedInputFlagsException):
|
||||||
@@ -248,123 +96,42 @@ class BaseApp:
|
|||||||
else:
|
else:
|
||||||
self._empty_input_command_handler()
|
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:
|
def _validate_routers_for_collisions(self) -> None:
|
||||||
"""
|
seen_names: set[str] = set()
|
||||||
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()
|
|
||||||
|
|
||||||
for router_entity in self.registered_routers:
|
for router_entity in self.registered_routers:
|
||||||
union_units: set[str] = all_triggers | all_aliases
|
if not seen_names.isdisjoint(router_entity.triggers):
|
||||||
trigger_collisions: set[str] = union_units & router_entity.triggers
|
|
||||||
if trigger_collisions:
|
|
||||||
raise RepeatedTriggerNameException()
|
raise RepeatedTriggerNameException()
|
||||||
|
|
||||||
alias_collisions: set[str] = union_units & router_entity.aliases
|
alias_collisions = seen_names.intersection(router_entity.aliases)
|
||||||
if alias_collisions:
|
if alias_collisions:
|
||||||
raise RepeatedAliasNameException(alias_collisions)
|
raise RepeatedAliasNameException(alias_collisions)
|
||||||
|
|
||||||
all_triggers.update(router_entity.triggers)
|
seen_names.update(router_entity.triggers)
|
||||||
all_aliases.update(router_entity.aliases)
|
seen_names.update(router_entity.aliases)
|
||||||
|
|
||||||
def _most_similar_command(self, unknown_command: str) -> str | None:
|
def _most_similar_command(self, unknown_command: str) -> str | None:
|
||||||
all_commands = self.registered_routers.get_triggers()
|
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(
|
def _setup_system_router(self) -> None:
|
||||||
cmd for cmd in all_commands if cmd.startswith(unknown_command)
|
@self._system_router.command(self._exit_command)
|
||||||
)
|
def _(response: Response) -> None:
|
||||||
matches_startswith_cmd: Matches = sorted(
|
self._exit_command_handler(response)
|
||||||
cmd for cmd in all_commands if unknown_command.startswith(cmd)
|
|
||||||
)
|
|
||||||
|
|
||||||
matches: Matches = matches_startswith_unknown_command or matches_startswith_cmd
|
self.registered_routers.add_registered_router(self._system_router)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def _pre_cycle_setup(self) -> None:
|
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._setup_system_router()
|
||||||
self._validate_routers_for_collisions()
|
self._validate_routers_for_collisions()
|
||||||
|
|
||||||
self._autocompleter.initial_setup(self.registered_routers.get_triggers())
|
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:
|
if self._messages_on_startup:
|
||||||
print("\n")
|
self._viewer.view_messages_on_startup(self._messages_on_startup)
|
||||||
|
|
||||||
if not self._repeat_command_groups_printing:
|
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:
|
def _process_exist_and_valid_command(self, input_command: InputCommand) -> None:
|
||||||
processing_router = self.registered_routers.get_router_by_trigger(input_command.trigger.lower())
|
processing_router = self.registered_routers.get_router_by_trigger(input_command.trigger.lower())
|
||||||
@@ -372,48 +139,57 @@ class BaseApp:
|
|||||||
if not processing_router:
|
if not processing_router:
|
||||||
raise RuntimeError(f"Router for '{input_command.trigger}' not found. Panic!")
|
raise RuntimeError(f"Router for '{input_command.trigger}' not found. Panic!")
|
||||||
|
|
||||||
if processing_router.disable_redirect_stdout:
|
self._viewer.view_framed_text_from_generator(
|
||||||
dividing_line_unit_part: str = self._dividing_line.get_unit_part()
|
output_text_generator=lambda: processing_router.finds_appropriate_handler(input_command),
|
||||||
self._print_func(
|
is_stdout_redirected_by_router=processing_router.is_redirect_stdout_disabled
|
||||||
StaticDividingLine(dividing_line_unit_part).get_full_static_line(
|
|
||||||
is_override=self._override_system_messages
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
stdout_result = self._capture_stdout(
|
|
||||||
lambda: processing_router.finds_appropriate_handler(input_command)
|
|
||||||
)
|
|
||||||
self._print_framed_text(stdout_result)
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine
|
print() # pre-prompt gap
|
||||||
DEFAULT_DIVIDING_LINE: StaticDividingLine = StaticDividingLine()
|
raw_command: str = self._autocompleter.prompt(self._renderer.render_prompt(self._prompt))
|
||||||
|
print() # post-prompt gap
|
||||||
|
|
||||||
DEFAULT_PRINT_FUNC: Printer = Console().print
|
try:
|
||||||
DEFAULT_AUTOCOMPLETER: AutoCompleter = AutoCompleter()
|
input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
|
||||||
DEFAULT_EXIT_COMMAND: Command = Command("q", description="Exit 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
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self._is_exit_command(input_command):
|
||||||
|
self._system_router.finds_appropriate_handler(input_command)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._process_exist_and_valid_command(input_command)
|
||||||
|
|
||||||
|
|
||||||
class App(BaseApp):
|
class App(BaseApp):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
prompt: str | HTML = ">>> ",
|
prompt: str = ">>> ",
|
||||||
initial_message: str = "Argenta\n",
|
initial_message: str = "Argenta",
|
||||||
farewell_message: str = "\nSee you\n",
|
farewell_message: str = "See you",
|
||||||
exit_command: Command = DEFAULT_EXIT_COMMAND,
|
exit_command: Command = Command("q", description="Exit command"),
|
||||||
system_router_title: str = "System points:",
|
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,
|
repeat_command_groups_printing: bool = False,
|
||||||
override_system_messages: bool = False,
|
override_system_messages: bool = False,
|
||||||
autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER,
|
autocompleter: AutoCompleter | None = None,
|
||||||
print_func: Printer = DEFAULT_PRINT_FUNC,
|
printer: Printer = Console().print,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Public. The essence of the application itself.
|
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 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 override_system_messages: whether to redefine the default formatting of system messages
|
||||||
:param autocompleter: the entity of the autocompleter
|
:param autocompleter: the entity of the autocompleter
|
||||||
:param print_func: system messages text output function
|
:param printer: system messages text output function
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -439,45 +215,9 @@ class App(BaseApp):
|
|||||||
dividing_line=dividing_line,
|
dividing_line=dividing_line,
|
||||||
repeat_command_groups_printing=repeat_command_groups_printing,
|
repeat_command_groups_printing=repeat_command_groups_printing,
|
||||||
override_system_messages=override_system_messages,
|
override_system_messages=override_system_messages,
|
||||||
autocompleter=autocompleter,
|
autocompleter=autocompleter or AutoCompleter(),
|
||||||
print_func=print_func,
|
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:
|
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
|
from argenta.response import Response
|
||||||
|
|
||||||
T = TypeVar("T", contravariant=True)
|
T = TypeVar("T", contravariant=True)
|
||||||
P = ParamSpec("P")
|
|
||||||
|
|
||||||
|
|
||||||
class NonStandardBehaviorHandler(Protocol[T]):
|
class NonStandardBehaviorHandler(Protocol[T]):
|
||||||
@@ -22,11 +29,16 @@ class Printer(Protocol):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class MostSimilarCommandGetter(Protocol):
|
||||||
|
def __call__(self, _unknown_trigger: str, /) -> str | None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class DescriptionMessageGenerator(Protocol):
|
class DescriptionMessageGenerator(Protocol):
|
||||||
def __call__(self, _command: str, _description: str, /) -> str:
|
def __call__(self, _command: str, _description: str, /) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class HandlerFunc(Protocol):
|
class HandlerFunc(Protocol):
|
||||||
def __call__(self, response: Response) -> None:
|
def __call__(self, response: Response, /, *args: Any, **kwargs: Any) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from argenta.command.flag import Flag as Flag
|
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 InputFlag as InputFlag
|
||||||
from argenta.command.flag import InputFlags as InputFlags
|
|
||||||
from argenta.command.flag import PossibleValues as PossibleValues
|
from argenta.command.flag import PossibleValues as PossibleValues
|
||||||
from argenta.command.flag.defaults import PredefinedFlags as PredefinedFlags
|
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 Command as Command
|
||||||
from argenta.command.models import InputCommand as InputCommand
|
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 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 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 PossibleValues as PossibleValues
|
||||||
from argenta.command.flag.models import ValidationStatus as ValidationStatus
|
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 enum import Enum
|
||||||
from re import Pattern
|
from re import Pattern
|
||||||
from typing import Literal, override
|
from typing import Any, Container, Generic, Iterator, Literal, TypeVar, override
|
||||||
|
|
||||||
PREFIX_TYPE = Literal["-", "--", "---"]
|
PREFIX_TYPE = Literal["-", "--", "---"]
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class Flag:
|
|||||||
name: str,
|
name: str,
|
||||||
*,
|
*,
|
||||||
prefix: PREFIX_TYPE = "--",
|
prefix: PREFIX_TYPE = "--",
|
||||||
possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
|
possible_values: Container[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Public. The entity of the flag being registered for subsequent processing
|
Public. The entity of the flag being registered for subsequent processing
|
||||||
@@ -35,7 +35,7 @@ class Flag:
|
|||||||
"""
|
"""
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.prefix: PREFIX_TYPE = prefix
|
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:
|
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
|
Public. The entity of the flag of the entered command
|
||||||
:param name: the name of the input flag
|
:param name: the name of the input flag
|
||||||
:param prefix: the prefix 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
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
@@ -122,3 +122,115 @@ class InputFlag:
|
|||||||
return self.name == other.name
|
return self.name == other.name
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
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"]
|
__all__ = ["Command", "InputCommand"]
|
||||||
|
|
||||||
import shlex
|
import shlex
|
||||||
from typing import Literal, Never, Self, cast
|
from typing import Iterable, Literal, Never, Self, cast
|
||||||
|
|
||||||
from argenta.command.exceptions import (
|
from argenta.command import Flags, InputFlags
|
||||||
EmptyInputCommandException,
|
from argenta.command.exceptions import (EmptyInputCommandException,
|
||||||
RepeatedInputFlagsException,
|
RepeatedInputFlagsException,
|
||||||
UnprocessedInputFlagException,
|
UnprocessedInputFlagException)
|
||||||
)
|
|
||||||
from argenta.command.flag.flags.models import Flags, InputFlags
|
|
||||||
from argenta.command.flag.models import Flag, InputFlag, ValidationStatus
|
from argenta.command.flag.models import Flag, InputFlag, ValidationStatus
|
||||||
|
|
||||||
ParseFlagsResult = tuple[InputFlags, str | None, str | None]
|
ParseFlagsResult = tuple[InputFlags, str | None, str | None]
|
||||||
@@ -16,10 +14,6 @@ ParseResult = tuple[str, InputFlags]
|
|||||||
|
|
||||||
MIN_FLAG_PREFIX: str = "-"
|
MIN_FLAG_PREFIX: str = "-"
|
||||||
PREFIX_TYPE = Literal["-", "--", "---"]
|
PREFIX_TYPE = Literal["-", "--", "---"]
|
||||||
DEFAULT_WITHOUT_FLAGS: Flags = Flags()
|
|
||||||
DEFAULT_WITHOUT_ALIASES: set[Never] = set()
|
|
||||||
|
|
||||||
DEFAULT_WITHOUT_INPUT_FLAGS: InputFlags = InputFlags()
|
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
class Command:
|
||||||
@@ -28,8 +22,8 @@ class Command:
|
|||||||
trigger: str,
|
trigger: str,
|
||||||
*,
|
*,
|
||||||
description: str = "Some useful command",
|
description: str = "Some useful command",
|
||||||
flags: Flag | Flags = DEFAULT_WITHOUT_FLAGS,
|
flags: Flag | Flags | None = None,
|
||||||
aliases: set[str] | set[Never] = DEFAULT_WITHOUT_ALIASES,
|
aliases: Iterable[str] | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Public. The command that can and should be registered in the Router
|
Public. The command that can and should be registered in the Router
|
||||||
@@ -38,11 +32,16 @@ class Command:
|
|||||||
:param flags: processed commands
|
:param flags: processed commands
|
||||||
:param aliases: string synonyms for the main trigger
|
: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.registered_flags: Flags = pretty_flags
|
||||||
self.trigger: str = trigger
|
self.trigger: str = trigger
|
||||||
self.description: str = description
|
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] = {
|
self._paired_string_entity_flag: dict[str, Flag] = {
|
||||||
flag.string_entity: flag for flag in pretty_flags
|
flag.string_entity: flag for flag in pretty_flags
|
||||||
@@ -68,7 +67,7 @@ class InputCommand:
|
|||||||
self,
|
self,
|
||||||
trigger: str,
|
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
|
Private. The model of the input command, after parsing
|
||||||
@@ -81,6 +80,8 @@ class InputCommand:
|
|||||||
input_flags
|
input_flags
|
||||||
if isinstance(input_flags, InputFlags)
|
if isinstance(input_flags, InputFlags)
|
||||||
else InputFlags([input_flags])
|
else InputFlags([input_flags])
|
||||||
|
if input_flags is not None
|
||||||
|
else InputFlags()
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -90,7 +91,14 @@ class InputCommand:
|
|||||||
:param raw_command: raw input command
|
:param raw_command: raw input command
|
||||||
:return: model of the input command, after parsing as InputCommand
|
: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:
|
if not tokens:
|
||||||
raise EmptyInputCommandException
|
raise EmptyInputCommandException
|
||||||
|
|||||||
@@ -39,4 +39,4 @@ class Orchestrator:
|
|||||||
)
|
)
|
||||||
setup_dishka(app, container, auto_inject=self._auto_inject_handlers)
|
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 dishka import Container
|
||||||
|
|
||||||
from argenta.command.flag.flags.models import InputFlags
|
from argenta.command import InputFlags
|
||||||
from argenta.response.status import ResponseStatus
|
from argenta.response.status import ResponseStatus
|
||||||
|
|
||||||
EMPTY_INPUT_FLAGS: InputFlags = InputFlags()
|
EMPTY_INPUT_FLAGS: InputFlags = InputFlags()
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ from typing import Callable
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from argenta.app.protocols import HandlerFunc
|
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 import ValidationStatus
|
||||||
from argenta.command.flag.flags import InputFlags
|
|
||||||
from argenta.response import Response, ResponseStatus
|
from argenta.response import Response, ResponseStatus
|
||||||
from argenta.router.command_handler.entity import CommandHandler, CommandHandlers
|
from argenta.router.command_handler.entity import CommandHandler, CommandHandlers
|
||||||
from argenta.router.exceptions import (RepeatedAliasNameException,
|
from argenta.router.exceptions import (RepeatedAliasNameException,
|
||||||
@@ -36,7 +35,7 @@ class Router:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.title: str = title
|
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.command_handlers: CommandHandlers = CommandHandlers()
|
||||||
self.aliases: set[str] = set()
|
self.aliases: set[str] = set()
|
||||||
@@ -57,7 +56,7 @@ class Router:
|
|||||||
self._update_routing_keys(redefined_command)
|
self._update_routing_keys(redefined_command)
|
||||||
|
|
||||||
def decorator(func: HandlerFunc) -> HandlerFunc:
|
def decorator(func: HandlerFunc) -> HandlerFunc:
|
||||||
_validate_func_args(func)
|
self._validate_func_args(func)
|
||||||
self.command_handlers.add_handler(CommandHandler(func, redefined_command))
|
self.command_handlers.add_handler(CommandHandler(func, redefined_command))
|
||||||
return func
|
return func
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ class Router:
|
|||||||
handle_command = command_handler.handled_command
|
handle_command = command_handler.handled_command
|
||||||
if handle_command.registered_flags.flags:
|
if handle_command.registered_flags.flags:
|
||||||
if input_command_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)
|
command_handler.handling(response)
|
||||||
else:
|
else:
|
||||||
response = Response(ResponseStatus.ALL_FLAGS_VALID)
|
response = Response(ResponseStatus.ALL_FLAGS_VALID)
|
||||||
@@ -134,8 +133,8 @@ class Router:
|
|||||||
response = Response(ResponseStatus.ALL_FLAGS_VALID)
|
response = Response(ResponseStatus.ALL_FLAGS_VALID)
|
||||||
command_handler.handling(response)
|
command_handler.handling(response)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response:
|
def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response:
|
||||||
"""
|
"""
|
||||||
Private. Validates flags of input command
|
Private. Validates flags of input command
|
||||||
:param handled_command: entity of the handled command
|
:param handled_command: entity of the handled command
|
||||||
@@ -159,8 +158,8 @@ def _structuring_input_flags(handled_command: Command, input_flags: InputFlags)
|
|||||||
|
|
||||||
return Response(status=status, input_flags=input_flags)
|
return Response(status=status, input_flags=input_flags)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def _validate_func_args(func: HandlerFunc) -> None:
|
def _validate_func_args(func: HandlerFunc) -> None:
|
||||||
"""
|
"""
|
||||||
Private. Validates the arguments of the handler
|
Private. Validates the arguments of the handler
|
||||||
:param func: entity of the handler func
|
:param func: entity of the handler func
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ from collections.abc import Iterator
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from argenta import App, Orchestrator, Router
|
from argenta import App, Orchestrator, Router
|
||||||
from argenta.command import Command, PredefinedFlags
|
from argenta.command import Command, PredefinedFlags, Flags
|
||||||
from argenta.command.flag.flags.models import Flags
|
|
||||||
from argenta.command.flag.models import ValidationStatus
|
from argenta.command.flag.models import ValidationStatus
|
||||||
from argenta.response import Response
|
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]
|
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
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.include_router(router)
|
||||||
app.set_empty_command_handler(lambda: print('Empty input command'))
|
app.set_empty_command_handler(lambda: print('Empty input command'))
|
||||||
orchestrator.start_polling(app)
|
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]
|
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
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.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}'))
|
||||||
orchestrator.start_polling(app)
|
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]
|
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
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.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}'))
|
||||||
orchestrator.start_polling(app)
|
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]
|
def test1(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
print('more command')
|
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.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}'))
|
||||||
orchestrator.start_polling(app)
|
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:
|
if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED:
|
||||||
print(f'test command with undefined flag: {undefined_flag.string_entity}')
|
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)
|
app.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
@@ -160,7 +159,7 @@ def test_unregistered_flag_with_value_is_accessible(monkeypatch: pytest.MonkeyPa
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
app = App(override_system_messages=True, print_func=print)
|
app = App(override_system_messages=True, printer=print)
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
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:
|
if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED:
|
||||||
print(f'connecting to host with flag: {undefined_flag.string_entity} {undefined_flag.input_value}')
|
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)
|
app.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
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]
|
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
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.include_router(router)
|
||||||
app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"'))
|
app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"'))
|
||||||
orchestrator.start_polling(app)
|
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]
|
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
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.include_router(router)
|
||||||
app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"'))
|
app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"'))
|
||||||
orchestrator.start_polling(app)
|
orchestrator.start_polling(app)
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ from collections.abc import Iterator
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from argenta import App, Orchestrator, Router
|
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 import Flag
|
||||||
from argenta.command.flag.flags import Flags
|
|
||||||
from argenta.command.flag.models import PossibleValues, ValidationStatus
|
from argenta.command.flag.models import PossibleValues, ValidationStatus
|
||||||
from argenta.response import Response
|
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]
|
def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
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.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
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]
|
def test2(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
print('some command')
|
print('some command')
|
||||||
|
|
||||||
app = App(override_system_messages=True, print_func=print)
|
app = App(override_system_messages=True, printer=print)
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
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]
|
def test2(_response: Response) -> None: # pyright: ignore[reportUnusedFunction]
|
||||||
print('more command')
|
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.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
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:
|
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||||
print(f'\nhelp for {valid_flag.name} flag\n')
|
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)
|
app.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
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:
|
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||||
print(f'flag value for {valid_flag.name} flag : {valid_flag.input_value}')
|
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)
|
app.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
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:
|
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||||
print(f'help for {valid_flag.name} flag')
|
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)
|
app.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
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:
|
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||||
print('info about test command')
|
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)
|
app.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
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:
|
if valid_flag and valid_flag.status == ValidationStatus.VALID:
|
||||||
print(f'connecting to host {valid_flag.input_value}')
|
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)
|
app.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
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):
|
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}')
|
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)
|
app.include_router(router)
|
||||||
orchestrator.start_polling(app)
|
orchestrator.start_polling(app)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import pytest
|
|||||||
from pytest import CaptureFixture
|
from pytest import CaptureFixture
|
||||||
|
|
||||||
from argenta.app import App
|
from argenta.app import App
|
||||||
from argenta.app.dividing_line import DynamicDividingLine, StaticDividingLine
|
|
||||||
from argenta.app.protocols import DescriptionMessageGenerator, NonStandardBehaviorHandler
|
from argenta.app.protocols import DescriptionMessageGenerator, NonStandardBehaviorHandler
|
||||||
from argenta.command.models import Command, InputCommand
|
from argenta.command.models import Command, InputCommand
|
||||||
from argenta.response import Response
|
from argenta.response import Response
|
||||||
@@ -18,26 +17,31 @@ from argenta.router import Router
|
|||||||
|
|
||||||
def test_default_exit_command_lowercase_q_is_recognized() -> None:
|
def test_default_exit_command_lowercase_q_is_recognized() -> None:
|
||||||
app = App()
|
app = App()
|
||||||
|
app._setup_system_router()
|
||||||
assert app._is_exit_command(InputCommand('q')) is True
|
assert app._is_exit_command(InputCommand('q')) is True
|
||||||
|
|
||||||
|
|
||||||
def test_default_exit_command_uppercase_q_is_recognized() -> None:
|
def test_default_exit_command_uppercase_q_is_recognized() -> None:
|
||||||
app = App()
|
app = App()
|
||||||
|
app._setup_system_router()
|
||||||
assert app._is_exit_command(InputCommand('Q')) is True
|
assert app._is_exit_command(InputCommand('Q')) is True
|
||||||
|
|
||||||
|
|
||||||
def test_custom_exit_command_is_recognized() -> None:
|
def test_custom_exit_command_is_recognized() -> None:
|
||||||
app = App(exit_command=Command('quit'))
|
app = App(exit_command=Command('quit'))
|
||||||
|
app._setup_system_router()
|
||||||
assert app._is_exit_command(InputCommand('quit')) is True
|
assert app._is_exit_command(InputCommand('quit')) is True
|
||||||
|
|
||||||
|
|
||||||
def test_exit_command_alias_is_recognized() -> None:
|
def test_exit_command_alias_is_recognized() -> None:
|
||||||
app = App(exit_command=Command('q', aliases={'exit'}))
|
app = App(exit_command=Command('q', aliases={'exit'}))
|
||||||
|
app._setup_system_router()
|
||||||
assert app._is_exit_command(InputCommand('exit')) is True
|
assert app._is_exit_command(InputCommand('exit')) is True
|
||||||
|
|
||||||
|
|
||||||
def test_non_exit_command_is_not_recognized() -> None:
|
def test_non_exit_command_is_not_recognized() -> None:
|
||||||
app = App(exit_command=Command('q', aliases={'exit'}))
|
app = App(exit_command=Command('q', aliases={'exit'}))
|
||||||
|
app._setup_system_router()
|
||||||
assert app._is_exit_command(InputCommand('quit')) is False
|
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.include_routers(router)
|
||||||
app._pre_cycle_setup()
|
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:
|
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.include_routers(router)
|
||||||
app._pre_cycle_setup()
|
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
|
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
|
# 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
|
descr_gen: DescriptionMessageGenerator = lambda command, description: command + '-+-' + description
|
||||||
app.set_description_message_pattern(descr_gen)
|
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:
|
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
|
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
|
# Tests for command processing
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -672,11 +618,17 @@ def test_app_handlers_work_with_multiple_routers() -> None:
|
|||||||
|
|
||||||
app.set_unknown_command_handler(custom_handler)
|
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('cmd1'))
|
||||||
assert not app._is_unknown_command(InputCommand('cmd2'))
|
assert not app._is_unknown_command(InputCommand('cmd2'))
|
||||||
|
|
||||||
# Unknown command should trigger handler
|
|
||||||
assert app._is_unknown_command(InputCommand('unknown'))
|
assert app._is_unknown_command(InputCommand('unknown'))
|
||||||
app._unknown_command_handler(InputCommand('unknown'))
|
app._unknown_command_handler(InputCommand('unknown'))
|
||||||
assert call_tracker['called']
|
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'))
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from typing import Any, Callable
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from prompt_toolkit import HTML
|
||||||
|
from prompt_toolkit.completion import CompleteEvent
|
||||||
from prompt_toolkit.document import Document
|
from prompt_toolkit.document import Document
|
||||||
from prompt_toolkit.history import InMemoryHistory
|
from prompt_toolkit.history import InMemoryHistory
|
||||||
|
|
||||||
@@ -75,7 +83,7 @@ def test_history_completer_returns_matching_commands() -> None:
|
|||||||
completer = HistoryCompleter(history, {"status"})
|
completer = HistoryCompleter(history, {"status"})
|
||||||
doc = Document("sta")
|
doc = Document("sta")
|
||||||
|
|
||||||
completions = list(completer.get_completions(doc, None))
|
completions = list(completer.get_completions(doc, CompleteEvent()))
|
||||||
completion_texts = [c.text for c in completions]
|
completion_texts = [c.text for c in completions]
|
||||||
|
|
||||||
assert "start server" in completion_texts
|
assert "start server" in completion_texts
|
||||||
@@ -91,7 +99,7 @@ def test_history_completer_returns_all_when_empty_input() -> None:
|
|||||||
completer = HistoryCompleter(history, {"status"})
|
completer = HistoryCompleter(history, {"status"})
|
||||||
doc = Document("")
|
doc = Document("")
|
||||||
|
|
||||||
completions = list(completer.get_completions(doc, None))
|
completions = list(completer.get_completions(doc, CompleteEvent()))
|
||||||
completion_texts = [c.text for c in completions]
|
completion_texts = [c.text for c in completions]
|
||||||
|
|
||||||
assert len(completion_texts) == 3
|
assert len(completion_texts) == 3
|
||||||
@@ -107,7 +115,7 @@ def test_history_completer_returns_empty_when_no_matches() -> None:
|
|||||||
completer = HistoryCompleter(history, {"stop"})
|
completer = HistoryCompleter(history, {"stop"})
|
||||||
doc = Document("xyz")
|
doc = Document("xyz")
|
||||||
|
|
||||||
completions = list(completer.get_completions(doc, None))
|
completions = list(completer.get_completions(doc, CompleteEvent()))
|
||||||
assert len(completions) == 0
|
assert len(completions) == 0
|
||||||
|
|
||||||
|
|
||||||
@@ -119,7 +127,7 @@ def test_history_completer_deduplicates_commands() -> None:
|
|||||||
completer = HistoryCompleter(history, {"start"})
|
completer = HistoryCompleter(history, {"start"})
|
||||||
doc = Document("sta")
|
doc = Document("sta")
|
||||||
|
|
||||||
completions = list(completer.get_completions(doc, None))
|
completions = list(completer.get_completions(doc, CompleteEvent()))
|
||||||
assert len(completions) == 1
|
assert len(completions) == 1
|
||||||
|
|
||||||
|
|
||||||
@@ -132,7 +140,7 @@ def test_history_completer_sorts_results() -> None:
|
|||||||
completer = HistoryCompleter(history, set())
|
completer = HistoryCompleter(history, set())
|
||||||
doc = Document("st")
|
doc = Document("st")
|
||||||
|
|
||||||
completions = list(completer.get_completions(doc, None))
|
completions = list(completer.get_completions(doc, CompleteEvent()))
|
||||||
completion_texts = [c.text for c in completions]
|
completion_texts = [c.text for c in completions]
|
||||||
|
|
||||||
assert completion_texts == ["start", "status", "stop"]
|
assert completion_texts == ["start", "status", "stop"]
|
||||||
@@ -160,3 +168,311 @@ def test_find_common_prefix_with_empty_list() -> None:
|
|||||||
matches: list[str] = []
|
matches: list[str] = []
|
||||||
prefix = HistoryCompleter._find_common_prefix(matches)
|
prefix = HistoryCompleter._find_common_prefix(matches)
|
||||||
assert prefix == ""
|
assert prefix == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_lexer_handles_out_of_range_lineno() -> None:
|
||||||
|
lexer = CommandLexer({"start", "stop"})
|
||||||
|
doc = Document("start")
|
||||||
|
get_line_tokens = lexer.lex_document(doc)
|
||||||
|
tokens = get_line_tokens(1)
|
||||||
|
assert tokens == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_history_completer_returns_early_when_no_matches() -> None:
|
||||||
|
history = InMemoryHistory()
|
||||||
|
completer = HistoryCompleter(history, {"start", "stop"})
|
||||||
|
doc = Document("xyz")
|
||||||
|
|
||||||
|
result = completer.get_completions(doc, CompleteEvent())
|
||||||
|
completions = list(result)
|
||||||
|
assert completions == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_with_commands() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
|
||||||
|
completer.initial_setup({"start", "stop", "status"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
assert completer._fallback_mode is False
|
||||||
|
mock_session.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_with_history_file() -> None:
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
|
||||||
|
history_file = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
completer = AutoCompleter(history_filename=history_file)
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession'), \
|
||||||
|
patch('argenta.app.autocompleter.entity.ThreadedHistory') as mock_threaded_history:
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
assert completer._fallback_mode is False
|
||||||
|
mock_threaded_history.assert_called_once()
|
||||||
|
finally:
|
||||||
|
if os.path.exists(history_file):
|
||||||
|
os.unlink(history_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_without_history_file() -> None:
|
||||||
|
completer = AutoCompleter(history_filename=None)
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession'), \
|
||||||
|
patch('argenta.app.autocompleter.entity.InMemoryHistory') as mock_in_memory:
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
assert completer._fallback_mode is False
|
||||||
|
mock_in_memory.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_with_custom_autocomplete_button() -> None:
|
||||||
|
completer = AutoCompleter(autocomplete_button="c-space")
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession'):
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
assert completer.autocomplete_button == "c-space"
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_without_auto_suggestions() -> None:
|
||||||
|
completer = AutoCompleter(auto_suggestions=False)
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
call_kwargs = mock_session.call_args[1]
|
||||||
|
assert call_kwargs['auto_suggest'] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_initial_setup_without_command_highlighting() -> None:
|
||||||
|
completer = AutoCompleter(command_highlighting=False)
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession') as mock_session:
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._session is not None
|
||||||
|
call_kwargs = mock_session.call_args[1]
|
||||||
|
assert call_kwargs['style'] is None
|
||||||
|
assert call_kwargs['lexer'] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_key_binding_handler_with_complete_state() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
captured_handler: Callable[[Any], None] | None = None
|
||||||
|
|
||||||
|
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
|
||||||
|
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
|
||||||
|
nonlocal captured_handler
|
||||||
|
captured_handler = func
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession'), \
|
||||||
|
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
|
||||||
|
|
||||||
|
mock_kb = MagicMock()
|
||||||
|
mock_kb.add = capture_kb_add
|
||||||
|
mock_kb_class.return_value = mock_kb
|
||||||
|
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert captured_handler is not None
|
||||||
|
|
||||||
|
mock_event = MagicMock()
|
||||||
|
mock_buff = MagicMock()
|
||||||
|
mock_buff.complete_state = True
|
||||||
|
mock_event.app.current_buffer = mock_buff
|
||||||
|
|
||||||
|
captured_handler(mock_event)
|
||||||
|
|
||||||
|
mock_buff.complete_next.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_key_binding_handler_no_completions() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
captured_handler: Callable[[Any], None] | None = None
|
||||||
|
|
||||||
|
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
|
||||||
|
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
|
||||||
|
nonlocal captured_handler
|
||||||
|
captured_handler = func
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession'), \
|
||||||
|
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
|
||||||
|
|
||||||
|
mock_kb = MagicMock()
|
||||||
|
mock_kb.add = capture_kb_add
|
||||||
|
mock_kb_class.return_value = mock_kb
|
||||||
|
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
mock_event = MagicMock()
|
||||||
|
mock_buff = MagicMock()
|
||||||
|
mock_buff.complete_state = False
|
||||||
|
mock_completer = MagicMock()
|
||||||
|
mock_completer.get_completions.return_value = iter([])
|
||||||
|
mock_buff.completer = mock_completer
|
||||||
|
mock_event.app.current_buffer = mock_buff
|
||||||
|
|
||||||
|
assert captured_handler is not None
|
||||||
|
captured_handler(mock_event)
|
||||||
|
|
||||||
|
mock_buff.start_completion.assert_not_called()
|
||||||
|
mock_buff.apply_completion.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_key_binding_handler_single_completion() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
captured_handler: Callable[[Any], None] | None = None
|
||||||
|
|
||||||
|
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
|
||||||
|
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
|
||||||
|
nonlocal captured_handler
|
||||||
|
captured_handler = func
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession'), \
|
||||||
|
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
|
||||||
|
|
||||||
|
mock_kb = MagicMock()
|
||||||
|
mock_kb.add = capture_kb_add
|
||||||
|
mock_kb_class.return_value = mock_kb
|
||||||
|
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
mock_event = MagicMock()
|
||||||
|
mock_buff = MagicMock()
|
||||||
|
mock_buff.complete_state = False
|
||||||
|
mock_completion = MagicMock()
|
||||||
|
mock_completer = MagicMock()
|
||||||
|
mock_completer.get_completions.return_value = iter([mock_completion])
|
||||||
|
mock_buff.completer = mock_completer
|
||||||
|
mock_event.app.current_buffer = mock_buff
|
||||||
|
|
||||||
|
assert captured_handler is not None
|
||||||
|
captured_handler(mock_event)
|
||||||
|
|
||||||
|
mock_buff.apply_completion.assert_called_once_with(mock_completion)
|
||||||
|
mock_buff.start_completion.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_key_binding_handler_multiple_completions() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
captured_handler: Callable[[Any], None] | None = None
|
||||||
|
|
||||||
|
def capture_kb_add(key: str) -> Callable[[Callable[[Any], None]], Callable[[Any], None]]:
|
||||||
|
def decorator(func: Callable[[Any], None]) -> Callable[[Any], None]:
|
||||||
|
nonlocal captured_handler
|
||||||
|
captured_handler = func
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=True), \
|
||||||
|
patch('argenta.app.autocompleter.entity.PromptSession'), \
|
||||||
|
patch('argenta.app.autocompleter.entity.KeyBindings') as mock_kb_class:
|
||||||
|
|
||||||
|
mock_kb = MagicMock()
|
||||||
|
mock_kb.add = capture_kb_add
|
||||||
|
mock_kb_class.return_value = mock_kb
|
||||||
|
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
mock_event = MagicMock()
|
||||||
|
mock_buff = MagicMock()
|
||||||
|
mock_buff.complete_state = False
|
||||||
|
mock_completion1 = MagicMock()
|
||||||
|
mock_completion2 = MagicMock()
|
||||||
|
mock_completer = MagicMock()
|
||||||
|
mock_completer.get_completions.return_value = iter([mock_completion1, mock_completion2])
|
||||||
|
mock_buff.completer = mock_completer
|
||||||
|
mock_event.app.current_buffer = mock_buff
|
||||||
|
|
||||||
|
assert captured_handler is not None
|
||||||
|
captured_handler(mock_event)
|
||||||
|
|
||||||
|
mock_buff.start_completion.assert_called_once_with(select_first=False)
|
||||||
|
mock_buff.apply_completion.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_prompt_in_fallback_mode_with_string() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=False):
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._fallback_mode is True
|
||||||
|
|
||||||
|
with patch('builtins.input', return_value='test input'):
|
||||||
|
result = completer.prompt(">>> ")
|
||||||
|
|
||||||
|
assert result == 'test input'
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_prompt_in_fallback_mode_with_html() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
with patch.object(sys.stdin, 'isatty', return_value=False):
|
||||||
|
completer.initial_setup({"start", "stop"})
|
||||||
|
|
||||||
|
assert completer._fallback_mode is True
|
||||||
|
|
||||||
|
with patch('builtins.input', return_value='test input'):
|
||||||
|
result = completer.prompt(HTML("<b>>>> </b>"))
|
||||||
|
|
||||||
|
assert result == 'test input'
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_prompt_with_html_in_normal_mode() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
mock_session.prompt.return_value = 'test result'
|
||||||
|
completer._session = mock_session
|
||||||
|
completer._fallback_mode = False
|
||||||
|
|
||||||
|
html_prompt = HTML("<b>>>> </b>")
|
||||||
|
result = completer.prompt(html_prompt)
|
||||||
|
|
||||||
|
assert result == 'test result'
|
||||||
|
mock_session.prompt.assert_called_once()
|
||||||
|
call_args = mock_session.prompt.call_args
|
||||||
|
assert call_args[0][0] == html_prompt
|
||||||
|
|
||||||
|
|
||||||
|
def test_autocompleter_prompt_with_string_in_normal_mode() -> None:
|
||||||
|
completer = AutoCompleter()
|
||||||
|
|
||||||
|
mock_session = MagicMock()
|
||||||
|
mock_session.prompt.return_value = 'test result'
|
||||||
|
completer._session = mock_session
|
||||||
|
completer._fallback_mode = False
|
||||||
|
|
||||||
|
result = completer.prompt(">>> ")
|
||||||
|
|
||||||
|
assert result == 'test result'
|
||||||
|
mock_session.prompt.assert_called_once()
|
||||||
|
call_args = mock_session.prompt.call_args
|
||||||
|
assert isinstance(call_args[0][0], HTML)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user