feat: exporter to console

This commit is contained in:
egarette@silique.fr 2024-11-27 16:50:48 +01:00
parent a1113ddff2
commit 3283d85829
12 changed files with 208 additions and 377 deletions

View file

@ -1,2 +1,2 @@
# rougail-exporter # rougail-output-console

View file

@ -5,7 +5,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2024-11-01 11:03+0100\n" "POT-Creation-Date: 2024-11-27 16:46+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -15,43 +15,43 @@ msgstr ""
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"
#: src/rougail/output_exporter/__init__.py:73 #: src/rougail/output_console/__init__.py:82
msgid "The following variables are mandatory but have no value:" msgid "The following variables are mandatory but have no value:"
msgstr "" msgstr ""
#: src/rougail/output_exporter/__init__.py:84 #: src/rougail/output_console/__init__.py:93
msgid "The following variables are inaccessible but are empty and mandatory :" msgid "The following variables are inaccessible but are empty and mandatory :"
msgstr "" msgstr ""
#: src/rougail/output_exporter/output/console.py:53 #: src/rougail/output_console/__init__.py:170
msgid "Undocumented variable" msgid "Undocumented variable"
msgstr "" msgstr ""
#: src/rougail/output_exporter/output/console.py:54 #: src/rougail/output_console/__init__.py:171
msgid "Undocumented but modified variable" msgid "Undocumented but modified variable"
msgstr "" msgstr ""
#: src/rougail/output_exporter/output/console.py:57 #: src/rougail/output_console/__init__.py:174
msgid "Unmodifiable variable" msgid "Unmodifiable variable"
msgstr "" msgstr ""
#: src/rougail/output_exporter/output/console.py:61 #: src/rougail/output_console/__init__.py:178
msgid "Default value" msgid "Default value"
msgstr "" msgstr ""
#: src/rougail/output_exporter/output/console.py:62 #: src/rougail/output_console/__init__.py:179
msgid "Modified value" msgid "Modified value"
msgstr "" msgstr ""
#: src/rougail/output_exporter/output/console.py:63 #: src/rougail/output_console/__init__.py:180
msgid "Original default value" msgid "Original default value"
msgstr "" msgstr ""
#: src/rougail/output_exporter/output/console.py:67 #: src/rougail/output_console/__init__.py:184
msgid "Caption" msgid "Caption"
msgstr "" msgstr ""
#: src/rougail/output_exporter/output/console.py:92 #: src/rougail/output_console/__init__.py:209
msgid "Variables:" msgid "Variables:"
msgstr "" msgstr ""

View file

@ -3,11 +3,11 @@ build-backend = "flit_core.buildapi"
requires = ["flit_core >=3.8.0,<4"] requires = ["flit_core >=3.8.0,<4"]
[project] [project]
name = "rougail.output_exporter" name = "rougail.output_console"
version = "0.1.0" version = "0.1.0"
authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}] authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}]
readme = "README.md" readme = "README.md"
description = "Rougail output exporter" description = "Rougail output console"
requires-python = ">=3.8" requires-python = ">=3.8"
license = {file = "LICENSE"} license = {file = "LICENSE"}
classifiers = [ classifiers = [
@ -30,12 +30,12 @@ dependencies = [
] ]
[project.urls] [project.urls]
Home = "https://forge.cloud.silique.fr/stove/rougail-output-exporter" Home = "https://forge.cloud.silique.fr/stove/rougail-output-console"
[tool.commitizen] [tool.commitizen]
name = "cz_conventional_commits" name = "cz_conventional_commits"
tag_format = "$version" tag_format = "$version"
version_scheme = "pep440" version_scheme = "pep440"
version_provider = "pep621" version_provider = "pep621"
#update_changelog_on_bump = true update_changelog_on_bump = true
changelog_merge_prerelease = true changelog_merge_prerelease = true

View file

@ -17,19 +17,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
from typing import Any, List, Optional from typing import Any, List, Optional
from rich.tree import Tree from rich.tree import Tree
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
from rich.panel import Panel from rich.panel import Panel
from tiramisu import undefined from tiramisu import undefined
from ...i18n import _ from tiramisu.error import PropertiesOptionError, ConfigError
from .i18n import _
class Formater: class RougailOutputConsole:
name = "console"
level = 10
variable_hidden_color = "orange1" variable_hidden_color = "orange1"
variable_advanced_color = "bright_blue" variable_advanced_color = "bright_blue"
variable_advanced_and_modified_color = "red1" variable_advanced_and_modified_color = "red1"
@ -38,13 +37,131 @@ class Formater:
def __init__( def __init__(
self, self,
rougailconfig: "RougailConfig", config: "Config",
rougailconfig: "RougailConfig" = None,
user_data_errors: Optional[list] = None,
user_data_warnings: Optional[list] = None,
) -> None: ) -> None:
self.console = Console(force_terminal=True) if rougailconfig is None:
from rougail import RougailConfig
rougailconfig = RougailConfig
self.rougailconfig = rougailconfig self.rougailconfig = rougailconfig
self.read_write = self.rougailconfig["exporter.read_write"] self.config = config
self.show_secrets = self.rougailconfig["exporter.show_secrets"] self.read_write = self.rougailconfig["console.read_write"]
self.is_mandatory = self.rougailconfig["console.mandatory"]
self.show_secrets = self.rougailconfig["console.show_secrets"]
self.errors = []
self.warnings = []
if user_data_errors is None:
user_data_errors = []
self.user_data_errors = user_data_errors
if user_data_warnings is None:
user_data_warnings = []
self.user_data_warnings = user_data_warnings
self.console = Console(force_terminal=True)
self.out = [] self.out = []
self.root = self.get_root()
def mandatory(self):
if not self.is_mandatory:
return
title = False
options_with_error = []
try:
mandatories = self.config.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.config.property.read_write()
self.mandatory()
if self.read_write:
self.config.property.read_write()
else:
self.config.property.read_only()
errors = self.user_data_errors + self.errors
if errors:
self.display_errors(errors)
if self.errors:
return False
warnings = self.user_data_warnings + self.warnings
if warnings:
self.display_warnings(warnings)
self.header()
self.parse_options(
self.config,
self.root,
)
self.end()
return True
def run(self) -> None:
self.exporter()
return self.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)
leader_values = leader.value.get()
for idx, leader_value in enumerate(leader_values):
leader_obj = parent.add_family(leader)
leader_obj.add_variable(
leader,
value=leader_value,
leader_index=idx,
)
for follower in followers:
if follower.index() != idx:
continue
leader_obj.add_variable(follower)
def header(self): def header(self):
header_variable = "Variable\n" header_variable = "Variable\n"
@ -66,7 +183,7 @@ class Formater:
header.add_row(header_variable, header_value) header.add_row(header_variable, header_value)
self.out.append(Panel.fit(header, title=_("Caption"))) self.out.append(Panel.fit(header, title=_("Caption")))
def errors( def display_errors(
self, self,
errors, errors,
) -> None: ) -> None:
@ -78,7 +195,7 @@ class Formater:
tree.add(error) tree.add(error)
self.out.append(tree) self.out.append(tree)
def warnings( def display_warnings(
self, self,
warnings: list, warnings: list,
) -> None: ) -> None:
@ -87,7 +204,7 @@ class Formater:
tree.add(warning) tree.add(warning)
self.out.append(tree) self.out.append(tree)
def root(self) -> None: def get_root(self) -> None:
self.output = OutputFamily( self.output = OutputFamily(
_("Variables:"), _("Variables:"),
None, None,
@ -129,68 +246,6 @@ class OutputFamily:
) )
self.root = root 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( def add_family(
self, self,
option, option,
@ -316,3 +371,9 @@ class OutputFamily:
if not self.root.show_secrets and option.type() == "password": if not self.root.show_secrets and option.type() == "password":
return "*" * 10 return "*" * 10
return str(value) return str(value)
RougailOutput = RougailOutputConsole
__all__ = ("RougailOutputConsole",)

View file

@ -0,0 +1,56 @@
"""
Silique (https://www.silique.fr)
Copyright (C) 2024
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by the
Free Software Foundation, either version 3 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 Lesser General Public License for more
details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from pathlib import Path
def get_rougail_config(
*,
backward_compatibility=True,
) -> dict:
options = """
console:
description: Configuration rougail-console
disabled:
type: jinja
jinja: |
{% if step.output != 'console' %}
disabled
{% endif %}
read_write:
description: Display variables available in read_write mode
negative_description: Display variables available in read_only mode
default: false
show_secrets:
description: Show secrets instead of obscuring them
negative_description: Obscuring secrets instead of show them
default: false
mandatory:
description: Test mandatories variable before display in console
negative_description: Do not test mandatories variable before display in console
default: true
"""
return {
"name": "console",
"process": "output",
"options": options,
"level": 40,
}
__all__ = ("get_rougail_config",)

View file

@ -19,6 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
from gettext import translation from gettext import translation
from pathlib import Path from pathlib import Path
t = translation("rougail_output_exporter", str(Path(__file__).parent / "locale"), fallback=True) t = translation("rougail_output_console", str(Path(__file__).parent / "locale"), fallback=True)
_ = t.gettext _ = t.gettext

View file

@ -1,164 +0,0 @@
"""
Silique (https://www.silique.fr)
Copyright (C) 2022-2024
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by the
Free Software Foundation, either version 3 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 Lesser General Public License for more
details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from typing import Optional
from tiramisu.error import PropertiesOptionError, ConfigError
from .config import OutPuts
from .i18n import _
class RougailOutputExporter:
def __init__(
self,
config: "Config",
rougailconfig: "RougailConfig" = None,
user_data_errors: Optional[list] = None,
user_data_warnings: Optional[list] = None,
) -> None:
if rougailconfig is None:
from rougail import RougailConfig
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.config = config
self.read_write = self.rougailconfig["exporter.read_write"]
self.errors = []
self.warnings = []
if user_data_errors is None:
user_data_errors = []
self.user_data_errors = user_data_errors
if user_data_warnings is None:
user_data_warnings = []
self.user_data_warnings = user_data_warnings
self.formater = outputs[output](self.rougailconfig)
self.root = self.formater.root()
def mandatory(self):
if not self.rougailconfig["exporter.mandatory"]:
return
title = False
options_with_error = []
try:
mandatories = self.config.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.config.property.read_write()
self.mandatory()
if self.read_write:
self.config.property.read_write()
else:
self.config.property.read_only()
errors = self.user_data_errors + self.errors
if errors:
self.formater.errors(errors)
if self.errors:
return False
warnings = self.user_data_warnings + self.warnings
if warnings:
self.formater.warnings(warnings)
self.formater.header()
self.parse_options(
self.config,
self.root,
)
self.formater.end()
return True
def print(self) -> None:
return self.formater.print()
def run(self) -> None:
self.exporter()
return self.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)
leader_values = leader.value.get()
for idx, leader_value in enumerate(leader_values):
leader_obj = parent.add_family(leader)
leader_obj.add_variable(
leader,
value=leader_value,
leader_index=idx,
)
for follower in followers:
if follower.index() != idx:
continue
leader_obj.add_variable(follower)
RougailOutput = RougailOutputExporter
__all__ = ("RougailOutputExporter",)

View file

@ -1,108 +0,0 @@
"""
Config file for Rougail-exporter
Silique (https://www.silique.fr)
Copyright (C) 2024
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by the
Free Software Foundation, either version 3 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 Lesser General Public License for more
details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from pathlib import Path
from rougail.utils import load_modules
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 variables available in read_write mode
negative_description: Display variables available in read_only mode
alternative_name: er
default: false
show_secrets:
description: Show secrets instead of obscuring them
negative_description: Obscuring secrets instead of show them
alternative_name: es
default: false
mandatory:
description: Test mandatories variable before export
negative_description: Do not test mandatories variable before export
alternative_name: em
default: true
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")

View file

@ -1,14 +0,0 @@
"""
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by the
Free Software Foundation, either version 3 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 Lesser General Public License for more
details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""