__all__ = ["Router"] from inspect import get_annotations, getfullargspec, getsourcefile, getsourcelines from typing import Callable from rich.console import Console from argenta.app.protocols import HandlerFunc from argenta.command import Command, InputCommand, InputFlags from argenta.command.flag import ValidationStatus from argenta.response import Response, ResponseStatus from argenta.router.command_handler.entity import CommandHandler, CommandHandlers from argenta.router.exceptions import (RepeatedAliasNameException, RepeatedFlagNameException, RepeatedTriggerNameException, RequiredArgumentNotPassedException, TriggerContainSpacesException) class Router: def __init__( self, title: str = "Default title", *, disable_redirect_stdout: bool = False, ): """ Public. Directly configures and manages handlers :param title: the title of the router, displayed when displaying the available commands :param disable_redirect_stdout: Disables stdout forwarding, if the argument value is True, the StaticDividingLine will be forced to be used as a line separator for this router, disabled forwarding is needed when there is text output in conjunction with a text input request (for example, input()), if the argument value is True, the output of the input() prompt is intercepted and not displayed, which is ambiguous behavior and can lead to unexpected work :return: None """ self.title: str = title self.is_redirect_stdout_disabled: bool = disable_redirect_stdout self.command_handlers: CommandHandlers = CommandHandlers() self.aliases: set[str] = set() self.triggers: set[str] = set() def command(self, command: Command | str) -> Callable[[HandlerFunc], HandlerFunc]: """ Public. Registers handler :param command: Registered command :return: decorated handler as Callable """ if isinstance(command, str): redefined_command = Command(command) else: redefined_command = command self._validate_command(redefined_command) self._update_routing_keys(redefined_command) def decorator(func: HandlerFunc) -> HandlerFunc: self._validate_func_args(func) self.command_handlers.add_handler(CommandHandler(func, redefined_command)) return func return decorator def _validate_command(self, command: Command) -> None: """ Private. Validates the command registered in handler :param command: validated command :return: None if command is valid else raise exception """ command_name: str = command.trigger if command_name.find(" ") != -1: raise TriggerContainSpacesException() if command_name.lower() in self.triggers: raise RepeatedTriggerNameException() if command_name.lower() in self.aliases: raise RepeatedAliasNameException({command_name.lower()}) if overlapping := (self.aliases | self.triggers) & set(map(lambda x: x.lower(), command.aliases)): raise RepeatedAliasNameException(overlapping) flags_name: list[str] = [flag.string_entity.lower() for flag in command.registered_flags] if len(set(flags_name)) < len(flags_name): raise RepeatedFlagNameException() def _update_routing_keys(self, registered_command: Command) -> None: redefined_command_aliases_in_lower = set(map(lambda x: x.lower(), registered_command.aliases)) self.aliases.update(redefined_command_aliases_in_lower) self.triggers.add(registered_command.trigger.lower()) def finds_appropriate_handler(self, input_command: InputCommand) -> None: """ Private. Finds the appropriate handler for given input command and passes control to it :param input_command: input command as InputCommand :return: None """ input_command_name: str = input_command.trigger.lower() input_command_flags: InputFlags = input_command.input_flags command_handler = self.command_handlers.get_command_handler_by_trigger(input_command_name) if not command_handler: raise RuntimeError(f"Handler for '{input_command.trigger}' command not found. Panic!") else: self.process_input_command(input_command_flags, command_handler) def process_input_command(self, input_command_flags: InputFlags, command_handler: CommandHandler) -> None: """ Private. Processes input command with the appropriate handler :param input_command_flags: input command flags as InputFlags :param command_handler: command handler for input command as CommandHandler :return: None """ handle_command = command_handler.handled_command if handle_command.registered_flags.flags: if input_command_flags.flags: response: Response = self._structuring_input_flags(handle_command, input_command_flags) command_handler.handling(response) else: response = Response(ResponseStatus.ALL_FLAGS_VALID) command_handler.handling(response) else: if input_command_flags.flags: undefined_flags = InputFlags() for input_flag in input_command_flags: input_flag.status = ValidationStatus.UNDEFINED undefined_flags.add_flag(input_flag) response = Response(ResponseStatus.UNDEFINED_FLAGS, input_flags=undefined_flags) command_handler.handling(response) else: response = Response(ResponseStatus.ALL_FLAGS_VALID) command_handler.handling(response) @staticmethod def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response: """ Private. Validates flags of input command :param handled_command: entity of the handled command :param input_flags: :return: entity of response as Response """ invalid_value_flags, undefined_flags = False, False for flag in input_flags: flag_status: ValidationStatus = handled_command.validate_input_flag(flag) flag.status = flag_status if flag_status == ValidationStatus.INVALID: invalid_value_flags = True elif flag_status == ValidationStatus.UNDEFINED: undefined_flags = True status = ResponseStatus.from_flags( has_invalid_value_flags=invalid_value_flags, has_undefined_flags=undefined_flags ) return Response(status=status, input_flags=input_flags) @staticmethod def _validate_func_args(func: HandlerFunc) -> None: """ Private. Validates the arguments of the handler :param func: entity of the handler func :return: None if func is valid else raise exception """ transferred_args = getfullargspec(func).args if len(transferred_args) == 0: raise RequiredArgumentNotPassedException() response_arg: str = transferred_args[0] func_annotations: dict[str, None] = get_annotations(func) response_arg_annotation = func_annotations.get(response_arg) if response_arg_annotation is not None and response_arg_annotation is not Response: source_line: int = getsourcelines(func)[1] Console().print( f'\nFile "{getsourcefile(func)}", line {source_line}\n[b red]WARNING:[/b red] [i]The typehint ' + f"of argument([green]{response_arg}[/green]) passed to the handler must be [/i][bold blue]{Response}[/bold blue]," + f" [i]but[/i] [bold blue]{response_arg_annotation}[/bold blue] [i]is specified[/i]", highlight=False, )