""" Helper module with tools to dynamically convert :class:`~notifiers.core.Provider` and :class:`~notifiers.core.ProviderResource` classes to :mod:`click` data types """ from functools import partial import click CORE_COMMANDS = { "required": "'{}' required schema", "schema": "'{}' full schema", "metadata": "'{}' metadata", "defaults": "'{}' default values", } SCHEMA_BASE_MAP = { "string": click.STRING, "integer": click.INT, "number": click.FLOAT, "boolean": click.BOOL, } COMPLEX_TYPES = ["object", "array"] def handle_oneof(oneof_schema: list) -> tuple: """ Custom handle of `oneOf` JSON schema validator. Tried to match primitive type and see if it should be allowed to be passed multiple timns into a command :param oneof_schema: `oneOf` JSON schema :return: Tuple of :class:`click.ParamType`, ``multiple`` flag and ``description`` of option """ oneof_dict = {schema["type"]: schema for schema in oneof_schema} click_type = None multiple = False description = None for key, value in oneof_dict.items(): if key == "array": continue elif key in SCHEMA_BASE_MAP: if oneof_dict.get("array") and oneof_dict["array"]["items"]["type"] == key: multiple = True # Found a match to a primitive type click_type = SCHEMA_BASE_MAP[key] description = value.get("title") break return click_type, multiple, description def json_schema_to_click_type(schema: dict) -> tuple: """ A generic handler of a single property JSON schema to :class:`click.ParamType` converter :param schema: JSON schema property to operate on :return: Tuple of :class:`click.ParamType`, `description`` of option and optionally a :class:`click.Choice` if the allowed values are a closed list (JSON schema ``enum``) """ choices = None if isinstance(schema["type"], list): if "string" in schema["type"]: schema["type"] = "string" click_type = SCHEMA_BASE_MAP[schema["type"]] description = schema.get("title") if schema.get("enum"): # todo handle multi type enums better (or at all) enum = [value for value in schema["enum"] if isinstance(value, str)] choices = click.Choice(enum) return click_type, description, choices def clean_data(data: dict) -> dict: """Removes all empty values and converts tuples into lists""" new_data = {} for key, value in data.items(): # Verify that only explicitly passed args get passed on if not isinstance(value, bool) and not value: continue # Multiple choice command are passed as tuples, convert to list to match schema if isinstance(value, tuple): value = list(value) new_data[key] = value return new_data def params_factory(schema: dict, add_message: bool) -> list: """ Generates list of :class:`click.Option` based on a JSON schema :param schema: JSON schema to operate on :return: Lists of created :class:`click.Option` object to be added to a :class:`click.Command` """ # Immediately create message as an argument params = [] if add_message: params.append(click.Argument(["message"], required=False)) for property, prpty_schema in schema.items(): multiple = False choices = None if any(char in property for char in ["@"]): continue if prpty_schema.get("type") in COMPLEX_TYPES: continue if prpty_schema.get("duplicate"): continue if property == "message": continue elif not prpty_schema.get("oneOf"): click_type, description, choices = json_schema_to_click_type(prpty_schema) else: click_type, multiple, description = handle_oneof(prpty_schema["oneOf"]) # Not all oneOf schema can be handled by click if not click_type: continue # Convert bool values into flags if click_type == click.BOOL: param_decls = [get_flag_param_decals_from_bool(property)] click_type = None else: param_decls = [get_param_decals_from_name(property)] if description: description = description.capitalize() if multiple: if not description.endswith("."): description += "." description += " Multiple usages of this option are allowed" # Construct the base command options option = partial( click.Option, param_decls=param_decls, help=description, multiple=multiple ) if choices: option = option(type=choices) elif click_type: option = option(type=click_type) else: option = option() params.append(option) return params def schema_to_command( p, name: str, callback: callable, add_message: bool ) -> click.Command: """ Generates a ``notify`` :class:`click.Command` for :class:`~notifiers.core.Provider` :param p: Relevant Provider :param name: Command name :return: A ``notify`` :class:`click.Command` """ params = params_factory(p.schema["properties"], add_message=add_message) help = p.__doc__ cmd = click.Command(name=name, callback=callback, params=params, help=help) return cmd def get_param_decals_from_name(option_name: str) -> str: """Converts a name to a param name""" name = option_name.replace("_", "-") return f"--{name}" def get_flag_param_decals_from_bool(option_name: str) -> str: """Return a '--do/not-do' style flag param""" name = option_name.replace("_", "-") return f"--{name}/--no-{name}"