From b8df31f0361ad248ac7f1fea44c7c332212b8e25 Mon Sep 17 00:00:00 2001 From: mrjk Date: Sun, 8 Oct 2023 14:25:22 -0400 Subject: [PATCH] wip: shell integration, fixing cli --- iam/app.py | 26 +++- iam/assets/init_bash.sh | 180 +++++++++++++++++++++++++ iam/catalog.py | 254 +++++++++++++++++++++++++++++------ iam/cli_click.py | 240 +++++++++++++++++++++++++-------- iam/{cli.py => cli_typer.py} | 0 iam/cli_views.py | 192 -------------------------- iam/exceptions.py | 4 + iam/framework.py | 18 ++- iam/lib/cli_views.py | 47 ++++--- iam/lib/utils.py | 17 ++- iam/plugins/base.py | 4 +- iam/plugins/devops.yml | 4 +- iam/plugins/local.yml | 96 +++++++++++-- poetry.lock | 169 +---------------------- pyproject.toml | 4 +- 15 files changed, 753 insertions(+), 502 deletions(-) create mode 100644 iam/assets/init_bash.sh rename iam/{cli.py => cli_typer.py} (100%) delete mode 100644 iam/cli_views.py diff --git a/iam/app.py b/iam/app.py index e2b7558..dc34a4b 100644 --- a/iam/app.py +++ b/iam/app.py @@ -12,7 +12,7 @@ from . import exceptions as error from .catalog import Catalog from .framework import DictItem, scoped_ident from .idents import Idents -from .lib.utils import import_module, open_yaml +from .lib.utils import import_module, open_yaml, get_root_pkg_dir from .meta import NAME, VERSION from .providers import Providers @@ -181,10 +181,26 @@ class App: ret = {svc.name: svc.list_cmds() for svc in self.catalog.services.values()} return ret - def shell_enable(self): + def shell_enable(self, **kwargs): "Enable shell" - return self.catalog.shell_enable() + return self.catalog.shell_enable(**kwargs) - def shell_disable(self): + def shell_disable(self, **kwargs): "Disable shell" - return self.catalog.shell_disable() + return self.catalog.shell_disable(**kwargs) + + + def shell_install(self, shell="bash"): + "Install iam in shell" + + assets_dir = os.path.join(get_root_pkg_dir("iam"), "assets") + + + if shell == "bash": + config_file = ".bashrc" + template_file = os.path.join(assets_dir, "init_bash.sh") + + else: + raise Exception(f"Unsupported shell: {shell}") + + return f"source {template_file}" \ No newline at end of file diff --git a/iam/assets/init_bash.sh b/iam/assets/init_bash.sh new file mode 100644 index 0000000..1456f06 --- /dev/null +++ b/iam/assets/init_bash.sh @@ -0,0 +1,180 @@ + +# Workllow +# +# API +# i +# i status +# i switch +# i disable +# i help +# i FORWARD + +# ${debian_chroot:+($debian_chroot)}\u@\h:\w\$ + + +#IAM_BIN=/home/jez/volumes/data/prj/jez/lab/iam-python/.direnv/python-3.11.3/bin/iam +IAM_BIN=$(command -v iam) +IAM_IDENT= + +_usage () +{ + echo "iam shell wrapper" + + cat << EOF + + Usage: + i [status] Show shell status + i switch IDENT Enable shell identity + i disable Disable shell identity + i kill Disable and kill shell identity + i help Show this help + + Informations: + IAM_BIN=${IAM_BIN} + SHELL_IDENT=${SHELL_IDENT:-_} + +EOF +} + +i () +{ +# set -x + + # Get status + target_ident=_ + current_ident=${SHELL_IDENT:-_} + available_idents=$($IAM_BIN shell idents -O words) + + # Internal vars + remaining_args= + idents_pattern=:${available_idents// /:}: + index=0 + action= + + + while [[ -n "${1-}" ]] ; do + local arg=$1 + + case "$arg" in + status|st) + action=status + shift 1 + if [[ -n "${1}" ]]; then + target_ident=$1 + shift 1 + fi + ;; + switch|enable|en) + action=switch + shift 1 + if [[ -n "${1}" ]]; then + target_ident=$1 + shift 1 + fi + ;; + disable|dis|q) + action=disable + shift 1 + ;; + kill|k) + action=kill + shift 1 + ;; + stop|K) + action=kill_all + shift 1 + ;; + help|--help|-h) + action=help + shift 1 + ;; + *) + if [[ "$idents_pattern" == *":$arg:"* ]]; then + action=switch + target_ident=$arg + shift 1 + else + remaining_args="$remaining_args $arg" + shift 1 + fi + ;; + esac + + ((index++)) + done + + if [[ -z "$action" ]]; then + action="idents" + fi + + + # echo "Current Ident: $current_ident" + # echo "Target Ident: $target_ident" + # echo "Action: $action" + + case "$action" in + idents) + echo "Iam path: ${IAM_BIN}" + echo "Current ident: ${SHELL_IDENT:-}" + echo "Available identities: $($IAM_BIN shell idents -O words)" + ;; + + switch) + # if [[ "$current_ident" != '_' ]]; then + # eval $IAM_BIN shell disable $current_ident + # fi + # $IAM_BIN shell switch $target_ident + + if [[ "$target_ident" != '_' ]]; then + >&2 echo "DEBUG: eval: $IAM_BIN shell switch $target_ident" + eval "$($IAM_BIN shell switch $target_ident)" + fi + ;; + + disable) + if [[ "$current_ident" != '_' ]]; then + >&2 echo "DEBUG: eval: $IAM_BIN shell switch" + eval "$($IAM_BIN shell disable)" + fi + ;; + + kill) + if [[ "$current_ident" != '_' ]]; then + >&2 echo "DEBUG: eval: $IAM_BIN shell switch --kill" + eval "$($IAM_BIN shell disable --kill)" + fi + ;; + + kill_all) + eval "$($IAM_BIN shell kill -a)" + ;; + + status) + echo "IAM_BIN=${IAM_BIN}" + echo "SHELL_IDENT=${SHELL_IDENT:-_}" + + #echo $IAM_BIN shell status $current_ident + ;; + help) + _usage + ;; + esac + + + + set +x + return + + + # Source or switch + if $source_output; then + out=($IAM_BIN $@) + echo "SOURCE: $out" + else + echo $IAM_BIN $@ + fi + + set +x +} + +#i2 $@ diff --git a/iam/catalog.py b/iam/catalog.py index 4ab3bca..508fa4f 100644 --- a/iam/catalog.py +++ b/iam/catalog.py @@ -9,6 +9,7 @@ from .idents import Ident from .lib.utils import format_render, jinja_render, uniq logger = logging.getLogger(__name__) +cli_logger = logging.getLogger("iam.cli") IamException = error.IamException @@ -47,7 +48,18 @@ class ServiceCommand(DictItem): default_attrs = { "desc": "Service without description", - "cmd": "", + + "cmd": "", # Direct commands to launch + + "shell": "", # Will be run in a shell (or specific shell below) + "shell_sh": "", + "shell_bash": "", + "shell_zsh": "", + "shell_fish": "", + "shell_bash": "", + + "source": False, # True for enable commands ! + } # Overrides @@ -55,12 +67,15 @@ class ServiceCommand(DictItem): def payload_transform(self, name, kwargs=None): "Transform short form into long form" + self.service = self.parent payload = self._payload if not isinstance(payload, dict): payload = {"cmd": payload} self._payload = payload + def cmd_name(self): + return self.name.replace("_", " ") class Service(DictItem): "Hold provider services" @@ -83,9 +98,10 @@ class Service(DictItem): self.catalog = self.parent.catalog def init(self): + ret = {} for cmd_name, cmd in self.commands.items(): - ret[cmd_name] = ServiceCommand(cmd_name, cmd) + ret[cmd_name] = ServiceCommand(cmd_name, cmd, parent=self) self.commands = ret # def require_resolved_deps(func): @@ -200,7 +216,7 @@ class Service(DictItem): out = cmd.format(**env_var) except KeyError as err: msg = f"Missing variable: {err} for service: {res.key}" - # print (msg) + logger.warning (msg) out = msg # raise IamException(msg) @@ -213,6 +229,13 @@ class Service(DictItem): # def _list_cmds(self): + def get_cmds(self): + "Get all services commands objects" + + return [cmd for name, cmd in self.commands.items() if not name.startswith("shell")] + + + def list_cmds(self): "List all services commands" @@ -226,11 +249,15 @@ class Service(DictItem): for cmd_name, conf in self.commands.items(): target = ret2["cmds"] + if cmd_name.startswith(shell_prefix): target = ret2["shell"] - cmd_name = cmd_name[len(shell_prefix) :] + # cmd_name = cmd_name[len(shell_prefix):] + name = f"{cmd_name}_{prefix}" + else: + + name = cmd_name.replace("_", " ") - name = f"{prefix} {cmd_name}" target[name] = conf.desc return ret2 @@ -253,9 +280,76 @@ class Services(DictCtrl): items_class = Service + RESERVED_CMD_PREFIX=["kind", "res", "svc", "shell", "run"] + def prepare(self): self.catalog = self.parent + + + def get_svc_command(self, cmd): + "Return the command to be run" + + # Prepare context + cmd_parts = cmd + if isinstance(cmd, str): + cmd_parts = cmd.split(" ") + cmd_req = " ".join(cmd_parts) + + # Retrieve command list + cmds = self.list_cmds() + cmd_names = [cmd.name for cmd in cmds ] + + + # Find best matching command + cmd_split_idx = None + cmd = None + curr=None + for index, part in enumerate(cmd_parts): + curr = f"{curr} {part}" if curr is not None else part + if curr in cmd_names: + cmd_idx = cmd_names.index(curr) + cmd = cmds[cmd_idx] + cmd_split_idx = index + 1 + break + + # Validate result + if not cmd: + _choices = ','.join(cmd_names) + _msg = f"No such services command named: {cmd_req}, please choose one of: {_choices}" + raise error.UnknownServiceCommand(_msg) + + args = cmd_parts[cmd_split_idx:] + + return cmd, args + + + + def list_cmds(self): + "List all services commands" + + ret = [] + for name, service in self.items(): + cmds = service.get_cmds() + ret.extend(cmds) + + # Check for invalid configs + conf = [] + for cmd in ret: + prefix = cmd.name.split(" ")[0] + if prefix in self.RESERVED_CMD_PREFIX: + _msg = f'Forbidden prefix! {cmd}' + raise Exception (_msg) + conf.append(cmd.name ) + dump = uniq(conf) + + if conf != dump: + _msg = f'Duplicates values! {conf}' + raise Exception (_msg) + + return ret + + def get_linked_resources(self): """ Like get method, but only grab enabled services @@ -798,18 +892,36 @@ class Catalog: # return self.services.get_loading_order(reverse=reverse) + # Shell helpers # ================ - def shell_enable(self): + def shell_enable(self, run_start=True): "Enable shell" - return self._shell_action() - def shell_disable(self): + + actions = ["shell_enable"] + if run_start: + actions.insert(0, "shell_start") + # logger.warning("Shell Start") + # logger.warning("Shell Enable") + + return self._shell_action(actions) + + + def shell_disable(self, run_stop=False): "Disable shell" - return self._shell_action(reverse=True) - def _shell_action(self, reverse=False): + actions = ["shell_disable"] + if run_stop: + actions.insert(0, "shell_stop") + # cli_logger.info("Shell Stop") + # cli_logger.info("Shell Disable") + + return self._shell_action(actions) + + + def _shell_action(self, action_names, reverse=False): "Run command order" ident = self.ident @@ -819,12 +931,10 @@ class Catalog: order, services = self.services.get_loading_order(reverse=reverse) # Prepare context - action_name = "shell_enable" log_action_name = "Enable" if reverse: - action_name = "shell_disable" log_action_name = "Disable" - logger.info(f"{log_action_name} identity: {ident.name}") + cli_logger.info(f"Identity {ident.name} action: {','.join(action_names)}") # Execute on each plugins commands output_code = [] @@ -833,38 +943,102 @@ class Catalog: res = services[srv_name].res # Load service command - command = service.commands.get(action_name, None) - if not command: - continue + for action_name in action_names: + command = service.commands.get(action_name, None) + if not command: + continue - # Build loop with at least ONE item - cmd = command.cmd - if not cmd: - continue + # Build loop with at least ONE item + cmd = command.cmd + if not cmd: + continue - # Create context var dict - ctx_vars = dict() - ctx_vars.update(service.input) - ctx_vars.update(vars) - ctx_vars.update(res.resolve_inputs(vars=ctx_vars)) + # Create context var dict + ctx_vars = dict() + ctx_vars.update(service.input) + ctx_vars.update(vars) + ctx_vars.update(res.resolve_inputs(vars=ctx_vars)) - loops = res.loop_extend() or [res] - res_vars = [x.resolve_inputs(vars=ctx_vars) for x in loops] + loops = res.loop_extend() or [res] + res_vars = [x.resolve_inputs(vars=ctx_vars) for x in loops] - ctx_vars.update( - { - "item": res_vars[0], - "loop": res_vars, - } - ) + ctx_vars.update( + { + "item": res_vars[0], + "loop": res_vars, + } + ) - logger.debug(f"{log_action_name} service: {res.name}") + logger.debug(f"{log_action_name} service: {res.name}") - # pprint (ctx_vars) - cmd = jinja_render(cmd, ctx_vars) - output_code.append(f"# Loading of {res.name} ({service.name})") - output_code.append(f"# =====================") - output_code.append(cmd) - output_code.append("\n") + # pprint (ctx_vars) + cmd = jinja_render(cmd, ctx_vars) + output_code.append(f"# Loading: {action_name} {res.name} ({service.name})") + output_code.append(f"# =====================") + output_code.append(cmd) + output_code.append("\n") return "\n".join(output_code) + + + + + def run_svc_cmd(self, cmd): + "Run a command against the services" + + cmd, args = self.services.get_svc_command(cmd) + + + + + # pprint (cmd.cmd) + pprint (args) + + service = cmd.service + + pprint (service) + pprint (service.__dict__) + # pprint (dir(service)) + res = service.get_linked_resource() + # pprint (res) + + vars = self.context.get_vars() + + ctx_vars = dict() + ctx_vars.update(vars) + ctx_vars.update(service.inputs) + + # ctx_vars.update(res.resolve_inputs(vars=ctx_vars)) + + # loops = res.loop_extend() or [res] + # res_vars = [x.resolve_inputs(vars=ctx_vars) for x in loops] + + # ctx_vars.update( + # { + # "item": res_vars[0], + # "loop": res_vars, + # } + # ) + + pprint (ctx_vars) + + + if cmd.cmd: + mode = "cmd" + payload = cmd.cmd + real_cmd = jinja_render(payload, ctx_vars) + + + elif cmd.shell: + mode = "shell" + payload = cmd.shell + real_cmd = jinja_render(payload, ctx_vars) + + + else: + raise Exception("MIssing cmd or shell in config !") + + + + + print ("RUN CMD", mode, "\n\n\n====\n", real_cmd) \ No newline at end of file diff --git a/iam/cli_click.py b/iam/cli_click.py index 14fa453..fb2e573 100755 --- a/iam/cli_click.py +++ b/iam/cli_click.py @@ -45,7 +45,7 @@ else: import rich_click as click from rich_click import RichGroup - from rich import box, print + from rich import box #, print from rich.console import Console # Overrides defaults from rich.pretty import pprint @@ -67,6 +67,8 @@ else: logger = logging.getLogger() +cli_logger = logging.getLogger("iam.cli") + # Global vars # ====================== @@ -97,7 +99,7 @@ DEFAULT_LOG_LEVEL = 30 # warning DEBUG = os.environ.get("IAM_DEBUG", "False") DEFAULT_IDENT = os.environ.get("IAM_IDENT", os.environ.get("SHELL_IDENT", "")) DEFAULT_FORMAT = os.environ.get("IAM_FORMAT", "table") - +DEFAULT_HELP_OPTIONS=["-h", "--help"] # list_view.formats_enum @@ -110,7 +112,7 @@ _cmd2_options = [click.option("--cmd2-opt")] CONTEXT_SETTINGS = dict( show_default=True, - help_option_names=["-h", "--help"], + help_option_names=DEFAULT_HELP_OPTIONS, auto_envvar_prefix=APP_NAME.upper(), ) @@ -219,7 +221,8 @@ def get_var(name, default=None): # @click.group(cls=NestedHelpGroup, context_settings=CONTEXT_SETTINGS) -@click.group(cls=NestedHelpGroup) +@click.group(cls=NestedHelpGroup +) @global_options @format_options @click.version_option() @@ -259,7 +262,7 @@ def cli_app( # Define loggers loggers = { "iam": {"level": log_level}, - "iam.cli": {"level": "INFO"}, + "iam.cli": {"level": "DEBUG", "handlers": ["info"]}, } # Instanciate logger @@ -275,8 +278,6 @@ def cli_app( "table_settings": table_box_params, } - pprint(cli_config) - render_item = lambda *args, conf={}, **kwargs: item_view.render( *args, conf={**render_config, **prune(conf)}, **kwargs ) @@ -630,7 +631,7 @@ def svc_show(ctx, name, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=N def svc_commands(ctx, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None): "Show service" - columns = ["name", "type", "desc"] + columns = ["service", "name", "type", "desc"] data = [] for svc_name, svc in ctx.obj.app.list_services().items(): @@ -641,10 +642,7 @@ def svc_commands(ctx, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=Non for cmd_name, conf in items.items(): # target[cmd_name] = conf - data.append([cmd_name, source, conf]) - - # pprint(ret) - # pprint(ret, expand_all=True) + data.append([svc_name, cmd_name, source, conf]) # Render data ctx.obj.render_list( @@ -659,7 +657,7 @@ def svc_commands(ctx, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=Non ) -@cli__svc.command("run") +@cli__svc.command("run_v1") @click.argument("name") @click.argument("command") @format_options @@ -687,23 +685,12 @@ def cli__shell(): @format_options @click.pass_context def shell_idents( - ctx, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None, reverse=False + ctx, fmt_name=None, fmt_fields=None, fmt_sort=None, verbose=None, ): "Shell identities" columns = ["name"] data = [[x] for x in ctx.obj.app.get_idents()] - # print(" ".join(ret)) - - # return - - # columns = ["order", "name"] - # data = [] - # services, _ = ctx.obj.app.catalog.services.get_loading_order(reverse=reverse) - # for index, name in enumerate(services): - # # table.add_row(str(index), name) - # data.append([str(index), name]) - # Render data ctx.obj.render_list( data, @@ -748,44 +735,191 @@ def shell_order( ) + + @cli__shell.command("enable") -@click.argument("ident") -# @format_options +@click.option( + "skip_start", "--no-start", "-b", is_flag=True, show_default=False, default=False, help="Disable background process start" +) @click.pass_context -def shell_enable(ctx, ident, verbose=None): +def shell_enable(ctx, skip_start=True, verbose=None): + "Enable identity in shell" + + app = ctx.obj.app + print(app.shell_enable(run_start=not skip_start)) + + + +@cli__shell.command("disable") +@click.option( + "run_stop", "--kill", "-k", is_flag=True, show_default=False, default=False, help="Kill background process on leave" +) +@click.pass_context +def shell_disable(ctx, run_stop=False, verbose=None): + "Disable identity in shell" + + ret = ctx.obj.app.shell_disable(run_stop=run_stop) + print(ret) + + +@cli__shell.command("kill") +@click.option( + "all_idents", "--all", "-a", is_flag=True, show_default=False, default=False, help="Kill on all users" +) +@click.pass_context +def shell_kill(ctx, all_idents=False, verbose=None): + "Disable identity in shell" + + ret = [] + + app = ctx.obj.app + if not all_idents: + ret.append(app.shell_enable(run_start=False)) + ret.append(app.shell_disable(run_stop=True)) + + else: + for ident in app.idents.names(): + cli_logger.warning(f"Kill ident {ident}") + app.init_ident(ident) + ret.append(app.shell_enable(run_start=False)) + ret.append(app.shell_disable(run_stop=True)) + + print('\n'.join(ret)) + + + +@cli__shell.command("switch") +@click.argument("ident", required=False) +# @format_options +@click.option( + "skip_start", "--no-start", "-b", is_flag=True, show_default=False, default=False, help="Disable background process start" +) +@click.option( + "run_stop", "--kill", "-k", is_flag=True, show_default=False, default=False, help="Kill background process on leave" +) + +@click.pass_context +def shell_switch(ctx, ident="", run_stop=False, skip_start=True, verbose=None): "Enable identity in shell" app = ctx.obj.app - # Disable existing - # logger.info(f"Disabling ident: {app.ident_name}") - ret1 = app.shell_disable() - print("-- %< --" * 8) - print(ret1) - print("-- %< --" * 8) - - # Enable new - # logger.info(f"Enabling ident: {ident}") - app.init_ident(ident) - ret2 = app.shell_enable() - - # Output - print("-- %< --" * 8) - print(ret2) - print("-- %< --" * 8) + src_ident = app.catalog.ident.name + dst_ident = ident -@cli__shell.command("disable") -# @format_options + if src_ident == dst_ident: + print (">&2 echo 'No need to change'") + else: + ret = [] + + # Disable existing + # logger.info(f"Disabling ident: {app.ident_name}") + ret.append(app.shell_disable(run_stop=run_stop)) + + # Enable new + # logger.info(f"Enabling ident: {ident}") + app.init_ident(dst_ident) + ret.append(app.shell_enable(run_start=not skip_start)) + + # Output + print('\n'.join(ret)) + + + + + +class TmpGroup(click.Group): + def format_help(self, ctx, formatter): + + val = "List of available commands!" + formatter.write(val) + + + +@cli_app.command("run", + # cls=TmpGroup, + context_settings=dict( + ignore_unknown_options=True, + allow_extra_args=True, + help_option_names=[] + ) +) +@click.argument('cmd_params', nargs=-1, type=click.UNPROCESSED) @click.pass_context -def shell_disable(ctx, verbose=None): - "Disable identity in shell" +def cli_app_run(ctx, cmd_params): - ret = ctx.obj.app.shell_disable() + print("Will run cmd:", cmd_params) - print("-- %< --" * 8) + # Check first help parameter only + show_help = False + show_list = False + for idx, param in enumerate(cmd_params): + if idx == 0: + if param in DEFAULT_HELP_OPTIONS: + show_help = True + if param in ["list", "ls"]: + show_list = True + + if show_help: + ctx = click.get_current_context() + click.echo(ctx.get_help()) + return + + + # Get instanciated app + app = ctx.obj.app + + if show_list: + ret = app.catalog.services.list_cmds() + data = [[x.name, x.desc] for x in ret] + + + # Render data + ctx.obj.render_list( + data, + ["name", "desc"], + conf={ + "table_title": f"Services commands", + # "fmt_name": fmt_name, + # "fmt_sort": fmt_sort, + # "fmt_fields": fmt_fields, + }, + ) + + return + + # Run the output + ret = app.catalog.run_svc_cmd(cmd=cmd_params) + pprint (ret, expand_all=True) + + + +# @cli_app.command("run2", +# context_settings=dict( +# ignore_unknown_options=True, +# allow_extra_args=True, +# ) +# ) +# @click.pass_context +# def cli_app_run(ctx): +# print("Hello world") +# pprint (ctx.args) + +# return + + +@cli__shell.command("install") +@click.pass_context +def shell_install(ctx, verbose=None): + "Install iam in your shell" + + ret = ctx.obj.app.shell_install() + + # print("-- %< --" * 8) print(ret) - print("-- %< --" * 8) + # print("-- %< --" * 8) + # Exception handler @@ -796,8 +930,8 @@ def clean_terminate(err): # Choose dead end way if APP_EXCEPTION is not None and isinstance(err, APP_EXCEPTION): err_name = err.__class__.__name__ - logger.error(err) - logger.critical("MyApp exited with error: %s", err_name) + # logger.error(err) + logger.critical("%s: %s" % (err_name, err)) sys.exit(1) diff --git a/iam/cli.py b/iam/cli_typer.py similarity index 100% rename from iam/cli.py rename to iam/cli_typer.py diff --git a/iam/cli_views.py b/iam/cli_views.py deleted file mode 100644 index ee5391d..0000000 --- a/iam/cli_views.py +++ /dev/null @@ -1,192 +0,0 @@ -import enum -import logging -from enum import Enum -from pprint import pformat, pprint -from types import SimpleNamespace - -from iam.framework import (empty, get_app_logger, iterate_any, open_yaml, - prune, to_csv, to_json, to_yaml) -from rich import box -from rich.console import Console -from rich.pretty import pprint -from rich.prompt import Prompt -from rich.table import Table - -from .lib.cli_views import ViewItem, ViewKind, ViewList - -# Iam Implementations -# =============================== - - -MISSING_PKG = [] - -try: - import hcl2 -except ModuleNotFoundError: - MISSING_PKG.append("hcl2") - -try: - import yaml -except ModuleNotFoundError: - MISSING_PKG.append("yaml") - - -# Main views -# ====================== - - -# def view_list(data, columns, title=None, fmt=None, **kwargs): -# "Show list view" - -# fmt = fmt or OutputFormat.DEFAULT -# ret = None -# _kwargs = { -# key[len(fmt.value) + 1 :]: val -# for key, val in kwargs.items() -# if key.startswith(fmt.value) -# } - -# # print ("YOOO", fmt, OutputFormat.YAML) -# if fmt == OutputFormat.YAML: -# ret = to_yaml(restructure_list_to_dict(data, columns)) - -# elif fmt == OutputFormat.JSON: -# ret = to_json(restructure_list_to_dict(data, columns), nice=True) - -# elif fmt == OutputFormat.PYTHON: -# ret = pformat(restructure_list_to_dict(data, columns), indent=2) - -# elif fmt == OutputFormat.CSV: -# ret = to_csv(restructure_list_to_csv(data, columns)) - -# elif fmt == OutputFormat.ENV: -# ret = to_vars(restructure_list_to_env(data, columns), export=True) - -# elif fmt == OutputFormat.VARS: -# ret = to_vars(restructure_list_to_env(data, columns)) - -# elif fmt == OutputFormat.RICH_TABLE: -# ret = to_rich_table(data, columns=columns, title=title, **_kwargs) - -# else: -# raise Exception(f"Unmanagable format: {fmt}") - -# console.print(ret) - - -# def view_show(data, columns=None, title=None): -# "Show single view" - -# ret = None -# _kwargs = { -# key[len(fmt.value) + 1 :]: val -# for key, val in kwargs.items() -# if key.startswith(fmt.value) -# } - -# # print ("YOOO", fmt, OutputFormat.YAML) -# if fmt == OutputFormat.YAML: -# ret = to_yaml(data) - -# elif fmt == OutputFormat.JSON: -# ret = to_json(data, indent=2) - -# elif fmt == OutputFormat.PYTHON: -# ret = pformat(data, indent=2) - -# elif fmt == OutputFormat.CSV: -# ret = to_csv(data) - -# elif fmt == OutputFormat.RICH_TABLE: -# data = list(zip(*data)) -# data.insert(0, ["Field", "Value"]) -# ret = to_rich_table(data, **_kwargs) - -# else: -# raise Exception(f"Unmanagable format: {fmt}") - -# return ret - - -# # DEPRECATED -# # ====================== - - -# def output_list(payload, fmt=None, **kwargs): -# "Render output format" -# assert False, "DEPRECATED" -# fmt = fmt or OutputFormat.YAML - -# # Requested format -# # payload = [ -# # ["Header1", "Header2"], # First line is always headers -# # [["val1", "val2"]], # First row -# # [["val1", "val2"]], # Second row, etc ... -# # ] - -# ret = None -# # _kwargs = {key.lstrip(f"{fmt.value}_"): val for key, val in kwargs.items() if key.startswith(fmt.value)} -# _kwargs = { -# key[len(fmt.value) + 1 :]: val -# for key, val in kwargs.items() -# if key.startswith(fmt.value) -# } - -# # print ("YOOO", fmt, OutputFormat.YAML) -# if fmt == OutputFormat.YAML: -# ret = to_yaml(payload) -# elif fmt == OutputFormat.JSON: -# ret = to_json(payload, indent=2) -# elif fmt == OutputFormat.PYTHON: -# ret = pformat(payload, indent=2) -# elif fmt == OutputFormat.CSV: -# ret = to_csv(payload) -# elif fmt == OutputFormat.RICH_TABLE: -# # pprint (kwargs) -# # pprint (_kwargs) -# return to_rich_table(payload, **_kwargs) -# else: -# raise Exception(f"Unmanagable format list: {fmt}") - -# assert isinstance(ret, str) -# return ret - - -# def output_show(payload, fmt=None, **kwargs): -# "Show one item" -# assert False, "DEPRECATED" - -# # Requested format -# # payload = [ -# # ["Header1", "Header2"], # First line is always headers -# # [["val1", "val2"]], # First row ONLY -# # [["val1", "val2"]], # Second row, etc ... -# # ] - -# ret = None -# _kwargs = { -# key[len(fmt.value) + 1 :]: val -# for key, val in kwargs.items() -# if key.startswith(fmt.value) -# } - -# # print ("YOOO", fmt, OutputFormat.YAML) -# if fmt == OutputFormat.YAML: -# ret = to_yaml(payload) - -# elif fmt == OutputFormat.JSON: -# ret = to_json(payload, indent=2) -# elif fmt == OutputFormat.PYTHON: -# ret = pformat(payload, indent=2) -# elif fmt == OutputFormat.CSV: -# ret = to_csv(payload) -# elif fmt == OutputFormat.RICH_TABLE: -# # Return data -# payload = list(zip(*payload)) -# payload.insert(0, ["Field", "Value"]) -# ret = to_rich_table(payload, **_kwargs) - -# else: -# raise Exception(f"Unmanagable format show: {fmt}") - -# return ret diff --git a/iam/exceptions.py b/iam/exceptions.py index 1f1d32a..23c47bb 100644 --- a/iam/exceptions.py +++ b/iam/exceptions.py @@ -16,3 +16,7 @@ class UnknownResourceKind(IamException): class MissingConfigFiles(IamException): "Raised when iam can't find any valid configuration file" + + +class UnknownServiceCommand(IamException): + "Raised when a command is not matched against services" diff --git a/iam/framework.py b/iam/framework.py index 9ef338d..4967f82 100644 --- a/iam/framework.py +++ b/iam/framework.py @@ -42,12 +42,12 @@ def get_app_logger(loggers=None, level="WARNING", colors=False, format="default" formatters = { "default": { "()": fclass, - "format": "[%(levelname)s] %(message)s", + "format": "[%(levelname)8s] %(message)s", # 'datefmt': '%Y-%m-%d %H:%M:%S', }, "extended": { "()": fclass, - "format": "[%(levelname)s] %(name)s: %(message)s", + "format": "[%(levelname)8s] %(name)s: %(message)s", "datefmt": "%H:%M:%S", }, "audit": { @@ -57,7 +57,7 @@ def get_app_logger(loggers=None, level="WARNING", colors=False, format="default" }, "debug": { "()": fclass, - "format": "%(msecs)03d [%(levelname)s] %(name)s: %(message)s [%(filename)s/%(funcName)s:%(lineno)d]", + "format": "%(msecs)03d [%(levelname)8s] %(name)s: %(message)s [%(filename)s/%(funcName)s:%(lineno)d]", "datefmt": "%H:%M:%S", }, } @@ -81,15 +81,24 @@ def get_app_logger(loggers=None, level="WARNING", colors=False, format="default" "class": "logging.StreamHandler", "stream": "ext://sys.stderr", # Default is stderr }, + "info": { + "level": "INFO", + "formatter": format, + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", # Default is stderr + }, }, # Where logs come from "loggers": { + # Used to catch ALL logs "": { # root logger "handlers": ["default"], "level": "WARNING", "propagate": False, }, + + # # Used to catch all logs of myapp and sublibs # 'myapp': { # 'handlers': ['default'], @@ -297,6 +306,9 @@ class DictCtrl(_DictBase): def keys(self): return self._items.keys() + def names(self): + return list(self._items.keys()) + def has(self, name): "Return true or false if an item exists" return True if name in self._items else False diff --git a/iam/lib/cli_views.py b/iam/lib/cli_views.py index badca40..1c30944 100644 --- a/iam/lib/cli_views.py +++ b/iam/lib/cli_views.py @@ -16,6 +16,12 @@ except ModuleNotFoundError: MISSING_PKGS.append("Table") +# TODO: +# toml/hcl support +# duckdb support => https://stackoverflow.com/a/70538527 + + + # MISSING_PKGS = [] # try: # from rich.console import Console @@ -311,33 +317,32 @@ class _RootView: } ) fmt_ = SimpleNamespace(**fmt_config) - print("FORMAT CONFIG") - pprint(conf) - pprint(fmt_) + # print("FORMAT CONFIG") + # pprint(conf) + # pprint(fmt_) assert fmt_.name, f"Format name can't be None: {fmt_.name}" # check module presence - pprint(self.formats_modules) mod_spec = self.formats_modules.get(fmt_.name, None) - print("MOD_NAME", fmt_.name, mod_spec) - if mod_spec: - # Extract specs - mod_package = None - mod_name = mod_spec - if isinstance(mod_spec, (list, set, tuple)): - mod_name = mod_spec[0] - mod_package = mod_spec[1] + # if mod_spec: + # # Extract specs + # mod_package = None + # mod_name = mod_spec + # if isinstance(mod_spec, (list, set, tuple)): + # mod_name = mod_spec[0] + # mod_package = mod_spec[1] + + # # Check module presence + # # print("CHECKING MODE NAME", mod_name) + # ret = is_module_present(mod_name) + # if not ret: + # _msg = f"Missing python module '{mod_name}' to support '{fmt_.name}' output." + # if mod_package: + # _msg = f"{_msg} Please first install package: {mod_package}" + # logger.warning(_msg) + # # raise Exception(_msg) - # Check module presence - print("CHECKING MODE NAME", mod_name) - ret = is_module_present(mod_name) - if not ret: - _msg = f"Missing python module '{mod_name}' to support '{fmt_.name}' output." - if mod_package: - _msg = f"{_msg} Please first install package: {mod_package}" - logger.warning(_msg) - # raise Exception(_msg) # Fetch renderer method # --------------------------- diff --git a/iam/lib/utils.py b/iam/lib/utils.py index 614eb79..554ef81 100644 --- a/iam/lib/utils.py +++ b/iam/lib/utils.py @@ -1,3 +1,4 @@ +import sys import importlib import json import logging @@ -235,9 +236,23 @@ def iterate_any(payload): # raise Exception(f"Could not iterate over: {payload}") +def get_root_pkg_dir(name): + """Return the dir where the actual paasify source code lives""" + + assert not "." in name, "Only resrved for root pacakges" + + # Look into each paths + for cand in sys.path: + target = os.path.join(cand, name) + if os.path.isdir(target): + return target + + return None + + def get_pkg_dir(name): - """Return the dir where the actual paasify source code lives""" + """Return the dir where the actual paasify source code lives, it loads the module !!!""" # pylint: disable=import-outside-toplevel mod = import_module_pkg(name) diff --git a/iam/plugins/base.py b/iam/plugins/base.py index 213f7c4..643f88c 100644 --- a/iam/plugins/base.py +++ b/iam/plugins/base.py @@ -49,11 +49,11 @@ plugin_base = { "desc": "Disable shell ident", "cmd": "unset SHELL_IDENT", }, - "new": { + "id new": { "desc": "Create shell identy", "cmd": "add_ident {{ param }}", }, - "delete": { + "id delete": { "desc": "Delete shell identy", "cmd": "rm_ident {{ param }}", }, diff --git a/iam/plugins/devops.yml b/iam/plugins/devops.yml index 84b92db..a6d8f7a 100644 --- a/iam/plugins/devops.yml +++ b/iam/plugins/devops.yml @@ -134,12 +134,12 @@ providers: unset MINIO_ACCESS_KEY MINIO_SECRET_KEY - new_alias: + minio new alias: desc: Create alias cmd: | mc alias set {{ident}} {{api_url}} {{minio_user}} {{minio_password}} - rm_alias: + minio delete alias: desc: Remove alias cmd: | mc alias rm {{ident}} diff --git a/iam/plugins/local.yml b/iam/plugins/local.yml index 4c619f2..add62e0 100644 --- a/iam/plugins/local.yml +++ b/iam/plugins/local.yml @@ -14,9 +14,10 @@ providers: commands: - new: + ssh new: desc: Create new SSH key - cmd: | + + shell: | SSH_KEY_ALG={{ssh_key_alg}} SSH_KEY_VERSION="$(date +'%Y%m%d')" @@ -30,7 +31,9 @@ providers: -N "{{ssh_key_secret}}" \ -C "$SSH_KEY_COMMENT" - delete: + + + ssh delete: desc: Delete existing SSH key cmd: | find $HOME/.ssh/{ident}/ -name "{user}_*" @@ -119,28 +122,92 @@ providers: commands: + shell_start: + desc: Start ssh-agent + cmd: | + socket=$HOME/.local/state/ssh-agent/{{user}} + start=true + + running=2 + if [[ ! -e "$socket.env" ]]; then + running=2 + elif [[ -e "$socket" ]]; then + running=$(SSH_AUTH_SOCK=$socket ssh-add -l &>/dev/null; echo $rc) + fi + + if [[ "$running" -eq 2 ]]; then + # Start agent + >&2 echo "Start ssh-agent for {{ident}}" + mkdir -p "${socket%/*}" + ssh-agent -a $socket -t {{ssh_agent_tmout}} > $socket.env + fi + + unset socket start running + + # if [[ -d "/run/user/$(id -u)" ]]; then + # socket=/run/user/$(id -u)/ssh-agent/{{user}} + # else + + # fi + + shell_enable: desc: Enable ssh-agent + cmd: | - export SSH_AUTH_SOCK={{ssh_agent_socket_dir}}/{{user}} - ssh-agent -a $SSH_AUTH_SOCK -t {{ssh_agent_tmout}} - # SSH_AGENT_PID= ??? + socket=$HOME/.local/state/ssh-agent/{{user}} + + if [[ -e "$socket.env" ]]; then + # >&2 echo "Enable ssh-agent for {{ident}}" + source "$socket.env" >/dev/null + fi + + unset socket + + shell_disable: desc: Disable ssh-agent - cmd: ssh-agent -k && unset SSH_AUTH_SOCK + cmd: | + unset SSH_AUTH_SOCK SSH_AGENT_PID + shell_stop: + desc: Kill ssh-agent + cmd: | + socket=$HOME/.local/state/ssh-agent/{{user}} + + if [[ -e "$socket.env" ]]; then + # >&2 echo "Enable ssh-agent for {{ident}}" + source "$socket.env" >/dev/null + # fi + + # if [[ -n "$SSH_AGENT_PID" ]]; then + >&2 echo "Kill ssh-agent for {{ident}}" + eval "(ssh-agent -k)" >/dev/null + [[ -e "$socket.env" ]] && rm "$socket.env" || true + fi + unset socket + + # env_file="$HOME/.local/state/ssh-agent/{{user}}.env" + + # if [[ -f "$env_file" ]]; then + # source "$env_file" + # fi + # if [[ -f "$env_file" ]]; then + # rm "$env_file" + # fi + local.ssh_agent_keys: desc: Local ssh-agent keys commands: - enable: + ssh add: desc: Unload keys into ssh-agent cmd: ssh-agent -d {ssh_key_file} - disable: + ssh rm: desc: Load keys into ssh-agent cmd: | ssh-add {% for item in loop %} {{item.ssh_key_file}} {% endfor %} @@ -263,15 +330,16 @@ providers: commands: shell_enable: - desc: Enable git identity + desc: Enable PS1 cmd: | - OLD_PS1=$PS1 - export PS1="\[\033[0;34m\]({{ident}})\[\033[00m\] ${PS1}" + export OLD_PS1=$PS1 + export PS1="\033[0;34m\]({{ident}})\033[00m\] ${PS1}" shell_disable: - desc: Disable git identity + desc: Disable PS1 cmd: | - export PS1=$OLD_PS1 + export PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' + # export PS1="$OLD_PS1" diff --git a/poetry.lock b/poetry.lock index 08c17c1..c8c98ee 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,34 +1,5 @@ # This file is automatically @generated by Poetry 1.6.0 and should not be changed by hand. -[[package]] -name = "attrs" -version = "23.1.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] - -[[package]] -name = "autopage" -version = "0.5.1" -description = "A library to provide automatic paging for console output" -optional = false -python-versions = ">=3.6" -files = [ - {file = "autopage-0.5.1-py3-none-any.whl", hash = "sha256:0fbf5efbe78d466a26753e1dee3272423a3adc989d6a778c700e89a3f8ff0d88"}, - {file = "autopage-0.5.1.tar.gz", hash = "sha256:01be3ee61bb714e9090fcc5c10f4cf546c396331c620c6ae50a2321b28ed3199"}, -] - [[package]] name = "black" version = "23.9.1" @@ -87,47 +58,6 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -[[package]] -name = "cliff" -version = "4.3.0" -description = "Command Line Interface Formulation Framework" -optional = false -python-versions = ">=3.8" -files = [ - {file = "cliff-4.3.0-py3-none-any.whl", hash = "sha256:db3dc8774f47db9aa86796921ff158d0f023630261c2746c4fff12584b75f5b2"}, - {file = "cliff-4.3.0.tar.gz", hash = "sha256:fc5b6ebc8fb815332770b2485ee36c09753937c37cce4f3227cdd4e10b33eacc"}, -] - -[package.dependencies] -autopage = ">=0.4.0" -cmd2 = ">=1.0.0" -importlib-metadata = ">=4.4" -PrettyTable = ">=0.7.2" -PyYAML = ">=3.12" -stevedore = ">=2.0.1" - -[[package]] -name = "cmd2" -version = "2.4.3" -description = "cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "cmd2-2.4.3-py3-none-any.whl", hash = "sha256:f1988ff2fff0ed812a2d25218a081b0fa0108d45b48ba2a9272bb98091b654e6"}, - {file = "cmd2-2.4.3.tar.gz", hash = "sha256:71873c11f72bd19e2b1db578214716f0d4f7c8fa250093c601265a9a717dee52"}, -] - -[package.dependencies] -attrs = ">=16.3.0" -pyperclip = ">=1.6" -pyreadline3 = {version = "*", markers = "sys_platform == \"win32\""} -wcwidth = ">=0.1.7" - -[package.extras] -dev = ["codecov", "doc8", "flake8", "invoke", "mypy", "nox", "pytest (>=4.6)", "pytest-cov", "pytest-mock", "sphinx", "sphinx-autobuild", "sphinx-rtd-theme", "twine (>=1.11)"] -test = ["codecov", "coverage", "gnureadline", "pytest (>=4.6)", "pytest-cov", "pytest-mock"] -validate = ["flake8", "mypy", "types-pkg-resources"] - [[package]] name = "colorama" version = "0.4.6" @@ -170,25 +100,6 @@ files = [ [package.dependencies] pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} -[[package]] -name = "importlib-metadata" -version = "6.8.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - [[package]] name = "isort" version = "5.12.0" @@ -350,17 +261,6 @@ files = [ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] -[[package]] -name = "pbr" -version = "5.11.1" -description = "Python Build Reasonableness" -optional = false -python-versions = ">=2.6" -files = [ - {file = "pbr-5.11.1-py2.py3-none-any.whl", hash = "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b"}, - {file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"}, -] - [[package]] name = "platformdirs" version = "3.11.0" @@ -376,23 +276,6 @@ files = [ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] -[[package]] -name = "prettytable" -version = "3.9.0" -description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" -optional = false -python-versions = ">=3.8" -files = [ - {file = "prettytable-3.9.0-py3-none-any.whl", hash = "sha256:a71292ab7769a5de274b146b276ce938786f56c31cf7cea88b6f3775d82fe8c8"}, - {file = "prettytable-3.9.0.tar.gz", hash = "sha256:f4ed94803c23073a90620b201965e5dc0bccf1760b7a7eaf3158cab8aaffdf34"}, -] - -[package.dependencies] -wcwidth = "*" - -[package.extras] -tests = ["pytest", "pytest-cov", "pytest-lazy-fixture"] - [[package]] name = "pyaml" version = "23.9.6" @@ -424,16 +307,6 @@ files = [ [package.extras] plugins = ["importlib-metadata"] -[[package]] -name = "pyperclip" -version = "1.8.2" -description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" -optional = false -python-versions = "*" -files = [ - {file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"}, -] - [[package]] name = "pyreadline3" version = "3.4.1" @@ -551,20 +424,6 @@ rich = ">=10.7.0" [package.extras] dev = ["pre-commit"] -[[package]] -name = "stevedore" -version = "5.1.0" -description = "Manage dynamic plugins for Python applications" -optional = false -python-versions = ">=3.8" -files = [ - {file = "stevedore-5.1.0-py3-none-any.whl", hash = "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d"}, - {file = "stevedore-5.1.0.tar.gz", hash = "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"}, -] - -[package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" - [[package]] name = "typer" version = "0.9.0" @@ -597,33 +456,7 @@ files = [ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] -[[package]] -name = "wcwidth" -version = "0.2.8" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -files = [ - {file = "wcwidth-0.2.8-py2.py3-none-any.whl", hash = "sha256:77f719e01648ed600dfa5402c347481c0992263b81a027344f3e1ba25493a704"}, - {file = "wcwidth-0.2.8.tar.gz", hash = "sha256:8705c569999ffbb4f6a87c6d1b80f324bd6db952f5eb0b95bc07517f4c1813d4"}, -] - -[[package]] -name = "zipp" -version = "3.17.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "6c7761768920ef58522d19560d56ce366cca74dc275b4e019acf10838923d63d" +content-hash = "1747400a30523d50e0ccaf55386f60f05b4d6e5e222aeb8a737d4e78b702a210" diff --git a/pyproject.toml b/pyproject.toml index b6d12f7..1c96b3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,11 +20,13 @@ pyxdg = "^0.28" coloredlogs = "^15.0.1" rich = "^13.6.0" rich-click = "^1.6.1" +colorama = "^0.4.6" [tool.poetry.scripts] -iam2 = "iam.cli:run" iam = "iam.cli_click:run" +iam2 = "iam.cli_typer:run" + [tool.poetry.group.dev.dependencies] black = "^23.9.1"