1114 lines
39 KiB
Python
1114 lines
39 KiB
Python
"""Rougail object model
|
|
|
|
Silique (https://www.silique.fr)
|
|
Copyright (C) 2023-2026
|
|
|
|
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 warnings import warn
|
|
from typing import Optional, Union, get_type_hints, Any, Literal, List, Dict, Iterator
|
|
from pydantic import (
|
|
BaseModel,
|
|
StrictBool,
|
|
StrictInt,
|
|
StrictFloat,
|
|
StrictStr,
|
|
ConfigDict,
|
|
)
|
|
import tiramisu
|
|
from ..utils import (
|
|
get_jinja_variable_to_param,
|
|
calc_multi_for_type_variable,
|
|
undefined,
|
|
PROPERTY_ATTRIBUTE,
|
|
)
|
|
from ..i18n import _
|
|
from ..error import DictConsistencyError, VariableCalculationDependencyError, RougailWarning
|
|
from ..tiramisu import CONVERT_OPTION, RENAME_TYPE, display_xmlfiles, convert_boolean
|
|
|
|
BASETYPE = Union[StrictBool, StrictInt, StrictFloat, StrictStr, None]
|
|
|
|
|
|
def get_convert_option_types():
|
|
for typ, datas in CONVERT_OPTION.items():
|
|
typ_description = datas.get("msg", typ)
|
|
obj = getattr(tiramisu, datas["opttype"])
|
|
initkwargs = datas.get("initkwargs", {})
|
|
if obj == tiramisu.SymLinkOption:
|
|
continue
|
|
if obj == tiramisu.ChoiceOption:
|
|
inst = obj("a", "a", ("a",), **initkwargs)
|
|
else:
|
|
inst = obj("a", "a", **initkwargs)
|
|
extra = getattr(inst, "_extra", {})
|
|
if not extra:
|
|
continue
|
|
params = []
|
|
for key, value in extra.items():
|
|
if key.startswith("_"):
|
|
continue
|
|
if "params" in datas and key in datas["params"]:
|
|
multi = datas["params"][key].get('multi', False)
|
|
description = datas["params"][key]["description"]
|
|
choices = datas["params"][key].get("choices")
|
|
else:
|
|
description = None
|
|
choices = None
|
|
multi = False
|
|
if choices:
|
|
key_type = "choice"
|
|
elif isinstance(value, bool):
|
|
key_type = "boolean"
|
|
elif isinstance(value, str):
|
|
key_type = "string"
|
|
elif isinstance(value, list):
|
|
key_type = "string"
|
|
multi = True
|
|
params.append((key, key_type, description, multi, value, choices))
|
|
yield typ, typ_description, params
|
|
|
|
|
|
class Param(BaseModel):
|
|
key: str
|
|
namespace: Optional[str]
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
def __init__(
|
|
self,
|
|
path,
|
|
attribute,
|
|
family_is_dynamic,
|
|
xmlfiles,
|
|
**kwargs,
|
|
) -> None:
|
|
super().__init__(**kwargs)
|
|
|
|
def to_param(
|
|
self, attribute_name, objectspace, path, version, namespace, xmlfiles
|
|
) -> dict:
|
|
return self.model_dump()
|
|
|
|
|
|
class AnyParam(Param):
|
|
type: str
|
|
value: Union[BASETYPE, List[BASETYPE]]
|
|
|
|
|
|
class VariableParam(Param):
|
|
type: str
|
|
variable: str
|
|
propertyerror: bool = True
|
|
whole: bool = False
|
|
# dynamic: bool = True
|
|
optional: bool = False
|
|
|
|
def to_param(
|
|
self, attribute_name, objectspace, path, version, namespace, xmlfiles
|
|
) -> dict:
|
|
param = super().to_param(
|
|
attribute_name, objectspace, path, version, namespace, xmlfiles
|
|
)
|
|
variable, identifier = objectspace.paths.get_with_dynamic(
|
|
param["variable"],
|
|
path,
|
|
version,
|
|
namespace,
|
|
xmlfiles,
|
|
)
|
|
if not variable:
|
|
if not param.get("optional"):
|
|
msg = _(
|
|
'cannot find variable "{0}" defined in attribute "{1}" for "{2}"'
|
|
).format(param["variable"], attribute_name, path)
|
|
raise DictConsistencyError(msg, 22, xmlfiles)
|
|
return None
|
|
if isinstance(variable, objectspace.family):
|
|
msg = _(
|
|
'the variable "{0}" is in fact a family in attribute "{1}" for "{2}"'
|
|
).format(variable["name"], attribute_name, path)
|
|
raise DictConsistencyError(msg, 42, xmlfiles)
|
|
if not isinstance(variable, objectspace.variable):
|
|
msg = _('unknown object "{0}" in attribute "{1}" for "{2}"').format(
|
|
variable, attribute_name, path
|
|
)
|
|
raise DictConsistencyError(msg, 44, xmlfiles)
|
|
param["variable"] = variable
|
|
if identifier:
|
|
param["identifier"] = identifier
|
|
return param
|
|
|
|
|
|
class IdentifierParam(Param):
|
|
type: str
|
|
identifier: Optional[int] = None
|
|
|
|
def __init__(
|
|
self,
|
|
**kwargs,
|
|
) -> None:
|
|
if not kwargs["family_is_dynamic"]:
|
|
msg = _(
|
|
'identifier parameter for "{0}" in "{1}" cannot be set none dynamic family'
|
|
).format(kwargs["attribute"], kwargs["path"])
|
|
raise DictConsistencyError(msg, 10, kwargs["xmlfiles"])
|
|
super().__init__(**kwargs)
|
|
|
|
|
|
class InformationParam(Param):
|
|
type: str
|
|
information: str
|
|
variable: Optional[str] = None
|
|
|
|
def to_param(
|
|
self, attribute_name, objectspace, path, version, namespace, xmlfiles
|
|
) -> dict:
|
|
param = super().to_param(
|
|
attribute_name, objectspace, path, version, namespace, xmlfiles
|
|
)
|
|
if not param["variable"]:
|
|
del param["variable"]
|
|
return param
|
|
variable, identifier = objectspace.paths.get_with_dynamic(
|
|
param["variable"],
|
|
path,
|
|
version,
|
|
namespace,
|
|
xmlfiles,
|
|
)
|
|
if not variable:
|
|
msg = _('cannot find variable "{0}" defined in "{1}" for "{2}"').format(
|
|
param["variable"], attribute_name, path
|
|
)
|
|
raise DictConsistencyError(msg, 14, xmlfiles)
|
|
if identifier:
|
|
msg = _(
|
|
'variable "{0}" defined in "{1}" for "{2}" is a dynamic variable'
|
|
).format(param["variable"], attribute_name, path)
|
|
raise DictConsistencyError(msg, 15, xmlfiles)
|
|
param["variable"] = variable
|
|
return param
|
|
|
|
|
|
class IndexParam(Param):
|
|
type: str
|
|
|
|
def to_param(
|
|
self, attribute_name, objectspace, path, version, namespace, xmlfiles
|
|
) -> dict:
|
|
if path not in objectspace.followers and (
|
|
attribute_name != "validators" or path not in objectspace.multis
|
|
):
|
|
msg = _(
|
|
'the variable "{0}" is not a follower, so cannot have index type for param in "{1}"'
|
|
).format(path, attribute)
|
|
raise DictConsistencyError(msg, 25, xmlfiles)
|
|
return super().to_param(
|
|
attribute_name, objectspace, path, version, namespace, xmlfiles
|
|
)
|
|
|
|
|
|
class NamespaceParam(Param):
|
|
type: str
|
|
namespace: str
|
|
|
|
def to_param(
|
|
self, attribute_name, objectspace, path, version, namespace, xmlfiles
|
|
) -> dict:
|
|
namespace = self.namespace
|
|
if namespace:
|
|
namespace = objectspace.paths[namespace].description
|
|
return {
|
|
"type": "any",
|
|
"value": namespace,
|
|
"key": self.key,
|
|
}
|
|
|
|
|
|
PARAM_TYPES = {
|
|
"any": AnyParam,
|
|
"variable": VariableParam,
|
|
"identifier": IdentifierParam,
|
|
"information": InformationParam,
|
|
"index": IndexParam,
|
|
"namespace": NamespaceParam,
|
|
}
|
|
|
|
|
|
class Calculation(BaseModel):
|
|
# path: str
|
|
inside_list: bool
|
|
version: str
|
|
ori_path: Optional[str] = None
|
|
default_values: Any = None
|
|
namespace: Optional[str]
|
|
warnings: Optional[bool] = None
|
|
description: Optional[StrictStr] = None
|
|
xmlfiles: List[str]
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
def get_params(self, objectspace, path):
|
|
if self.warnings is not None and self.attribute_name != "validators":
|
|
msg = _(
|
|
'"warnings" are only available with attribute "{self.attribute_name}" for variable "{self.path}"'
|
|
)
|
|
raise DictConsistencyError(msg, 83, xmlfiles)
|
|
if not self.params:
|
|
return {}
|
|
params = {}
|
|
for param_obj in self.params:
|
|
param = param_obj.to_param(
|
|
self.attribute_name,
|
|
objectspace,
|
|
path,
|
|
self.version,
|
|
self.namespace,
|
|
self.xmlfiles,
|
|
)
|
|
if param is None:
|
|
continue
|
|
params[param.pop("key")] = param
|
|
return params
|
|
|
|
|
|
class JinjaCalculation(Calculation):
|
|
attribute_name: Literal[
|
|
"frozen",
|
|
"hidden",
|
|
"mandatory",
|
|
"empty",
|
|
"disabled",
|
|
"default",
|
|
"validators",
|
|
"choices",
|
|
"dynamic",
|
|
"secret_manager",
|
|
]
|
|
jinja: StrictStr
|
|
params: Optional[List[Param]] = None
|
|
return_type: BASETYPE = None
|
|
|
|
def _jinja_to_function(
|
|
self,
|
|
function,
|
|
return_type,
|
|
multi,
|
|
objectspace,
|
|
path,
|
|
*,
|
|
add_help=False,
|
|
params: Optional[dict] = None,
|
|
):
|
|
internal_variable = path
|
|
jinja_path = f"{self.attribute_name}_{path}"
|
|
idx = 0
|
|
while jinja_path in objectspace.jinja:
|
|
jinja_path = f"{self.attribute_name}_{path}_{idx}"
|
|
idx += 1
|
|
if self.ori_path is not None:
|
|
path = self.ori_path
|
|
variable = objectspace.paths[path]
|
|
objectspace.jinja[jinja_path] = self.jinja
|
|
if return_type in RENAME_TYPE:
|
|
warning = _('the variable "{0}" has a depreciated return_type "{1}", please use "{2}" instead in {3}')
|
|
warn(
|
|
warning.format(path, return_type, RENAME_TYPE[return_type], display_xmlfiles(self.xmlfiles)),
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return_type = RENAME_TYPE[return_type]
|
|
default = {
|
|
"function": function,
|
|
"params": {
|
|
"__internal_jinja": jinja_path,
|
|
"__internal_type": return_type,
|
|
"__internal_multi": multi,
|
|
"__internal_files": self.xmlfiles,
|
|
"__internal_attribute": self.attribute_name,
|
|
"__internal_variable": internal_variable,
|
|
},
|
|
}
|
|
if self.default_values:
|
|
default["params"]["__default_value"] = self.default_values
|
|
if add_help:
|
|
default["help"] = function + "_help"
|
|
if self.params:
|
|
default["params"] |= self.get_params(objectspace, path)
|
|
if params:
|
|
default["params"] |= params
|
|
if self.warnings:
|
|
default["warnings_only"] = True
|
|
for sub_variable, identifier, true_path in get_jinja_variable_to_param(
|
|
path,
|
|
self.jinja,
|
|
objectspace,
|
|
variable.xmlfiles,
|
|
objectspace.functions,
|
|
self.version,
|
|
self.namespace,
|
|
):
|
|
if true_path in default["params"]:
|
|
continue
|
|
if isinstance(sub_variable, dict):
|
|
default["params"][true_path] = {
|
|
"type": "value",
|
|
"value": sub_variable,
|
|
}
|
|
else:
|
|
default["params"][true_path] = {
|
|
"type": "variable",
|
|
"variable": sub_variable,
|
|
}
|
|
if self.version != "1.0":
|
|
default["params"][true_path]["propertyerror"] = False
|
|
default["params"][true_path]["optional"] = True
|
|
if identifier:
|
|
default["params"][true_path]["identifier"] = identifier
|
|
return default
|
|
|
|
def to_function(
|
|
self,
|
|
objectspace,
|
|
path,
|
|
) -> dict:
|
|
if self.attribute_name in ["default", "secret_manager"]:
|
|
if self.ori_path is not None:
|
|
path = self.ori_path
|
|
if self.return_type:
|
|
raise Exception("return_type not allowed!")
|
|
variable = objectspace.paths[path]
|
|
return_type = variable.type
|
|
if self.inside_list:
|
|
multi = False
|
|
elif path in objectspace.followers:
|
|
multi = objectspace.multis[path] == "submulti"
|
|
else:
|
|
multi = path in objectspace.multis
|
|
return self._jinja_to_function(
|
|
"jinja_to_function",
|
|
return_type,
|
|
multi,
|
|
objectspace,
|
|
path,
|
|
)
|
|
elif self.attribute_name == "validators":
|
|
return_type = self.return_type
|
|
if return_type is None:
|
|
return_type = "string"
|
|
if return_type not in ["string", "boolean"]:
|
|
if self.ori_path is not None:
|
|
path = self.ori_path
|
|
msg = _(
|
|
'variable "{0}" has a calculating "{1}" with an invalid return_type, should be boolean or string, not "{2}"'
|
|
).format(path, self.attribute_name, return_type)
|
|
raise DictConsistencyError(msg, 81, self.xmlfiles)
|
|
if return_type == 'boolean':
|
|
description = self.description
|
|
if description is None:
|
|
if self.ori_path is not None:
|
|
opath = self.ori_path
|
|
else:
|
|
opath = path
|
|
warning = _('the variable "{0}" has a return_type "{1}", for attribute "{2}" but has not description in {3}')
|
|
warn(
|
|
warning.format(opath, return_type, self.attribute_name, display_xmlfiles(self.xmlfiles)),
|
|
RougailWarning,
|
|
)
|
|
self.description = _('value is invalid')
|
|
else:
|
|
description = None
|
|
return self._jinja_to_function(
|
|
"valid_with_jinja",
|
|
return_type,
|
|
False,
|
|
objectspace,
|
|
path,
|
|
params={'description': description},
|
|
)
|
|
elif self.attribute_name in PROPERTY_ATTRIBUTE:
|
|
return_type = self.return_type
|
|
if return_type is None:
|
|
return_type = "string"
|
|
if return_type not in ["string", "boolean"]:
|
|
if self.ori_path is not None:
|
|
path = self.ori_path
|
|
msg = _(
|
|
'variable "{0}" has a calculating "{1}" with an invalid return_type, should be boolean or string, not "{2}"'
|
|
).format(path, self.attribute_name, return_type)
|
|
raise DictConsistencyError(msg, 81, self.xmlfiles)
|
|
description = self.description
|
|
return self._jinja_to_function(
|
|
"jinja_to_property",
|
|
return_type,
|
|
False,
|
|
objectspace,
|
|
path,
|
|
add_help=True,
|
|
params={
|
|
None: [self.attribute_name, description],
|
|
"when": True,
|
|
"inverse": False,
|
|
},
|
|
)
|
|
elif self.attribute_name == "choices":
|
|
return_type = self.return_type
|
|
if return_type is None:
|
|
return_type = "string"
|
|
return self._jinja_to_function(
|
|
"jinja_to_function",
|
|
return_type,
|
|
not self.inside_list,
|
|
objectspace,
|
|
path,
|
|
)
|
|
elif self.attribute_name == "dynamic":
|
|
return_type = self.return_type
|
|
if return_type is None:
|
|
return_type = "string"
|
|
return self._jinja_to_function(
|
|
"jinja_to_function",
|
|
return_type,
|
|
True,
|
|
objectspace,
|
|
path,
|
|
)
|
|
raise Exception("hu?")
|
|
|
|
|
|
class _VariableCalculation(Calculation):
|
|
variable: StrictStr
|
|
propertyerror: bool = True,
|
|
allow_none: bool = False
|
|
optional: bool = False
|
|
|
|
def get_variable(
|
|
self,
|
|
objectspace,
|
|
path,
|
|
) -> "Variable":
|
|
if self.version != "1.0" and objectspace.paths.regexp_relative.search(
|
|
self.variable
|
|
):
|
|
variable_full_path = objectspace.paths.get_full_path(
|
|
self.variable,
|
|
path,
|
|
)
|
|
elif self.version == "1.0" and "{{ suffix }}" in self.variable:
|
|
variable_full_path = self.variable.replace(
|
|
"{{ suffix }}", "{{ identifier }}"
|
|
)
|
|
else:
|
|
variable_full_path = self.variable
|
|
variable, identifier = objectspace.paths.get_with_dynamic(
|
|
variable_full_path,
|
|
path,
|
|
self.version,
|
|
self.namespace,
|
|
self.xmlfiles,
|
|
)
|
|
if variable and not isinstance(variable, objectspace.variable):
|
|
if isinstance(variable, objectspace.family):
|
|
msg = _(
|
|
'a variable "{0}" is needs in attribute "{1}" for "{2}" but it\'s a family'
|
|
).format(variable_full_path, self.attribute_name, path)
|
|
raise DictConsistencyError(msg, 47, self.xmlfiles)
|
|
else:
|
|
msg = _('unknown object "{0}" in attribute "{1}" for "{2}"').format(
|
|
variable, self.attribute_name, path
|
|
)
|
|
raise DictConsistencyError(msg, 48, self.xmlfiles)
|
|
return variable_full_path, variable, identifier
|
|
|
|
def get_params(
|
|
self,
|
|
objectspace,
|
|
path,
|
|
variable_in_calculation_path: str,
|
|
variable_in_calculation: "Variable",
|
|
variable_in_calculation_identifier: Optional[str],
|
|
key: str = None,
|
|
):
|
|
if not variable_in_calculation:
|
|
if not objectspace.force_optional:
|
|
msg = _(
|
|
'variable "{0}" has an attribute "{1}" calculated with the unknown variable "{2}"'
|
|
).format(path, self.attribute_name, self.variable)
|
|
raise DictConsistencyError(msg, 88, self.xmlfiles)
|
|
return {None: [["example"]]}
|
|
param = {
|
|
"type": "variable",
|
|
"variable": variable_in_calculation,
|
|
"propertyerror": self.propertyerror,
|
|
}
|
|
if isinstance(self, VariableCalculation) and self.optional:
|
|
param["optional"] = self.optional
|
|
if variable_in_calculation_identifier:
|
|
param["identifier"] = variable_in_calculation_identifier
|
|
if key:
|
|
params = {key: param}
|
|
else:
|
|
params = {None: [param]}
|
|
if self.default_values:
|
|
params["__default_value"] = self.default_values
|
|
if self.allow_none:
|
|
params["allow_none"] = True
|
|
self.check_multi(
|
|
objectspace, path, variable_in_calculation_path, variable_in_calculation
|
|
)
|
|
if path in objectspace.followers:
|
|
multi = objectspace.multis[path] == "submulti"
|
|
else:
|
|
multi = path in objectspace.multis
|
|
if multi and not self.inside_list:
|
|
params["__internal_multi"] = True
|
|
return params
|
|
|
|
def check_multi(
|
|
self, objectspace, path, variable_in_calculation_path, variable_in_calculation
|
|
):
|
|
local_variable = objectspace.paths[path]
|
|
local_variable_multi, variable_in_calculation_multi = (
|
|
calc_multi_for_type_variable(
|
|
local_variable,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation,
|
|
objectspace,
|
|
)
|
|
)
|
|
if self.attribute_name == "default":
|
|
if variable_in_calculation_multi == "submulti":
|
|
if objectspace.paths.is_dynamic(variable_in_calculation.path):
|
|
msg = _(
|
|
'the variable "{0}" has an invalid "{1}" the variable "{2}" is in a sub dynamic option'
|
|
).format(
|
|
local_variable.path,
|
|
self.attribute_name,
|
|
variable_in_calculation.path,
|
|
)
|
|
raise DictConsistencyError(msg, 69, self.xmlfiles)
|
|
else:
|
|
msg = _(
|
|
'the leader "{0}" has an invalid "{1}" the follower "{2}" is a multi'
|
|
).format(
|
|
local_variable.path,
|
|
self.attribute_name,
|
|
variable_in_calculation.path,
|
|
)
|
|
raise DictConsistencyError(msg, 74, self.xmlfiles)
|
|
if not self.inside_list:
|
|
if local_variable_multi != variable_in_calculation_multi:
|
|
if local_variable_multi:
|
|
self.check_variable_in_calculation_multi(
|
|
local_variable.path,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation_multi,
|
|
)
|
|
self.check_variable_in_calculation_not_multi(
|
|
local_variable.path,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation_multi,
|
|
)
|
|
else:
|
|
self.check_variable_in_calculation_in_list_not_multi(
|
|
local_variable.path,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation_multi,
|
|
)
|
|
elif self.attribute_name in ["choices", "dynamic"]:
|
|
# calculated variable must be a multi
|
|
if not self.inside_list:
|
|
self.check_variable_in_calculation_multi(
|
|
local_variable.path,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation_multi,
|
|
)
|
|
else:
|
|
self.check_variable_in_calculation_in_list_not_multi(
|
|
local_variable.path,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation_multi,
|
|
)
|
|
elif variable_in_calculation_multi is True:
|
|
msg = _(
|
|
'the variable "{0}" has an invalid attribute "{1}", the variable "{2}" must not be multi'
|
|
).format(
|
|
local_variable.path, self.attribute_name, variable_in_calculation_path
|
|
)
|
|
raise DictConsistencyError(msg, 23, self.xmlfiles)
|
|
|
|
def check_variable_in_calculation_multi(
|
|
self,
|
|
local_variable_path,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation_multi,
|
|
):
|
|
if variable_in_calculation_multi is False:
|
|
msg = _(
|
|
'the variable "{0}" has an invalid attribute "{1}", the variable must not be a multi or the variable "{2}" must be multi'
|
|
).format(
|
|
local_variable_path, self.attribute_name, variable_in_calculation_path
|
|
)
|
|
raise DictConsistencyError(msg, 20, self.xmlfiles)
|
|
|
|
def check_variable_in_calculation_not_multi(
|
|
self,
|
|
local_variable_path,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation_multi,
|
|
):
|
|
if variable_in_calculation_multi is True:
|
|
msg = _(
|
|
'the variable "{0}" has an invalid attribute "{1}", the variable must be a multi or the variable "{2}" must not be multi'
|
|
).format(
|
|
local_variable_path, self.attribute_name, variable_in_calculation_path
|
|
)
|
|
raise DictConsistencyError(msg, 21, self.xmlfiles)
|
|
|
|
def check_variable_in_calculation_in_list_not_multi(
|
|
self,
|
|
local_variable_path,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation_multi,
|
|
):
|
|
if variable_in_calculation_multi is True:
|
|
msg = _(
|
|
'the variable "{0}" has an invalid attribute "{1}", the variable "{2}" is multi but is inside a list'
|
|
).format(
|
|
local_variable_path, self.attribute_name, variable_in_calculation_path
|
|
)
|
|
raise DictConsistencyError(msg, 18, self.xmlfiles)
|
|
|
|
def get_default_value_optional(self, objectspace, path, default):
|
|
if self.attribute_name == "default":
|
|
if self.inside_list:
|
|
expected_multiple_value = False
|
|
elif path in objectspace.followers:
|
|
expected_multiple_value = objectspace.multis[path] == "submulti"
|
|
else:
|
|
expected_multiple_value = path in objectspace.multis
|
|
elif self.attribute_name in PROPERTY_ATTRIBUTE:
|
|
expected_multiple_value = False
|
|
else:
|
|
expected_multiple_value = True
|
|
value_is_multi = isinstance(default, list)
|
|
if expected_multiple_value != value_is_multi:
|
|
if self.attribute_name != "default" or expected_multiple_value:
|
|
msg = _(
|
|
'the variable "{0}" is waiting for a list as "{1}" but the attribute "default" is not a list ("{2}")'
|
|
)
|
|
else:
|
|
msg = _(
|
|
'the variable "{0}" is not waiting for a list as "{1}" but the attribute "default" is a list ("{2}")'
|
|
)
|
|
msg = msg.format(path, self.attribute_name, default)
|
|
raise DictConsistencyError(msg, 77, self.xmlfiles)
|
|
return default
|
|
|
|
|
|
class VariableCalculation(_VariableCalculation):
|
|
attribute_name: Literal["default", "choices", "dynamic"]
|
|
default: Any = undefined
|
|
|
|
def to_function(
|
|
self,
|
|
objectspace,
|
|
path,
|
|
) -> dict:
|
|
if self.ori_path is not None:
|
|
path = self.ori_path
|
|
if (
|
|
self.attribute_name != "default"
|
|
and self.optional
|
|
and self.default is undefined
|
|
):
|
|
msg = _(
|
|
'"{0}" attribut shall not have an "optional" attribute without the "default" attribute for variable "{1}"'
|
|
).format(self.attribute_name, self.variable)
|
|
raise DictConsistencyError(msg, 33, self.xmlfiles)
|
|
(
|
|
variable_in_calculation_path,
|
|
variable_in_calculation,
|
|
variable_in_calculation_identifier,
|
|
) = self.get_variable(objectspace, path)
|
|
if not variable_in_calculation and (
|
|
self.optional or objectspace.force_optional
|
|
):
|
|
if self.default is not undefined:
|
|
return self.get_default_value_optional(objectspace, path, self.default)
|
|
if self.default_values is not None:
|
|
return self.default_values
|
|
raise VariableCalculationDependencyError()
|
|
if variable_in_calculation and self.attribute_name == "default":
|
|
local_variable = objectspace.paths[path]
|
|
if CONVERT_OPTION.get(local_variable.type, {}).get(
|
|
"func", str
|
|
) != CONVERT_OPTION.get(variable_in_calculation.type, {}).get("func", str):
|
|
msg = _(
|
|
'variable "{0}" has a default value calculated with "{1}" which has incompatible type'
|
|
).format(path, self.variable)
|
|
raise DictConsistencyError(msg, 67, self.xmlfiles)
|
|
params = self.get_params(
|
|
objectspace,
|
|
path,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation,
|
|
variable_in_calculation_identifier,
|
|
)
|
|
return {
|
|
"function": "calc_value",
|
|
"params": params,
|
|
}
|
|
|
|
|
|
class VariablePropertyCalculation(_VariableCalculation):
|
|
attribute_name: Literal[*PROPERTY_ATTRIBUTE]
|
|
propertyerror: Union[Literal["transitive"], bool] = True
|
|
when: Any = undefined
|
|
when_not: Any = undefined
|
|
default: bool = False
|
|
|
|
def to_function(
|
|
self,
|
|
objectspace,
|
|
path,
|
|
) -> dict:
|
|
if self.ori_path is not None:
|
|
path = self.ori_path
|
|
(
|
|
variable_in_calculation_path,
|
|
variable_in_calculation,
|
|
variable_in_calculation_identifier,
|
|
) = self.get_variable(objectspace, path)
|
|
if (
|
|
# self.default is not undefined and
|
|
not variable_in_calculation
|
|
and (self.optional or objectspace.force_optional)
|
|
):
|
|
if self.default is undefined:
|
|
default = False
|
|
else:
|
|
default = self.default
|
|
if not isinstance(default, bool):
|
|
msg = _(
|
|
'the variable "{0}" is waiting for a boolean as "{1}" but the attribute "default" is not a boolean ("{2}")'
|
|
)
|
|
msg = msg.format(path, self.attribute_name, default)
|
|
raise DictConsistencyError(msg, 79, self.xmlfiles)
|
|
return self.get_default_value_optional(objectspace, path, default)
|
|
params = self.get_params(
|
|
objectspace,
|
|
path,
|
|
variable_in_calculation_path,
|
|
variable_in_calculation,
|
|
variable_in_calculation_identifier,
|
|
key="value",
|
|
)
|
|
params["prop"] = self.attribute_name
|
|
func = "variable_to_property"
|
|
if objectspace.force_optional and (
|
|
not params["value"] or "variable" not in params["value"]
|
|
):
|
|
params = {"value": None, "when": None, "inverse": False}
|
|
else:
|
|
variable = params["value"]["variable"]
|
|
if self.when is not undefined:
|
|
if self.version == "1.0":
|
|
msg = _(
|
|
'"when" is not allowed in format version 1.0 for attribute "{0}" for variable "{1}"'
|
|
).format(self.attribute_name, path)
|
|
raise DictConsistencyError(msg, 103, variable.xmlfiles)
|
|
if self.when_not is not undefined:
|
|
msg = _(
|
|
'the variable "{0}" has an invalid attribute "{1}", "when" and "when_not" cannot set together'
|
|
).format(path, self.attribute_name)
|
|
raise DictConsistencyError(msg, 31, variable.xmlfiles)
|
|
params["when"] = self.when
|
|
params["inverse"] = False
|
|
elif self.when_not is not undefined:
|
|
if self.version == "1.0":
|
|
msg = _(
|
|
'"when_not" is not allowed in format version 1.0 for attribute "{0}" for variable "{1}"'
|
|
).format(self.attribute_name, path)
|
|
raise DictConsistencyError(msg, 104, variable.xmlfiles)
|
|
params["when"] = self.when_not
|
|
params["inverse"] = True
|
|
elif self.propertyerror != "transitive":
|
|
if variable.multi:
|
|
params["when"] = []
|
|
else:
|
|
if variable.type != "boolean":
|
|
msg = _(
|
|
'"when" or "when_not" is mandatory for the not boolean variable "{0}" in attribute "{1}"'
|
|
).format(path, self.attribute_name)
|
|
raise DictConsistencyError(msg, 106, variable.xmlfiles)
|
|
params["when"] = True
|
|
params["inverse"] = False
|
|
else:
|
|
func = "variable_to_property_transitive"
|
|
return {
|
|
"function": func,
|
|
"params": params,
|
|
"help": func,
|
|
}
|
|
|
|
|
|
class InformationCalculation(Calculation):
|
|
attribute_name: Literal["default", "choice", "dynamic"]
|
|
information: StrictStr
|
|
variable: Optional[StrictStr]
|
|
|
|
def to_function(
|
|
self,
|
|
objectspace,
|
|
path,
|
|
) -> dict:
|
|
if self.ori_path is not None:
|
|
path = self.ori_path
|
|
params = {
|
|
None: [
|
|
{
|
|
"type": "information",
|
|
"information": self.information,
|
|
}
|
|
]
|
|
}
|
|
if self.variable:
|
|
variable, identifier = objectspace.paths.get_with_dynamic(
|
|
self.variable,
|
|
path,
|
|
self.version,
|
|
self.namespace,
|
|
self.xmlfiles,
|
|
)
|
|
if variable is None:
|
|
if not objectspace.force_optional:
|
|
msg = _(
|
|
'cannot find variable "{0}" for the information "{1}" when calculating "{2}"'
|
|
).format(self.variable, self.information, self.attribute_name)
|
|
raise DictConsistencyError(msg, 40, self.xmlfiles)
|
|
if identifier is not None:
|
|
msg = _(
|
|
'identifier not allowed for the information "{0}" when calculating "{1}"'
|
|
).format(self.information, self.attribute_name)
|
|
raise DictConsistencyError(msg, 41, self.xmlfiles)
|
|
if variable:
|
|
params[None][0]["variable"] = variable
|
|
if self.default_values:
|
|
params["__default_value"] = self.default_values
|
|
return {
|
|
"function": "calc_value",
|
|
"params": params,
|
|
}
|
|
|
|
|
|
class _IdentifierCalculation(Calculation):
|
|
identifier: Optional[int] = None
|
|
|
|
def get_identifier(self) -> dict:
|
|
identifier = {"type": "identifier"}
|
|
if self.identifier is not None:
|
|
identifier["identifier"] = self.identifier
|
|
return identifier
|
|
|
|
|
|
class IdentifierCalculation(_IdentifierCalculation):
|
|
attribute_name: Literal["default", "choice", "dynamic"]
|
|
|
|
def to_function(
|
|
self,
|
|
objectspace,
|
|
path,
|
|
) -> dict:
|
|
identifier = {"type": "identifier"}
|
|
if self.identifier is not None:
|
|
identifier["identifier"] = self.identifier
|
|
return {
|
|
"function": "calc_value",
|
|
"params": {None: [self.get_identifier()]},
|
|
}
|
|
|
|
|
|
class IdentifierPropertyCalculation(_IdentifierCalculation):
|
|
attribute_name: Literal[*PROPERTY_ATTRIBUTE]
|
|
when: Any = undefined
|
|
when_not: Any = undefined
|
|
|
|
def to_function(
|
|
self,
|
|
objectspace,
|
|
path,
|
|
) -> dict:
|
|
if self.version == "1.0":
|
|
msg = _(
|
|
'"when" is not allowed in format version 1.0 for attribute "{0}"'
|
|
).format(self.attribute_name)
|
|
raise DictConsistencyError(msg, 105, variable.xmlfiles)
|
|
if self.when is not undefined:
|
|
if self.when_not is not undefined:
|
|
msg = _(
|
|
'the identifier has an invalid attribute "{0}", "when" and "when_not" cannot set together'
|
|
).format(self.attribute_name)
|
|
raise DictConsistencyError(msg, 35, variable.xmlfiles)
|
|
when = self.when
|
|
inverse = False
|
|
elif self.when_not is not undefined:
|
|
when = self.when_not
|
|
inverse = True
|
|
else:
|
|
msg = _(
|
|
'the identifier has an invalid attribute "{0}", "when" and "when_not" cannot set together'
|
|
).format(self.attribute_name)
|
|
raise DictConsistencyError
|
|
params = {
|
|
"prop": self.attribute_name,
|
|
"value": self.get_identifier(),
|
|
"when": when,
|
|
"inverse": inverse,
|
|
}
|
|
return {
|
|
"function": "variable_to_property",
|
|
"params": params,
|
|
"help": "variable_to_property",
|
|
}
|
|
|
|
|
|
class IndexCalculation(Calculation):
|
|
attribute_name: Literal["default", "choice", "dynamic"]
|
|
|
|
def to_function(
|
|
self,
|
|
objectspace,
|
|
path,
|
|
) -> dict:
|
|
if path not in objectspace.followers:
|
|
msg = _(
|
|
'the variable "{0}" is not a follower, so cannot have index type for "{1}"'
|
|
).format(path, self.attribute_name)
|
|
raise DictConsistencyError(msg, 60, self.xmlfiles)
|
|
return {
|
|
"function": "calc_value",
|
|
"params": {None: [{"type": "index"}]},
|
|
}
|
|
|
|
|
|
class NamespaceCalculation(Calculation):
|
|
attribute_name: Literal["default", "secret_manager"]
|
|
|
|
def to_function(
|
|
self,
|
|
objectspace,
|
|
path,
|
|
) -> dict:
|
|
namespace = self.namespace
|
|
if namespace:
|
|
namespace = objectspace.paths[namespace].description
|
|
return namespace
|
|
|
|
|
|
CALCULATION_TYPES = {
|
|
"jinja": JinjaCalculation,
|
|
"information": InformationCalculation,
|
|
"variable": VariableCalculation,
|
|
"identifier": IdentifierCalculation,
|
|
# FOR VERSION 1.0
|
|
"suffix": IdentifierCalculation,
|
|
"index": IndexCalculation,
|
|
"namespace": NamespaceCalculation,
|
|
}
|
|
CALCULATION_PROPERTY_TYPES = {
|
|
"jinja": JinjaCalculation,
|
|
"information": InformationCalculation,
|
|
"variable": VariablePropertyCalculation,
|
|
"identifier": IdentifierPropertyCalculation,
|
|
"index": IndexCalculation,
|
|
}
|
|
BASETYPE_CALC = Union[StrictBool, StrictInt, StrictFloat, StrictStr, Calculation, None]
|
|
SECRET_BASETYPE_CALC = Union[StrictStr, JinjaCalculation]
|
|
|
|
|
|
class Family(BaseModel):
|
|
name: str
|
|
# informations
|
|
description: Optional[StrictStr] = None
|
|
help: Optional[StrictStr] = None
|
|
mode: Optional[StrictStr] = None
|
|
# validation
|
|
type: Literal["family", "leadership", "dynamic"] = "family"
|
|
# properties
|
|
hidden: Union[bool, Calculation] = False
|
|
disabled: Union[bool, Calculation] = False
|
|
# others
|
|
namespace: Optional[StrictStr]
|
|
path: StrictStr
|
|
version: StrictStr
|
|
xmlfiles: List[StrictStr] = []
|
|
|
|
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
|
|
|
|
|
|
class Dynamic(Family):
|
|
# None only for format 1.0
|
|
variable: str = None
|
|
dynamic: Union[List[Union[StrictStr, Calculation]], Calculation]
|
|
|
|
|
|
class Variable(BaseModel):
|
|
name: str
|
|
# user informations
|
|
description: Optional[StrictStr] = None
|
|
help: Optional[StrictStr] = None
|
|
mode: Optional[StrictStr] = None
|
|
tags: Optional[list] = None
|
|
examples: Optional[list] = None
|
|
test: Optional[list] = None
|
|
# validations
|
|
## type will be set dynamically in `annotator/value.py`, default is None
|
|
type: str = None
|
|
params: Optional[List[Param]] = None
|
|
regexp: Optional[StrictStr] = None
|
|
choices: Optional[Union[List[BASETYPE_CALC], Calculation]] = None
|
|
multi: Optional[bool] = None
|
|
validators: Optional[List[Calculation]] = None
|
|
warnings: bool = False
|
|
# value
|
|
default: Union[List[BASETYPE_CALC], BASETYPE_CALC] = None
|
|
secret_manager: Optional[JinjaCalculation] = None
|
|
# properties
|
|
auto_save: bool = False
|
|
mandatory: Union[None, bool, Calculation] = None
|
|
empty: Union[None, bool, Calculation] = True
|
|
unique: Optional[bool] = None
|
|
hidden: Union[bool, Calculation] = False
|
|
disabled: Union[bool, Calculation] = False
|
|
frozen: Union[bool, Calculation] = False
|
|
# others
|
|
path: str
|
|
namespace: Optional[str]
|
|
version: str
|
|
xmlfiles: List[str] = []
|
|
|
|
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
|
|
|
|
|
|
class SymLink(BaseModel):
|
|
type: Literal["symlink"] = "symlink"
|
|
name: str
|
|
path: str
|
|
opt: Variable
|
|
namespace: Optional[str]
|
|
version: str
|
|
xmlfiles: List[str] = []
|
|
|
|
model_config = ConfigDict(extra="forbid")
|