From 14105acb7cfbc541be3c36f3f3df560b6df370e2 Mon Sep 17 00:00:00 2001 From: blnkoff Date: Wed, 5 Jun 2024 19:28:29 +0300 Subject: [PATCH 1/2] The release of 0.2.0 version. Fixing issue `there are many options to show them instantly.` Now available on UNIX systems and Windows. Previous version was only available on UNIX --- croco_cli/cli/change.py | 7 ++- croco_cli/cli/init.py | 15 ++++-- croco_cli/cli/install.py | 4 +- croco_cli/cli/make.py | 4 +- croco_cli/cli/reset.py | 4 +- croco_cli/cli/set.py | 12 ++++- croco_cli/cli/user.py | 6 ++- croco_cli/database.py | 17 +++++-- croco_cli/globals.py | 1 - croco_cli/utils.py | 98 ++++++++++++++++++---------------------- pyproject.toml | 5 +- 11 files changed, 98 insertions(+), 75 deletions(-) diff --git a/croco_cli/cli/change.py b/croco_cli/cli/change.py index 6da7e69..66256cb 100644 --- a/croco_cli/cli/change.py +++ b/croco_cli/cli/change.py @@ -1,5 +1,5 @@ import click -from croco_cli.database import database +from croco_cli.database import Database from croco_cli.types import Option, Wallet, CustomAccount from croco_cli.utils import show_key_mode, sort_wallets, echo_error, make_screen_option @@ -12,6 +12,7 @@ def change(): def _make_wallet_option(wallet: Wallet) -> Option: """Create a new wallet option for keyboard interactive mode""" label = wallet['label'] + database = Database() def _handler(): database.set_wallet(wallet['private_key'], label) @@ -35,6 +36,7 @@ def _deleting_handler(): def _make_custom_option(account: CustomAccount) -> Option: """Create a new custom account option for keyboard interactive mode""" label = account["email"] + database = Database() def _handler(): account.pop('current') @@ -59,6 +61,8 @@ def _deleting_handler(): def _wallet(): """Change the current wallet for unit tests""" wallets = [] + database = Database() + if database.wallets.table_exists(): wallets = database.get_wallets() @@ -76,6 +80,7 @@ def _wallet(): def custom(): """Change the custom user account""" accounts_map = dict() + database = Database() custom_accounts = database.get_custom_accounts() if not len(custom_accounts): diff --git a/croco_cli/cli/init.py b/croco_cli/cli/init.py index 265a01e..b1b980c 100644 --- a/croco_cli/cli/init.py +++ b/croco_cli/cli/init.py @@ -1,11 +1,11 @@ """ This module contains functions to initialize python packages and projects """ - +import datetime import os import click from croco_cli.utils import snake_case, require_github -from croco_cli.database import database +from croco_cli.database import Database @click.group() @@ -25,6 +25,8 @@ def _add_poetry( :param is_package: Whether project should be configured as Python package :return: None """ + database = Database() + snaked_name = snake_case(project_name) github_user = database.get_github_user() @@ -93,6 +95,8 @@ def _initialize_folders( :param description: The description of the project :return: None """ + database = Database() + github_user = database.get_github_user() snaked_name = snake_case(project_name) os.mkdir(snaked_name) @@ -132,7 +136,7 @@ def _initialize_folders( with open('LICENSE', 'w') as license_file: license_file.write(f"""MIT License -Copyright (c) 2023 {github_user['name']} +Copyright (c) {datetime.datetime.now().year} {github_user['name']} Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -168,8 +172,7 @@ async def main(): if __name__ == '__main__': asyncio.run(main())""") with open('globals.py', 'w') as global_file: - global_file.write(f""" -import os + global_file.write(f"""import os import tomllib PROJECT_PATH = os.path.dirname(os.path.abspath(__file__)) @@ -197,6 +200,8 @@ def _add_readme( :param is_package: Whether the readme should be configured for the Python package :return: None """ + database = Database() + github_user = database.get_github_user() content = (f"""# {project_name} diff --git a/croco_cli/cli/install.py b/croco_cli/cli/install.py index 5f73d0d..50c0060 100644 --- a/croco_cli/cli/install.py +++ b/croco_cli/cli/install.py @@ -5,10 +5,10 @@ import os import click from functools import partial +from croco_cli.database import Database from croco_cli.types import Option, Package, GithubPackage, PackageSet from croco_cli.utils import show_key_mode, require_github, is_github_package from croco_cli.globals import PYPI_PACKAGES, GITHUB_PACKAGES, PACKAGE_SETS -from croco_cli.database import database _DESCRIPTION = "Install Croco Factory packages" @@ -47,6 +47,8 @@ def _make_install_option( :param package: package to install :return: An installing option """ + database = Database() + github_user = database.get_github_user() package_name = package['name'] description = package['description'] diff --git a/croco_cli/cli/make.py b/croco_cli/cli/make.py index ebc17c7..d0b789f 100644 --- a/croco_cli/cli/make.py +++ b/croco_cli/cli/make.py @@ -1,7 +1,7 @@ import json import click +from croco_cli.database import Database from croco_cli.utils import require_wallet, constant_case -from croco_cli.database import database @click.group() @@ -13,6 +13,8 @@ def make(): @require_wallet def dotenv(): """Make file with environment variables. Use with python-dotenv""" + database = Database() + wallets = database.get_wallets() custom_accounts = database.get_custom_accounts(current=True) env_variables = database.get_env_variables() diff --git a/croco_cli/cli/reset.py b/croco_cli/cli/reset.py index cae7d5e..34e36a9 100644 --- a/croco_cli/cli/reset.py +++ b/croco_cli/cli/reset.py @@ -1,5 +1,5 @@ import click -from croco_cli.database import database +from croco_cli.database import Database @click.command() @@ -45,6 +45,8 @@ ) def reset(user: bool, git: bool, wallets: bool, custom: bool, envar: bool): """Reset user accounts""" + database = Database() + if git or wallets or custom or envar: if git: database.github_users.drop_table() diff --git a/croco_cli/cli/set.py b/croco_cli/cli/set.py index faba59c..43ab767 100644 --- a/croco_cli/cli/set.py +++ b/croco_cli/cli/set.py @@ -1,9 +1,9 @@ import click from typing import Optional -from croco_cli.database import database from croco_cli.types import CustomAccount, Wallet from .user import _show_github, _show_custom_account from croco_cli.utils import show_wallet, show_detail, constant_case +from croco_cli.database import Database @click.group(name='set') @@ -17,6 +17,8 @@ def _set(): @click.argument('mnemonic', default=None, required=False, type=click.STRING) def wallet(private_key: str, label: Optional[str] = None, mnemonic: Optional[str] = None) -> None: """Set wallet for unit tests using its private key""" + database = Database() + database.set_wallet(private_key, label, mnemonic) public_key = database.get_public_key(private_key) current_wallet = Wallet( @@ -33,10 +35,12 @@ def wallet(private_key: str, label: Optional[str] = None, mnemonic: Optional[str @click.argument('access_token', default=None, required=False, type=click.STRING) def git(access_token: str): """Set GitHub user account, using access token""" + database = Database() + if not access_token: access_token = click.prompt('Please enter the access token of new account', hide_input=True) - database.set_github(access_token) + database.set_github_user(access_token) _show_github() @@ -62,6 +66,8 @@ def custom( email_password: Optional[str] = None ) -> None: """Set a custom user account""" + database = Database() + email_password = email_password or password data = {key: value for key, value in fields} @@ -85,6 +91,8 @@ def custom( @click.argument('value', type=click.STRING) def envar(key: str, value: str) -> None: """Set an environment variable""" + database = Database() + key = constant_case(key) database.set_env_variable(key, value) show_detail(key, value, 0) diff --git a/croco_cli/cli/user.py b/croco_cli/cli/user.py index 70e9ce6..07db056 100644 --- a/croco_cli/cli/user.py +++ b/croco_cli/cli/user.py @@ -1,6 +1,6 @@ import json import click -from croco_cli.database import database +from croco_cli.database import Database from croco_cli.types import CustomAccount from croco_cli.utils import require_github, show_detail, show_label, hide_value, show_account_dict, show_wallets, echo_error @@ -43,6 +43,8 @@ def user(git: bool, wallets: bool, custom: bool) -> None: def _show_github() -> None: """Show GitHub user account""" + database = Database() + github_user = database.get_github_user() access_token = hide_value(github_user['access_token'], 10) show_label('GitHub') @@ -66,6 +68,8 @@ def _show_custom_account(custom_account: CustomAccount) -> None: def _show_custom_accounts() -> None: """Show custom accounts of user""" + database = Database() + if not database.custom_accounts.table_exists(): echo_error('There are no custom accounts to show') return diff --git a/croco_cli/database.py b/croco_cli/database.py index 8262ec0..49e62b0 100644 --- a/croco_cli/database.py +++ b/croco_cli/database.py @@ -13,6 +13,16 @@ from peewee import Model, CharField, BlobField, SqliteDatabase, BooleanField +class _DatabaseMeta(type): + _instance = None + + def __call__(cls, *args, **kwargs): + if not isinstance(cls._instance, cls): + cls._instance = super().__call__(*args, **kwargs) + + return cls._instance + + def _get_cache_folder() -> str: username = getpass.getuser() os_name = os.name @@ -32,7 +42,7 @@ def _get_cache_folder() -> str: return cache_path -class Database: +class Database(metaclass=_DatabaseMeta): _path: ClassVar[str] = os.path.join(_get_cache_folder(), 'user.db') interface: ClassVar[SqliteDatabase] = SqliteDatabase(_path) @@ -368,7 +378,7 @@ def set_env_variable( def get_env_variables(self) -> list[EnvVariable] | None: env_variables = self._env_variables if not env_variables.table_exists(): - return + return query = env_variables.select() @@ -383,6 +393,3 @@ def get_env_variables(self) -> list[EnvVariable] | None: def delete_env_variables(self) -> None: env_variables = self._env_variables env_variables.delete().execute() - - -database = Database() diff --git a/croco_cli/globals.py b/croco_cli/globals.py index 2bdea81..a10ec0f 100644 --- a/croco_cli/globals.py +++ b/croco_cli/globals.py @@ -26,7 +26,6 @@ GITHUB_PACKAGES = ( _PY_OKX, - _SELDEGEN ) PACKAGE_SETS = { diff --git a/croco_cli/utils.py b/croco_cli/utils.py index 6a9df31..fb212a3 100644 --- a/croco_cli/utils.py +++ b/croco_cli/utils.py @@ -3,13 +3,15 @@ """ import os import re -import curses +import blessed import getpass from typing import Any, Callable, Optional import click +from croco_cli.database import Database from croco_cli.types import Option, Wallet, Package, GithubPackage, AnyCallable from functools import partial, wraps -from croco_cli.database import database + +_term = blessed.Terminal() def snake_case(s: str) -> str: @@ -45,31 +47,20 @@ def is_github_package(package: Package | GithubPackage) -> bool: def _show_key_mode( options: list[Option], command_description: str, - stdscr: curses.window + terminal: blessed.Terminal ) -> Any: """ Shouldn't be used directly, instead use show_key_mode """ - # TODO: Sometimes, there are many options to show them instantly. Fix that - exit_option = Option( name='Exit', description='Return to the terminal', - handler=curses.endwin + handler=lambda: None # No-op handler ) options.append(exit_option) - curses.curs_set(0) # Hide the cursor - curses.start_color() - curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_GREEN) # Color pair for selected option - - stdscr.clear() - - stdscr.refresh() - - # Set up initial variables current_option = 0 padded_name_len = max([len(option['name']) for option in options]) + 2 @@ -82,55 +73,45 @@ def _show_key_mode( use_description = False break + padded_description_len = None if use_description: padded_description_len = max(padded_description_lengths) + 2 - while True: - stdscr.addstr(0, 0, command_description, curses.color_pair(1) | curses.A_BOLD | curses.A_REVERSE) + with terminal.fullscreen(), terminal.cbreak(), terminal.hidden_cursor(): + while True: + print(terminal.move_yx(0, 0) + _term.clear()) + print(terminal.bold_green(command_description + '\n')) - for i, option in enumerate(options): - name = option["name"].ljust(padded_name_len) + for i, option in enumerate(options): + name = option["name"].ljust(padded_name_len) - if use_description: - description = option['description'].ljust(padded_description_len) - option_text = f'{name} | {description}' - else: - option_text = name - if i == current_option: - stdscr.addstr(i + 2, 0, f"> {option_text}", curses.color_pair(1)) - else: - stdscr.addstr(i + 2, 0, f" {option_text}") + if use_description: + description = option['description'].ljust(padded_description_len) + option_text = f'{name} | {description}' + else: + option_text = name + + if i == current_option: + print(_term.green_reverse(f"> {option_text}")) + else: + print(f" {option_text}") - key = stdscr.getch() + key = terminal.inkey() - last_option_idx = len(options) - 1 - if key == curses.KEY_UP: - if current_option > 0: - current_option -= 1 - else: - current_option = last_option_idx - elif key == curses.KEY_DOWN: - if current_option < last_option_idx: - current_option += 1 - else: - current_option = 0 - elif key == 127 and (deleting_handler := options[current_option].get('deleting_handler')): - deleting_handler() - options.pop(current_option) - stdscr.clear() - if len(options) > 1: + last_option_idx = len(options) - 1 + if key.name == 'KEY_UP': if current_option > 0: current_option -= 1 else: + current_option = last_option_idx + elif key.name == 'KEY_DOWN': + if current_option < last_option_idx: current_option += 1 - else: - curses.endwin() - return - elif key == 10: - selected_option = options[current_option] - stdscr.refresh() - curses.endwin() - return selected_option['handler']() + else: + current_option = 0 + elif key == '\n': + selected_option = options[current_option] + return selected_option['handler']() def show_key_mode( @@ -144,8 +125,8 @@ def show_key_mode( :param command_description: description of the command :return: None """ - handler = partial(_show_key_mode, options, command_description) - curses.wrapper(handler) + handler = partial(_show_key_mode, options, command_description, _term) + handler() def echo_error(text: str) -> None: @@ -179,6 +160,8 @@ def require_github(func: Callable): :param func: The function to be decorated. :return: The decorated function. """ + database = Database() + @wraps(func) def wrapper(*args, **kwargs): if not database.github_users.table_exists(): @@ -202,6 +185,8 @@ def require_wallet(func: Callable): :param func: The function to be decorated. :return: The decorated function. """ + database = Database() + @wraps(func) def wrapper(*args, **kwargs): if not database.wallets.table_exists(): @@ -213,6 +198,7 @@ def wrapper(*args, **kwargs): echo_warning('Wallet private key is missing. Set it to continue (croco set wallet).') else: return func(*args, **kwargs) + return wrapper @@ -391,6 +377,8 @@ def show_wallets() -> None: :return: None """ + database = Database() + wallets = database.get_wallets() wallets = sort_wallets(wallets) for wallet in wallets: diff --git a/pyproject.toml b/pyproject.toml index 7b24a99..ab71f0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [tool.poetry] name = 'croco_cli' -version = '0.1.0' +version = '0.2.0' description = 'The CLI for developing Web3-based projects in Croco Factory' -authors = ['Alexey '] +authors = ['Alexey '] license = 'MIT' readme = 'README.md' repository = 'https://github.com/blnkoff/croco-cli' @@ -26,6 +26,7 @@ click = "^8.1.7" pygithub = "^2.2.0" peewee = "^3.17.0" eth-account = "^0.11.0" +blessed = "^1.20.0" [tool.poetry.group.dev.dependencies] pytest = "^8.0.0" From 568a827dd22ad0f3a4e24fcddaf636b5dd12c2c4 Mon Sep 17 00:00:00 2001 From: blnkoff Date: Wed, 5 Jun 2024 19:33:56 +0300 Subject: [PATCH 2/2] Updating dev-dependencies --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ab71f0e..fa863d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,9 @@ eth-account = "^0.11.0" blessed = "^1.20.0" [tool.poetry.group.dev.dependencies] -pytest = "^8.0.0" twine = "^5.1.0" build = "^1.2.1" +pytest = "^8.2.2" [build-system] requires = ['poetry-core']