From 60d7844946ede64c898c89577295ec2cb515952d Mon Sep 17 00:00:00 2001 From: kolo Date: Fri, 7 Feb 2025 00:56:53 +0300 Subject: [PATCH] first step to creating dream lib --- .gitignore | 5 ++ README.md | 2 +- argenta/__init__.py | 2 + argenta/app/__init__.py | 0 argenta/app/entity.py | 145 +++++++++++++++++++++++++++++++++++ argenta/app/exceptions.py | 39 ++++++++++ argenta/router/__init__.py | 0 argenta/router/entity.py | 70 +++++++++++++++++ argenta/router/exceptions.py | 13 ++++ poetry.lock | 7 ++ pyproject.toml | 17 ++++ setup.py | 23 ++++++ tests/__init__.py | 0 tests/main_test.py | 5 ++ 14 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 argenta/__init__.py create mode 100644 argenta/app/__init__.py create mode 100644 argenta/app/entity.py create mode 100644 argenta/app/exceptions.py create mode 100644 argenta/router/__init__.py create mode 100644 argenta/router/entity.py create mode 100644 argenta/router/exceptions.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/main_test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3be1274 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.venv +.idea +argenta/router/__pycache__/ +argenta/app/__pycache__/ +argenta/__pycache__/ \ No newline at end of file diff --git a/README.md b/README.md index d5b0880..aa7d49b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# Argon +# Argenta python library for creating cli apps diff --git a/argenta/__init__.py b/argenta/__init__.py new file mode 100644 index 0000000..88477f4 --- /dev/null +++ b/argenta/__init__.py @@ -0,0 +1,2 @@ +from .router import * +from .app import * \ No newline at end of file diff --git a/argenta/app/__init__.py b/argenta/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/argenta/app/entity.py b/argenta/app/entity.py new file mode 100644 index 0000000..d740596 --- /dev/null +++ b/argenta/app/entity.py @@ -0,0 +1,145 @@ +from typing import Callable +from ..router.entity import Router +from .exceptions import (InvalidRouterInstanceException, + InvalidDescriptionMessagePatternException, + OnlyOneMainRouterIsAllowedException, + MissingMainRouterException, + MissingHandlersForUnknownCommandsOnMainRouterException, + HandlerForUnknownCommandsCanOnlyBeDeclaredForMainRouterException) + + +class App: + def __init__(self, + prompt: str = 'Enter a command', + exit_command: str = 'q', + ignore_exit_command_register: bool = True, + initial_greeting: str = 'Hello', + goodbye_message: str = 'GoodBye', + line_separate: str = '\n', + command_group_description_separate: str = '\n', + print_func: Callable[[str], None] = print) -> None: + self.prompt = prompt + self.print_func = print_func + self.exit_command = exit_command + self.ignore_exit_command_register = ignore_exit_command_register + self.goodbye_message = goodbye_message + self.initial_greeting = initial_greeting + self.line_separate = line_separate + self.command_group_description_separate = command_group_description_separate + + self.routers: list[Router] = [] + self.registered_commands: list[dict[str, str | list[dict[str, Callable[[], None] | str]] | Router]] = [] + self.main_app_router: Router | None = None + self._description_message_pattern = '[{command}] *=*=* {description}' + + + def start_polling(self) -> None: + self.print_func(self.initial_greeting) + self.validate_main_router() + + while True: + self.print_command_group_description() + self.print_func(self.prompt) + + command: str = input() + + self.checking_command_for_exit_command(command) + self.print_func(self.line_separate) + + is_unknown_command: bool = self.check_is_command_unknown(command) + + if is_unknown_command: + continue + + for router in self.routers: + router.input_command_handler(command) + self.print_func(self.line_separate) + self.print_func(self.command_group_description_separate) + + + def set_initial_greeting(self, greeting: str) -> None: + self.initial_greeting = greeting + + + def set_goodbye_message(self, message: str) -> None: + self.goodbye_message = message + + + def set_description_message_pattern(self, pattern: str) -> None: + try: + pattern.format(command='command', + description='description') + except KeyError: + raise InvalidDescriptionMessagePatternException(pattern) + self._description_message_pattern = pattern + + + def validate_main_router(self): + if not self.main_app_router: + raise MissingMainRouterException() + + if not self.main_app_router.unknown_command_func: + raise MissingHandlersForUnknownCommandsOnMainRouterException() + + for router in self.routers: + if router.unknown_command_func and self.main_app_router is not router: + raise HandlerForUnknownCommandsCanOnlyBeDeclaredForMainRouterException() + + + def checking_command_for_exit_command(self, command: str): + if command.lower() == self.exit_command.lower(): + if self.ignore_exit_command_register: + self.print_func(self.goodbye_message) + exit(0) + else: + if command == self.exit_command: + self.print_func(self.goodbye_message) + exit(0) + + + def check_is_command_unknown(self, command: str): + registered_commands = self.registered_commands + for router in registered_commands: + for command_entity in router['commands']: + if command_entity['command'].lower() == command.lower(): + if router['router'].ignore_command_register: + return False + else: + if command_entity['command'] == command: + return False + self.main_app_router.unknown_command_handler(command) + self.print_func(self.line_separate) + self.print_func(self.command_group_description_separate) + return True + + + def print_command_group_description(self): + for router in self.registered_commands: + self.print_func(router['name']) + for command_entity in router['commands']: + self.print_func(self._description_message_pattern.format( + command=command_entity['command'], + description=command_entity['description'] + ) + ) + self.print_func(self.command_group_description_separate) + + + def include_router(self, router: Router, is_main: bool = False) -> None: + if not isinstance(router, Router): + raise InvalidRouterInstanceException() + + if is_main: + if not self.main_app_router: + self.main_app_router = router + router.set_router_as_main() + else: + raise OnlyOneMainRouterIsAllowedException(router) + + self.routers.append(router) + + registered_commands: list[dict[str, Callable[[], None] | str]] = router.get_registered_commands() + self.registered_commands.append({'name': router.get_name(), + 'router': router, + 'commands': registered_commands}) + diff --git a/argenta/app/exceptions.py b/argenta/app/exceptions.py new file mode 100644 index 0000000..d69184a --- /dev/null +++ b/argenta/app/exceptions.py @@ -0,0 +1,39 @@ +class InvalidRouterInstanceException(Exception): + def __str__(self): + return "Invalid Router Instance" + + +class InvalidDescriptionMessagePatternException(Exception): + def __init__(self, pattern: str): + self.pattern = pattern + def __str__(self): + return ("Invalid Description Message Pattern\n" + "Correct pattern example: [{command}] *=*=* {description}\n" + "The pattern must contain two variables: `command` and `description` - description of the command\n" + f"Your pattern: {self.pattern}") + + +class OnlyOneMainRouterIsAllowedException(Exception): + def __init__(self, existing_main_router): + self.existing_main_router = existing_main_router + + def __str__(self): + return ("Only One Main Router Allowed\n" + f"Existing main router is: {self.existing_main_router}") + + +class MissingMainRouterException(Exception): + def __str__(self): + return ("Missing Main Router\n" + "One of the registered routers must be the main one") + + +class MissingHandlersForUnknownCommandsOnMainRouterException(Exception): + def __str__(self): + return ("Missing Handlers For Unknown Commands On The Main Router\n" + "The main router must have a declared handler for unknown commands") + + +class HandlerForUnknownCommandsCanOnlyBeDeclaredForMainRouterException(Exception): + def __str__(self): + return '\nThe handler for unknown commands can only be declared for the main router' diff --git a/argenta/router/__init__.py b/argenta/router/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/argenta/router/entity.py b/argenta/router/entity.py new file mode 100644 index 0000000..ff81e9e --- /dev/null +++ b/argenta/router/entity.py @@ -0,0 +1,70 @@ +from typing import Callable, Any +from src.core.router.exceptions import (InvalidCommandInstanceException, + UnknownCommandHandlerHasAlreadyBeenCreatedException, + InvalidDescriptionInstanceException) + + +class Router: + def __init__(self, + name: str, + ignore_command_register: bool = False): + + self.ignore_command_register: bool = ignore_command_register + self._name = name + + self.processed_commands: list[dict[str, Callable[[], None] | str]] = [] + self.unknown_command_func: Callable[[str], None] | None = None + self._is_main_router: bool = False + + + def command(self, command: str, description: str) -> Callable[[Any], Any]: + if not isinstance(command, str): + raise InvalidCommandInstanceException() + if not isinstance(description, str): + raise InvalidDescriptionInstanceException() + else: + def command_decorator(func): + self.processed_commands.append({'func': func, + 'command': command, + 'description': description}) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + return command_decorator + + + def unknown_command(self, func): + if self.unknown_command_func is not None: + raise UnknownCommandHandlerHasAlreadyBeenCreatedException() + + self.unknown_command_func = func + + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + + + def input_command_handler(self, input_command): + for command_entity in self.processed_commands: + if input_command.lower() == command_entity['command'].lower(): + if self.ignore_command_register: + return command_entity['func']() + else: + if input_command == command_entity['command']: + return command_entity['func']() + + def unknown_command_handler(self, unknown_command): + self.unknown_command_func(unknown_command) + + + def set_router_as_main(self): + self._is_main_router = True + + + def get_registered_commands(self) -> list[dict[str, Callable[[], None] | str]]: + return self.processed_commands + + + def get_name(self) -> str: + return self._name + diff --git a/argenta/router/exceptions.py b/argenta/router/exceptions.py new file mode 100644 index 0000000..b8781cb --- /dev/null +++ b/argenta/router/exceptions.py @@ -0,0 +1,13 @@ +class InvalidCommandInstanceException(Exception): + def __str__(self): + return "Invalid Command Instance" + + +class InvalidDescriptionInstanceException(Exception): + def __str__(self): + return "Invalid Description Instance" + + +class UnknownCommandHandlerHasAlreadyBeenCreatedException(Exception): + def __str__(self): + return "Only one unknown command handler can be declared" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..db9aac4 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +package = [] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.11" +content-hash = "f5666f5625d676c506924a57dc0520a1f3ed2b2c774baed3dc85353594f8473d" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..823a95c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "argenta" +version = "0.1.0" +description = "python library for creating cli apps" +authors = [ + {name = "kolo",email = "kolo.is.main@gmail.com"} +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9673553 --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setup( + name="argenta", + version="0.1.0", + author="kolo", + author_email="kolo.is.main@gmail.com", + description="python library for creating cli apps", + long_description=long_description, + long_description_content_type="text/markdown", + packages=find_packages(), + install_requires=[ + "requests", + ], + classifiers=[ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + ], + python_requires='>=3.11', +) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/main_test.py b/tests/main_test.py new file mode 100644 index 0000000..b574760 --- /dev/null +++ b/tests/main_test.py @@ -0,0 +1,5 @@ +from argenta.app.entity import * + + +def test(): + assert App().exit_command == 'q'