Compare commits
No commits in common. "c7a66a034e7cfe04aaf5f19d84be67c45a0a03d0" and "b902e152435dbac4b6b350e4561e418fd6215203" have entirely different histories.
c7a66a034e
...
b902e15243
9 changed files with 138 additions and 159 deletions
|
|
@ -1,9 +1,3 @@
|
||||||
## 1.2.0a26 (2025-05-26)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- user_data better support for follower variable
|
|
||||||
|
|
||||||
## 1.2.0a25 (2025-05-14)
|
## 1.2.0a25 (2025-05-14)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ requires = ["flit_core >=3.8.0,<4"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "rougail"
|
name = "rougail"
|
||||||
version = "1.2.0a26"
|
version = "1.2.0a25"
|
||||||
authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}]
|
authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
description = "A consistency handling system that was initially designed in the configuration management"
|
description = "A consistency handling system that was initially designed in the configuration management"
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,44 @@ from .config import RougailConfig
|
||||||
from .utils import normalize_family
|
from .utils import normalize_family
|
||||||
from .object_model import CONVERT_OPTION
|
from .object_model import CONVERT_OPTION
|
||||||
from .user_datas import UserDatas
|
from .user_datas import UserDatas
|
||||||
from .tiramisu import tiramisu_display_name
|
|
||||||
from .__version__ import __version__
|
from .__version__ import __version__
|
||||||
|
|
||||||
|
|
||||||
|
def tiramisu_display_name(
|
||||||
|
kls,
|
||||||
|
subconfig,
|
||||||
|
with_quote: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""Replace the Tiramisu display_name function to display path + description"""
|
||||||
|
config_bag = subconfig.config_bag
|
||||||
|
context = config_bag.context
|
||||||
|
values = context.get_values()
|
||||||
|
context_subconfig = context.get_root(config_bag)
|
||||||
|
doc = values.get_information(subconfig, "doc", None)
|
||||||
|
comment = doc if doc and doc != kls.impl_getname() else ""
|
||||||
|
if "{{ identifier }}" in comment and subconfig.identifiers:
|
||||||
|
comment = comment.replace("{{ identifier }}", str(subconfig.identifiers[-1]))
|
||||||
|
path_in_description = values.get_information(
|
||||||
|
context_subconfig, "path_in_description", True
|
||||||
|
)
|
||||||
|
if path_in_description or not comment:
|
||||||
|
comment = f" ({comment})" if comment else ""
|
||||||
|
if path_in_description is False:
|
||||||
|
path = kls.impl_getname()
|
||||||
|
else:
|
||||||
|
path = kls.impl_getpath()
|
||||||
|
if "{{ identifier }}" in path and subconfig.identifiers:
|
||||||
|
path = path.replace(
|
||||||
|
"{{ identifier }}", normalize_family(str(subconfig.identifiers[-1]))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
path = comment
|
||||||
|
comment = ""
|
||||||
|
if with_quote:
|
||||||
|
return f'"{path}"{comment}'
|
||||||
|
return f"{path}{comment}"
|
||||||
|
|
||||||
|
|
||||||
class Rougail(UserDatas):
|
class Rougail(UserDatas):
|
||||||
"""Main Rougail object"""
|
"""Main Rougail object"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
__version__ = "1.2.0a26"
|
__version__ = "1.2.0a25"
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
def display_xmlfiles(xmlfiles: list) -> str:
|
||||||
|
"""The function format xmlfiles informations to generate errors"""
|
||||||
|
if len(xmlfiles) == 1:
|
||||||
|
return '"' + xmlfiles[0] + '"'
|
||||||
|
return '"' + '", "'.join(xmlfiles[:-1]) + '"' + " and " + '"' + xmlfiles[-1] + '"'
|
||||||
|
|
||||||
|
|
||||||
class ConfigError(Exception):
|
class ConfigError(Exception):
|
||||||
"""Standard error for templating"""
|
"""Standard error for templating"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,82 @@ from tiramisu.config import get_common_path
|
||||||
from .utils import get_jinja_variable_to_param, calc_multi_for_type_variable, undefined
|
from .utils import get_jinja_variable_to_param, calc_multi_for_type_variable, undefined
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
from .error import DictConsistencyError, VariableCalculationDependencyError
|
from .error import DictConsistencyError, VariableCalculationDependencyError
|
||||||
from .tiramisu import CONVERT_OPTION
|
|
||||||
|
|
||||||
BASETYPE = Union[StrictBool, StrictInt, StrictFloat, StrictStr, None]
|
BASETYPE = Union[StrictBool, StrictInt, StrictFloat, StrictStr, None]
|
||||||
PROPERTY_ATTRIBUTE = ["frozen", "hidden", "disabled", "mandatory"]
|
PROPERTY_ATTRIBUTE = ["frozen", "hidden", "disabled", "mandatory"]
|
||||||
|
|
||||||
|
|
||||||
|
def convert_boolean(value: str) -> bool:
|
||||||
|
"""Boolean coercion. The Rougail XML may contain srings like `True` or `False`"""
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
value = value.lower()
|
||||||
|
if value == "true":
|
||||||
|
return True
|
||||||
|
elif value == "false":
|
||||||
|
return False
|
||||||
|
elif value in ["", None]:
|
||||||
|
return None
|
||||||
|
raise Exception(_('unknown boolean value "{0}"').format(value))
|
||||||
|
|
||||||
|
|
||||||
|
CONVERT_OPTION = {
|
||||||
|
"string": dict(opttype="StrOption", example="example"),
|
||||||
|
"number": dict(opttype="IntOption", func=int, example=42),
|
||||||
|
"float": dict(opttype="FloatOption", func=float, example=1.42),
|
||||||
|
"boolean": dict(opttype="BoolOption", func=convert_boolean),
|
||||||
|
"secret": dict(opttype="PasswordOption", example="secrets"),
|
||||||
|
"mail": dict(opttype="EmailOption", example="user@example.net"),
|
||||||
|
"unix_filename": dict(opttype="FilenameOption", example="/tmp/myfile.txt"),
|
||||||
|
"date": dict(opttype="DateOption", example="2000-01-01"),
|
||||||
|
"unix_user": dict(opttype="UsernameOption", example="username"),
|
||||||
|
"ip": dict(
|
||||||
|
opttype="IPOption", initkwargs={"allow_reserved": True}, example="1.1.1.1"
|
||||||
|
),
|
||||||
|
"cidr": dict(opttype="IPOption", initkwargs={"cidr": True}, example="1.1.1.0/24"),
|
||||||
|
"netmask": dict(opttype="NetmaskOption", example="255.255.255.0"),
|
||||||
|
"network": dict(opttype="NetworkOption", example="1.1.1.0"),
|
||||||
|
"network_cidr": dict(
|
||||||
|
opttype="NetworkOption", initkwargs={"cidr": True}, example="1.1.1.0/24"
|
||||||
|
),
|
||||||
|
"broadcast": dict(opttype="BroadcastOption", example="1.1.1.255"),
|
||||||
|
"netbios": dict(
|
||||||
|
opttype="DomainnameOption",
|
||||||
|
initkwargs={"type": "netbios", "warnings_only": True},
|
||||||
|
example="example",
|
||||||
|
),
|
||||||
|
"domainname": dict(
|
||||||
|
opttype="DomainnameOption",
|
||||||
|
initkwargs={"type": "domainname", "allow_ip": False},
|
||||||
|
example="example.net",
|
||||||
|
),
|
||||||
|
"hostname": dict(
|
||||||
|
opttype="DomainnameOption",
|
||||||
|
initkwargs={"type": "hostname", "allow_ip": False},
|
||||||
|
example="example",
|
||||||
|
),
|
||||||
|
"web_address": dict(
|
||||||
|
opttype="URLOption",
|
||||||
|
initkwargs={"allow_ip": False, "allow_without_dot": True},
|
||||||
|
example="https://example.net",
|
||||||
|
),
|
||||||
|
"port": dict(
|
||||||
|
opttype="PortOption", initkwargs={"allow_private": True}, example="111"
|
||||||
|
),
|
||||||
|
"mac": dict(opttype="MACOption", example="00:00:00:00:00"),
|
||||||
|
"unix_permissions": dict(
|
||||||
|
opttype="PermissionsOption",
|
||||||
|
initkwargs={"warnings_only": True},
|
||||||
|
func=int,
|
||||||
|
example="644",
|
||||||
|
),
|
||||||
|
"choice": dict(opttype="ChoiceOption", example="a_choice"),
|
||||||
|
"regexp": dict(opttype="RegexpOption"),
|
||||||
|
#
|
||||||
|
"symlink": dict(opttype="SymLinkOption"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_convert_option_types():
|
def get_convert_option_types():
|
||||||
for typ, datas in CONVERT_OPTION.items():
|
for typ, datas in CONVERT_OPTION.items():
|
||||||
obj = getattr(tiramisu, datas["opttype"])
|
obj = getattr(tiramisu, datas["opttype"])
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,10 @@ from importlib.util import (
|
||||||
spec_from_loader as _spec_from_loader,
|
spec_from_loader as _spec_from_loader,
|
||||||
module_from_spec as _module_from_spec,
|
module_from_spec as _module_from_spec,
|
||||||
)
|
)
|
||||||
from unicodedata import normalize, combining
|
|
||||||
from jinja2 import StrictUndefined, DictLoader
|
from jinja2 import StrictUndefined, DictLoader
|
||||||
from jinja2.sandbox import SandboxedEnvironment
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
|
from rougail.object_model import CONVERT_OPTION
|
||||||
|
from rougail.error import display_xmlfiles
|
||||||
from tiramisu import DynOptionDescription, calc_value, function_waiting_for_error
|
from tiramisu import DynOptionDescription, calc_value, function_waiting_for_error
|
||||||
from tiramisu.error import (
|
from tiramisu.error import (
|
||||||
ValueWarning,
|
ValueWarning,
|
||||||
|
|
@ -43,97 +44,12 @@ from tiramisu.error import (
|
||||||
CancelParam,
|
CancelParam,
|
||||||
errors,
|
errors,
|
||||||
)
|
)
|
||||||
|
from .utils import normalize_family
|
||||||
|
from .i18n import _
|
||||||
|
|
||||||
ori_raise_carry_out_calculation_error = errors.raise_carry_out_calculation_error
|
ori_raise_carry_out_calculation_error = errors.raise_carry_out_calculation_error
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .i18n import _
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
# FIXME
|
|
||||||
def _(msg):
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
def display_xmlfiles(xmlfiles: list) -> str:
|
|
||||||
"""The function format xmlfiles informations to generate errors"""
|
|
||||||
if len(xmlfiles) == 1:
|
|
||||||
return '"' + xmlfiles[0] + '"'
|
|
||||||
return '"' + '", "'.join(xmlfiles[:-1]) + '"' + " and " + '"' + xmlfiles[-1] + '"'
|
|
||||||
|
|
||||||
|
|
||||||
def convert_boolean(value: str) -> bool:
|
|
||||||
"""Boolean coercion. The Rougail XML may contain srings like `True` or `False`"""
|
|
||||||
if isinstance(value, bool):
|
|
||||||
return value
|
|
||||||
value = value.lower()
|
|
||||||
if value == "true":
|
|
||||||
return True
|
|
||||||
elif value == "false":
|
|
||||||
return False
|
|
||||||
elif value in ["", None]:
|
|
||||||
return None
|
|
||||||
raise Exception(_('unknown boolean value "{0}"').format(value))
|
|
||||||
|
|
||||||
|
|
||||||
CONVERT_OPTION = {
|
|
||||||
"string": dict(opttype="StrOption", example="example"),
|
|
||||||
"number": dict(opttype="IntOption", func=int, example=42),
|
|
||||||
"float": dict(opttype="FloatOption", func=float, example=1.42),
|
|
||||||
"boolean": dict(opttype="BoolOption", func=convert_boolean),
|
|
||||||
"secret": dict(opttype="PasswordOption", example="secrets"),
|
|
||||||
"mail": dict(opttype="EmailOption", example="user@example.net"),
|
|
||||||
"unix_filename": dict(opttype="FilenameOption", example="/tmp/myfile.txt"),
|
|
||||||
"date": dict(opttype="DateOption", example="2000-01-01"),
|
|
||||||
"unix_user": dict(opttype="UsernameOption", example="username"),
|
|
||||||
"ip": dict(
|
|
||||||
opttype="IPOption", initkwargs={"allow_reserved": True}, example="1.1.1.1"
|
|
||||||
),
|
|
||||||
"cidr": dict(opttype="IPOption", initkwargs={"cidr": True}, example="1.1.1.0/24"),
|
|
||||||
"netmask": dict(opttype="NetmaskOption", example="255.255.255.0"),
|
|
||||||
"network": dict(opttype="NetworkOption", example="1.1.1.0"),
|
|
||||||
"network_cidr": dict(
|
|
||||||
opttype="NetworkOption", initkwargs={"cidr": True}, example="1.1.1.0/24"
|
|
||||||
),
|
|
||||||
"broadcast": dict(opttype="BroadcastOption", example="1.1.1.255"),
|
|
||||||
"netbios": dict(
|
|
||||||
opttype="DomainnameOption",
|
|
||||||
initkwargs={"type": "netbios", "warnings_only": True},
|
|
||||||
example="example",
|
|
||||||
),
|
|
||||||
"domainname": dict(
|
|
||||||
opttype="DomainnameOption",
|
|
||||||
initkwargs={"type": "domainname", "allow_ip": False},
|
|
||||||
example="example.net",
|
|
||||||
),
|
|
||||||
"hostname": dict(
|
|
||||||
opttype="DomainnameOption",
|
|
||||||
initkwargs={"type": "hostname", "allow_ip": False},
|
|
||||||
example="example",
|
|
||||||
),
|
|
||||||
"web_address": dict(
|
|
||||||
opttype="URLOption",
|
|
||||||
initkwargs={"allow_ip": False, "allow_without_dot": True},
|
|
||||||
example="https://example.net",
|
|
||||||
),
|
|
||||||
"port": dict(
|
|
||||||
opttype="PortOption", initkwargs={"allow_private": True}, example="111"
|
|
||||||
),
|
|
||||||
"mac": dict(opttype="MACOption", example="00:00:00:00:00"),
|
|
||||||
"unix_permissions": dict(
|
|
||||||
opttype="PermissionsOption",
|
|
||||||
initkwargs={"warnings_only": True},
|
|
||||||
func=int,
|
|
||||||
example="644",
|
|
||||||
),
|
|
||||||
"choice": dict(opttype="ChoiceOption", example="a_choice"),
|
|
||||||
"regexp": dict(opttype="RegexpOption"),
|
|
||||||
#
|
|
||||||
"symlink": dict(opttype="SymLinkOption"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def raise_carry_out_calculation_error(subconfig, *args, **kwargs):
|
def raise_carry_out_calculation_error(subconfig, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
ori_raise_carry_out_calculation_error(subconfig, *args, **kwargs)
|
ori_raise_carry_out_calculation_error(subconfig, *args, **kwargs)
|
||||||
|
|
@ -206,52 +122,6 @@ def load_functions(path, dict_func=None):
|
||||||
dict_func[function] = getattr(func_, function)
|
dict_func[function] = getattr(func_, function)
|
||||||
|
|
||||||
|
|
||||||
def normalize_family(family_name: str) -> str:
|
|
||||||
"""replace space, accent, uppercase, ... by valid character"""
|
|
||||||
if not family_name:
|
|
||||||
return
|
|
||||||
family_name = family_name.lower()
|
|
||||||
family_name = family_name.replace("-", "_").replace(" ", "_").replace(".", "_")
|
|
||||||
nfkd_form = normalize("NFKD", family_name)
|
|
||||||
family_name = "".join([c for c in nfkd_form if not combining(c)])
|
|
||||||
return family_name.lower()
|
|
||||||
|
|
||||||
|
|
||||||
def tiramisu_display_name(
|
|
||||||
kls,
|
|
||||||
subconfig,
|
|
||||||
with_quote: bool = False,
|
|
||||||
) -> str:
|
|
||||||
"""Replace the Tiramisu display_name function to display path + description"""
|
|
||||||
config_bag = subconfig.config_bag
|
|
||||||
context = config_bag.context
|
|
||||||
values = context.get_values()
|
|
||||||
context_subconfig = context.get_root(config_bag)
|
|
||||||
doc = values.get_information(subconfig, "doc", None)
|
|
||||||
comment = doc if doc and doc != kls.impl_getname() else ""
|
|
||||||
if "{{ identifier }}" in comment and subconfig.identifiers:
|
|
||||||
comment = comment.replace("{{ identifier }}", str(subconfig.identifiers[-1]))
|
|
||||||
path_in_description = values.get_information(
|
|
||||||
context_subconfig, "path_in_description", True
|
|
||||||
)
|
|
||||||
if path_in_description or not comment:
|
|
||||||
comment = f" ({comment})" if comment else ""
|
|
||||||
if path_in_description is False:
|
|
||||||
path = kls.impl_getname()
|
|
||||||
else:
|
|
||||||
path = kls.impl_getpath()
|
|
||||||
if "{{ identifier }}" in path and subconfig.identifiers:
|
|
||||||
path = path.replace(
|
|
||||||
"{{ identifier }}", normalize_family(str(subconfig.identifiers[-1]))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
path = comment
|
|
||||||
comment = ""
|
|
||||||
if with_quote:
|
|
||||||
return f'"{path}"{comment}'
|
|
||||||
return f"{path}{comment}"
|
|
||||||
|
|
||||||
|
|
||||||
def rougail_calc_value(*args, __default_value=None, __internal_multi=False, **kwargs):
|
def rougail_calc_value(*args, __default_value=None, __internal_multi=False, **kwargs):
|
||||||
values = calc_value(*args, **kwargs)
|
values = calc_value(*args, **kwargs)
|
||||||
if values is None and __internal_multi:
|
if values is None and __internal_multi:
|
||||||
|
|
|
||||||
|
|
@ -218,9 +218,6 @@ class UserDatas:
|
||||||
if not isinstance(value, list) or index >= len(value):
|
if not isinstance(value, list) or index >= len(value):
|
||||||
continue
|
continue
|
||||||
value = value[index]
|
value = value[index]
|
||||||
option_without_index = self.config.option(path)
|
|
||||||
else:
|
|
||||||
option_without_index = option
|
|
||||||
if option.isleader():
|
if option.isleader():
|
||||||
len_leader = len(option.value.get())
|
len_leader = len(option.value.get())
|
||||||
if len_leader:
|
if len_leader:
|
||||||
|
|
@ -228,26 +225,21 @@ class UserDatas:
|
||||||
option.value.pop(idx)
|
option.value.pop(idx)
|
||||||
try:
|
try:
|
||||||
option.value.set(value)
|
option.value.set(value)
|
||||||
value_is_set = True
|
option.information.set(
|
||||||
except Exception:
|
|
||||||
if path != option.path():
|
|
||||||
self.values[option.path()] = self.values.pop(path)
|
|
||||||
else:
|
|
||||||
if "source" in self.values[path]:
|
|
||||||
option_without_index.information.set(
|
|
||||||
"loaded_from", _("loaded from {0}").format(self.values[path]["source"])
|
"loaded_from", _("loaded from {0}").format(self.values[path]["source"])
|
||||||
)
|
)
|
||||||
|
value_is_set = True
|
||||||
# value is correctly set, remove variable to the set
|
# value is correctly set, remove variable to the set
|
||||||
if index is not None:
|
if index is not None:
|
||||||
# if it's a follower waiting for all followers are sets
|
# if it's a follower waiting for all followers are sets
|
||||||
self.values[path]["values"][index] = undefined
|
self.values[path]["values"][index] = undefined
|
||||||
for tmp_value in self.values[path]["values"]:
|
if set(self.values[path]["values"]) == {undefined}:
|
||||||
if tmp_value != undefined:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self.values.pop(path)
|
self.values.pop(path)
|
||||||
else:
|
else:
|
||||||
self.values.pop(path)
|
self.values.pop(path)
|
||||||
|
except Exception:
|
||||||
|
if path != option.path():
|
||||||
|
self.values[option.path()] = self.values.pop(path)
|
||||||
if not value_is_set:
|
if not value_is_set:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
from unicodedata import normalize, combining
|
||||||
import re
|
import re
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
|
|
@ -57,6 +58,17 @@ def valid_variable_family_name(
|
||||||
raise DictConsistencyError(msg, 76, xmlfiles)
|
raise DictConsistencyError(msg, 76, xmlfiles)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_family(family_name: str) -> str:
|
||||||
|
"""replace space, accent, uppercase, ... by valid character"""
|
||||||
|
if not family_name:
|
||||||
|
return
|
||||||
|
family_name = family_name.lower()
|
||||||
|
family_name = family_name.replace("-", "_").replace(" ", "_").replace(".", "_")
|
||||||
|
nfkd_form = normalize("NFKD", family_name)
|
||||||
|
family_name = "".join([c for c in nfkd_form if not combining(c)])
|
||||||
|
return family_name.lower()
|
||||||
|
|
||||||
|
|
||||||
def load_modules(name, module) -> List[str]:
|
def load_modules(name, module) -> List[str]:
|
||||||
"""list all functions in a module"""
|
"""list all functions in a module"""
|
||||||
loader = SourceFileLoader(name, module)
|
loader = SourceFileLoader(name, module)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue