diff --git a/src/rougail/output_exporter/__init__.py b/src/rougail/output_exporter/__init__.py new file mode 100644 index 0000000..4d60b54 --- /dev/null +++ b/src/rougail/output_exporter/__init__.py @@ -0,0 +1,127 @@ +""" +Silique (https://www.silique.fr) +Copyright (C) 2022-2024 + +distribued with GPL-2 or later license + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from tiramisu.error import PropertiesOptionError, ConfigError +from rougail import RougailConfig +from .config import OutPuts +from .utils import _ + + +class RougailOutputExporter: + def __init__(self, + conf: 'Config', + rougailconfig: RougailConfig=None, + ) -> None: + if rougailconfig is None: + rougailconfig = RougailConfig + outputs = OutPuts().get() + output = rougailconfig['exporter.output_format'] + if output not in outputs: + raise Exception(f'cannot find output "{output}", available outputs: {list(outputs)}') + self.rougailconfig = rougailconfig + self.conf = conf + self.read_write = self.rougailconfig['exporter.read_write'] + self.errors = [] + self.warnings = [] + self.formater = outputs[output](self.rougailconfig) + self.root = self.formater.root() + + def mandatory(self): + if self.rougailconfig['exporter.no_mandatory']: + return + title = False + options_with_error = [] + try: + mandatories = self.conf.value.mandatory() + except (ConfigError, PropertiesOptionError) as err: + self.errors.append(f'Error in config: {err}') + return + for option in mandatories: + try: + option.value.get() + if not title: + #self.errors.append("Les variables suivantes sont obligatoires mais n'ont pas de valeur :") + self.errors.append(_("The following variables are mandatory but have no value:")) + title = True + self.errors.append(f' - {option.description()}') + except PropertiesOptionError: + options_with_error.append(option) + if not title: + for idx, option in enumerate(options_with_error): + if not idx: + #self.errors.append("Les variables suivantes sont inaccessibles mais sont vides et obligatoires :") + self.errors.append(_("The following variables are inaccessible but are empty and mandatory :")) + self.errors.append(f' - {option.description()}') + + def exporter(self) -> bool: + self.mandatory() + if self.read_write: + self.conf.property.read_write() + else: + self.conf.property.read_only() + if self.errors: + self.formater.errors(self.errors) + return False + if self.warnings: + self.formater.warnings(self.warnings) + self.formater.header() + self.parse_options(self.conf, + self.root, + ) + self.formater.end() + return True + + def print(self) -> None: + return self.formater.print() + + def parse_options(self, + conf, + parent, + ): + for option in conf: + if option.isoptiondescription(): + family = parent.add_family(option) + if option.isleadership(): + self.parse_leadership(option, + family, + ) + else: + self.parse_options(option, + family, + ) + else: + parent.add_variable(option) + + def parse_leadership(self, + conf, + parent, + ): + leader, *followers = list(conf) + idx = -1 + leader_values = leader.value.get() + for follower in followers: + if idx != follower.index(): + idx += 1 + leader_obj = parent.add_family(leader) + leader_obj.add_variable(leader, + value=follower.value.get(), + ) + leader_obj.add_variable(follower) diff --git a/src/rougail/output_exporter/cli.py b/src/rougail/output_exporter/cli.py new file mode 100644 index 0000000..4d29738 --- /dev/null +++ b/src/rougail/output_exporter/cli.py @@ -0,0 +1,40 @@ +""" +Cli code for Rougail-output-exporter + +Silique (https://www.silique.fr) +Copyright (C) 2024 + +distribued with GPL-2 or later license + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" +from . import RougailOutputExporter + + +def run(rougailconfig, + config, + user_data, + ): + export = RougailOutputExporter(config, + rougailconfig, + ) + if user_data: + export.errors = user_data['errors'] + export.warnings = user_data['warnings'] + export.exporter() + export.print() + + +__all__ = ('run',) diff --git a/src/rougail/output_exporter/config.py b/src/rougail/output_exporter/config.py new file mode 100644 index 0000000..6955ad9 --- /dev/null +++ b/src/rougail/output_exporter/config.py @@ -0,0 +1,102 @@ +""" +Config file for Rougail-exporter + +Silique (https://www.silique.fr) +Copyright (C) 2024 + +distribued with GPL-2 or later license + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" +from pathlib import Path +from rougail.utils import load_modules +# from .utils import _ + + +OUTPUTS = None + + +def get_outputs() -> None: + module_name = 'rougail.output_exporter.output' + outputs = {} + for path in (Path(__file__).parent / 'output').iterdir(): + name = path.name + if not name.endswith(".py") or name.endswith("__.py"): + continue + module = load_modules(module_name + '.' + name, str(path)) + if "Formater" not in dir(module): + continue + level = module.Formater.level + if level in outputs: + raise Exception(f'duplicated level rougail-exporter for output "{level}": {module.Formater.name} and {outputs[level].name}') + outputs[module.Formater.level] = module.Formater + return {outputs[level].name: outputs[level] for level in sorted(outputs)} + + +class OutPuts: # pylint: disable=R0903 + """Transformations applied on a object instance""" + + def __init__( + self, + ) -> None: + global OUTPUTS + if OUTPUTS is None: + OUTPUTS = get_outputs() + + def get(self) -> dict: + return OUTPUTS + + +def get_rougail_config(*, + backward_compatibility=True, + ) -> dict: + outputs = tuple(OutPuts().get()) + options = """ +exporter: + description: Configuration rougail-exporter + disabled: + type: jinja + jinja: | + {% if step.output != 'exporter' %} + disabled + {% endif %} + read_write: + description: Display only variables available in read_write mode + alternative_name: er + default: false + show_secrets: + description: Show secrets instead of obscuring them + alternative_name: es + default: false + no_mandatory: + description: Do not test mandatories variable before export + alternative_name: em + default: false + output_format: + description: Generate document in format + alternative_name: eo + default: DEFAULT + choices: +""".replace('DEFAULT', outputs[0]) + for output in outputs: + options += f" - {output}\n" + return {'name': 'exporter', + 'process': 'output', + 'options': options, + 'level': 40, + } + + +__all__ = ("OutPuts", 'get_rougail_config') diff --git a/src/rougail/output_exporter/output/__init__.py b/src/rougail/output_exporter/output/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/rougail/output_exporter/output/console.py b/src/rougail/output_exporter/output/console.py new file mode 100644 index 0000000..1f18727 --- /dev/null +++ b/src/rougail/output_exporter/output/console.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +""" +Silique (https://www.silique.fr) +Copyright (C) 2022-2024 + +distribued with GPL-2 or later license + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from typing import Any, List + +from rich.tree import Tree +from rich.console import Console +from rich.table import Table +from rich.panel import Panel + +from tiramisu import undefined + +def echo(msg): + return msg +_ = echo + +class Formater: + name = 'console' + level = 10 + variable_hidden_color = 'orange1' + variable_advanced_color = 'bright_blue' + variable_advanced_and_modified_color = 'red1' + value_unmodified_color = 'gold1' + value_default_color = 'green' + + def __init__(self, + rougailconfig: 'RougailConfig', + ) -> None: + self.console = Console(force_terminal=True) + self.rougailconfig = rougailconfig + self.read_write = self.rougailconfig['exporter.read_write'] + self.show_secrets = self.rougailconfig['exporter.show_secrets'] + self.out = [] + + def header(self): + header_variable = 'Variable\n' + #header_variable += f'[{self.variable_advanced_color}]Variable non documentée[/{self.variable_advanced_color}]\n' + #header_variable += f'[{self.variable_advanced_and_modified_color}]Variable non documentée mais modifiée[/{self.variable_advanced_and_modified_color}]' + header_variable += f'[{self.variable_advanced_color}]{_("Undocumented variable")}[/{self.variable_advanced_color}]\n' + header_variable += f'[{self.variable_advanced_and_modified_color}]{_("Undocumented but modified variable")}[/{self.variable_advanced_and_modified_color}]' + if not self.read_write: + #header_variable += f'\n[{self.variable_hidden_color}]Variable non modifiable[/{self.variable_hidden_color}]' + header_variable += f'\n[{self.variable_hidden_color}]{_("Unmodifiable variable")}[/{self.variable_hidden_color}]' + #header_value = f'[{self.value_unmodified_color}]Valeur par défaut[/{self.value_unmodified_color}]\n' + #header_value += 'Valeur modifiée\n' + #header_value += f'([{self.value_default_color}]Valeur par défaut originale[/{self.value_default_color}])' + header_value = f'[{self.value_unmodified_color}]{_("Default value")}[/{self.value_unmodified_color}]\n' + header_value += _('Modified value') + '\n' + header_value += f'([{self.value_default_color}]{_("Original default value")}[/{self.value_default_color}])' + header = Table.grid(padding=1, collapse_padding=True) + header.pad_edge = False + header.add_row(header_variable, header_value) + self.out.append(Panel.fit(header, title=_("Caption"))) + + def errors(self, + errors, + ) -> None: + tree = Tree(":stop_sign: ERRORS", + guide_style="bold bright_red", + ) + for error in errors: + tree.add(error) + self.out.append(tree) + + def warnings(self, + warnings: list, + ) -> None: + tree = Tree(":warning: WARNINGS") + for warning in warnings: + tree.add(warning) + self.out.append(tree) + + def root(self) -> None: + self.output = OutputFamily(_("Variables:"), + None, + self, + no_icon=True, + ) + return self.output + + def end(self): + self.out.append(self.output.tree) + + def print(self): + for out in self.out: + self.console.print(out) + + +class OutputFamily: + def __init__(self, + family, + parent, + root, + *, + is_leader: bool=False, + no_icon: bool=False + ) -> None: + if parent is None: + tree = Tree + else: + tree = parent.add + if is_leader: + self.tree = tree(f":notebook: {family} :", + guide_style="bold bright_blue", + ) + elif no_icon: + self.tree = tree(f"{family}", + guide_style="bold bright_blue", + ) + else: + self.tree = tree(f":open_file_folder: {family}", + guide_style="bold bright_blue", + ) + self.root = root +# +# def parse_option(self, +# option, +# value, +# variables, +# ): +# if '.' in line: +# # it's a dict +# family, variable = line.split('.', 1) +# current_path = parent_path +# if current_path: +# current_path += '.' +# current_path += family +# if for_doc: +# if 'hidden' in self.conf.option(current_path).property.get() or family_hidden: +# family_hidden = True +# family = f'[orange1]{family}[/orange1]' +# elif 'advanced' in self.conf.option(current_path).property.get(): +# family = f'[bright_blue]{family}[/bright_blue]' +# if '.' not in variable and self.conf.option(full_path.rsplit('.', 1)[0]).isleadership(): +# dico.setdefault(family, []) +# leadership = True +# else: +# dico.setdefault(family, {}) +# leadership = False +# self.parse_option(full_path, +# variable, +# value, +# ) +# elif leadership: +# # it's a leadership +# for idx, val in enumerate(value): +# dic = {k.rsplit('.', 1)[-1]: v for k, v in val.items()} +# if for_doc: +# leader = True +# for k, v in val.items(): +# if leader: +# is_default = self.conf.option(k).owner.isdefault() +# properties = self.conf.option(k).property.get() +# else: +# is_default = self.conf.option(k, idx).owner.isdefault() +# properties = self.conf.option(k, idx).property.get() +# if self.conf.option(k).type() == _('password') and not self.args.show_secrets: +# v = "*" * 10 +# subpath = k.rsplit('.', 1)[-1] +# if 'hidden' in properties or family_hidden: +# subpath = f'[orange1]{subpath}[/orange1]' +# elif 'advanced' in properties: +# if isdefault: +# subpath = f'[bright_blue]{subpath}[/bright_blue]' +# else: +# subpath = f'[red1]{subpath}[/red1]' +# if is_default: +# v = '[gold1]' + str(v) + '[/gold1]' +# dico.append(f'{subpath}: {v}') +# leader = False +# else: +# dico.append(dic) +# else: +# # it's a variable +# self.parse_variable(option, value) +# + def add_family(self, + option, + ) -> None: + properties = option.property.get() + if 'hidden' in properties: + color = self.root.variable_hidden_color + elif 'advanced' in properties: + color = self.root.variable_advanced_color + else: + color = None + return OutputFamily(self.colorize(None, + option.name(), + color, + None, + ), + self.tree, + self.root, + ) + + def add_variable(self, + option, + value: Any=undefined, + ): + properties = option.property.get() + variable_color = None + if option.owner.isdefault(): + if 'hidden' in properties: + variable_color = self.root.variable_hidden_color + elif 'advanced' in properties: + variable_color = self.root.variable_advanced_color + color = self.root.value_unmodified_color + default_value = None + else: + if 'hidden' in properties: + variable_color = self.root.variable_hidden_color + elif 'advanced' in properties: + variable_color = self.root.variable_advanced_and_modified_color + color = None + default_value = option.value.default() + if value is undefined: + value = option.value.get() + key = self.colorize(None, + option.name(), + variable_color, + None, + ) + value = self.colorize(option, + value, + color, + default_value, + ) + if isinstance(value, list): + subtree = self.tree.add(f":notebook: {key} :", + guide_style="bold bright_blue", + ) + for val in value: + subtree.add(str(val)) + else: + self.tree.add(f":notebook: {key}: {value}") + + def colorize(self, + option, + value, + color: str, + default_value, + ) -> str: + if isinstance(value, list): + if default_value is None: + default_value = [] + len_value = len(value) + len_default_value = len(default_value) + len_values = max(len_value, len_default_value) + ret = [] + for idx in range(len_values): + if idx < len_value: + val = value[idx] + else: + val = '' + if idx < len_default_value: + if val: + val += ' ' + default = default_value[idx] + else: + default = None + ret.append(self.colorize(option, + val, + color, + default, + )) + return ret + if option and value is not None: + value = self.convert_value(option, + value, + ) + else: + value = str(value) + if color is not None: + ret = f'[{color}]{value}[/{color}]' + else: + ret = value + if default_value: + default_value_color = self.root.value_default_color + ret += f' ([{default_value_color}]{default_value}[/{default_value_color}])' + return ret + + def convert_value(self, + option, + value, + ): + if not self.root.show_secrets and option.type() == 'password': + return "*" * 10 + return str(value) diff --git a/src/rougail/output_exporter/utils.py b/src/rougail/output_exporter/utils.py new file mode 100644 index 0000000..3b7552f --- /dev/null +++ b/src/rougail/output_exporter/utils.py @@ -0,0 +1,25 @@ +""" +Silique (https://www.silique.fr) +Copyright (C) 2024 + +distribued with GPL-2 or later license + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + + +def echo(msg): + return msg +_ = echo