Compare commits
No commits in common. "9ee03b22bb3548098c3aeab6a087f0bf239184e1" and "62620d9b2ee869f2ad7788beb48a0101da931d52" have entirely different histories.
9ee03b22bb
...
62620d9b2e
23 changed files with 36 additions and 257 deletions
|
|
@ -24,13 +24,10 @@ 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 .__version__ import __version__
|
||||
|
||||
try:
|
||||
from .convert import Rougail
|
||||
from .config import RougailConfig
|
||||
|
||||
__all__ = ("Rougail", "RougailConfig", "__version__")
|
||||
except ModuleNotFoundError as err:
|
||||
__all__ = ("__version__",)
|
||||
|
|
|
|||
|
|
@ -162,16 +162,6 @@ class Annotator(Walk): # pylint: disable=R0903
|
|||
)
|
||||
if calculated_variable is not None:
|
||||
if calculated_variable.multi is None:
|
||||
if (
|
||||
isinstance(calculated_variable.default, VariableCalculation)
|
||||
and variable.path == calculated_variable.default.path
|
||||
):
|
||||
msg = _(
|
||||
'the "{0}" default value is a calculation with itself'.format(
|
||||
variable.path
|
||||
)
|
||||
)
|
||||
raise DictConsistencyError(msg, 75, variable.xmlfiles)
|
||||
self._convert_variable_multi(calculated_variable)
|
||||
variable.multi = calc_multi_for_type_variable(
|
||||
variable,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ from ..tiramisu import normalize_family
|
|||
from ..convert import RougailConvert
|
||||
from ..convert.object_model import get_convert_option_types
|
||||
|
||||
#import rougail.structural_commandline.object_model
|
||||
|
||||
RENAMED = {
|
||||
"dictionaries_dir": "main_dictionaries",
|
||||
|
|
@ -178,7 +179,7 @@ class _RougailConfig:
|
|||
yield f"{option.path()}: {option.value.get()}"
|
||||
|
||||
def __repr__(self):
|
||||
self.generate_config()
|
||||
print(self.config)
|
||||
self.config.property.read_write()
|
||||
try:
|
||||
values = "\n".join(self.parse(self.config))
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ class Rougail(UserDatas):
|
|||
self.converted = RougailConvert(self.rougailconfig)
|
||||
self.config = None
|
||||
|
||||
def get_root_option(self):
|
||||
def run(self):
|
||||
"""Get Tiramisu Config"""
|
||||
if not self.config:
|
||||
tiram_obj = self.converted.save()
|
||||
optiondescription = {}
|
||||
custom_types = {
|
||||
|
|
@ -45,13 +47,8 @@ class Rougail(UserDatas):
|
|||
for custom in self.rougailconfig["custom_types"].values()
|
||||
}
|
||||
exec(tiram_obj, custom_types, optiondescription) # pylint: disable=W0122
|
||||
return optiondescription["option_0"]
|
||||
|
||||
def run(self):
|
||||
"""Get Tiramisu Config"""
|
||||
if not self.config:
|
||||
self.config = Config(
|
||||
self.get_root_option(),
|
||||
optiondescription["option_0"],
|
||||
display_name=tiramisu_display_name,
|
||||
)
|
||||
self.config.property.read_write()
|
||||
|
|
|
|||
|
|
@ -1164,3 +1164,4 @@ class RougailConvert(ParserVariable):
|
|||
tiramisu.write(output)
|
||||
# print(output)
|
||||
return output
|
||||
|
||||
|
|
|
|||
|
|
@ -28,12 +28,7 @@ from pydantic import (
|
|||
)
|
||||
import tiramisu
|
||||
from tiramisu.config import get_common_path
|
||||
from ..utils import (
|
||||
get_jinja_variable_to_param,
|
||||
calc_multi_for_type_variable,
|
||||
undefined,
|
||||
PROPERTY_ATTRIBUTE,
|
||||
)
|
||||
from ..utils import get_jinja_variable_to_param, calc_multi_for_type_variable, undefined, PROPERTY_ATTRIBUTE
|
||||
from ..i18n import _
|
||||
from ..error import DictConsistencyError, VariableCalculationDependencyError
|
||||
from ..tiramisu import CONVERT_OPTION
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ from importlib.util import (
|
|||
from unicodedata import normalize, combining
|
||||
from jinja2 import StrictUndefined, DictLoader
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
from re import findall
|
||||
from tiramisu import DynOptionDescription, calc_value, function_waiting_for_error
|
||||
from tiramisu.error import (
|
||||
ValueWarning,
|
||||
|
|
@ -135,16 +134,6 @@ CONVERT_OPTION = {
|
|||
}
|
||||
|
||||
|
||||
def get_identifier_from_dynamic_family(true_name, name) -> str:
|
||||
if true_name == "{{ identifier }}":
|
||||
return name
|
||||
regexp = true_name.replace("{{ identifier }}", "(.*)")
|
||||
finded = findall(regexp, name)
|
||||
if len(finded) != 1 or not finded[0]:
|
||||
return None
|
||||
return finded[0]
|
||||
|
||||
|
||||
def raise_carry_out_calculation_error(subconfig, *args, **kwargs):
|
||||
try:
|
||||
ori_raise_carry_out_calculation_error(subconfig, *args, **kwargs)
|
||||
|
|
@ -434,11 +423,3 @@ class ConvertDynOptionDescription(DynOptionDescription):
|
|||
self.convert_identifier_to_path(self.get_identifiers(subconfig)[-1]),
|
||||
)
|
||||
return display
|
||||
|
||||
def name_could_conflict(self, dynchild, child):
|
||||
return (
|
||||
get_identifier_from_dynamic_family(
|
||||
dynchild.impl_getname(), child.impl_getname()
|
||||
)
|
||||
is not None
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|||
"""
|
||||
|
||||
from typing import List
|
||||
from re import findall
|
||||
|
||||
from tiramisu import Calculation, owners
|
||||
from tiramisu.error import (
|
||||
|
|
@ -30,11 +31,7 @@ from tiramisu.error import (
|
|||
CancelParam,
|
||||
)
|
||||
from .utils import undefined
|
||||
from .tiramisu import (
|
||||
normalize_family,
|
||||
CONVERT_OPTION,
|
||||
get_identifier_from_dynamic_family,
|
||||
)
|
||||
from .tiramisu import normalize_family, CONVERT_OPTION
|
||||
from .error import DictConsistencyError
|
||||
|
||||
from .i18n import _
|
||||
|
|
@ -44,13 +41,12 @@ class UserDatas:
|
|||
def __init__(self, config) -> None:
|
||||
self.config = config
|
||||
|
||||
def user_datas(
|
||||
self,
|
||||
def user_datas(self,
|
||||
user_datas: List[dict],
|
||||
*,
|
||||
return_values_not_error=False,
|
||||
user_datas_type: str = "user_datas",
|
||||
only_default: bool = False,
|
||||
user_datas_type: str="user_datas",
|
||||
only_default: bool=False,
|
||||
):
|
||||
self.values = {}
|
||||
self.errors = []
|
||||
|
|
@ -130,9 +126,7 @@ class UserDatas:
|
|||
if not tconfig.isdynamic(only_self=True):
|
||||
# it's not a dynamic variable
|
||||
continue
|
||||
identifier = get_identifier_from_dynamic_family(
|
||||
tconfig.name(), name
|
||||
)
|
||||
identifier = self._get_identifier(tconfig.name(), name)
|
||||
if identifier != normalize_family(identifier):
|
||||
msg = _(
|
||||
'cannot load variable path "{0}", the identifier "{1}" is not valid in {2}'
|
||||
|
|
@ -246,8 +240,7 @@ class UserDatas:
|
|||
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 correctly set, remove variable to the set
|
||||
if index is not None:
|
||||
|
|
@ -263,6 +256,15 @@ class UserDatas:
|
|||
if not value_is_set:
|
||||
break
|
||||
|
||||
def _get_identifier(self, true_name, name) -> str:
|
||||
if true_name == "{{ identifier }}":
|
||||
return name
|
||||
regexp = true_name.replace("{{ identifier }}", "(.*)")
|
||||
finded = findall(regexp, name)
|
||||
if len(finded) != 1 or not finded[0]:
|
||||
return None
|
||||
return finded[0]
|
||||
|
||||
def _display_value(self, option, value):
|
||||
if not self.show_secrets and option.type() == "password":
|
||||
return "*" * 10
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ except:
|
|||
ALLOWED_LEADER_PROPERTIES.add("basic")
|
||||
ALLOWED_LEADER_PROPERTIES.add("standard")
|
||||
ALLOWED_LEADER_PROPERTIES.add("advanced")
|
||||
dict_env['default_rougail.dyn{{ identifier }}.var'] = "{% for val in __.var %}\n {% if val is not none %}\nt{{ val }}\n {% endif %}\n{% endfor %}\n"
|
||||
dict_env['default_rougail.dyn{{ identifier }}.var'] = "{% for val in __.var %}\nt{{ val }}\n{% endfor %}\n"
|
||||
dict_env['default_rougail.dyn{{ identifier }}.dyn_{{ identifier }}.var_identifiers'] = "{{ s1 }}-{{ s2 }}\n"
|
||||
option_2 = StrOption(name="var", doc="A identifier variable", multi=True, properties=frozenset({"standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_6family_dynamic_sub_dynamic_empty/rougail/00-base.yml'], 'type': 'string', 'test': ('val1', 'val2')})
|
||||
option_4 = StrOption(name="var", doc="A dynamic variable", multi=True, default=Calculation(func['jinja_to_function'], Params((), kwargs={'__internal_jinja': ParamValue("default_rougail.dyn{{ identifier }}.var"), '__internal_type': ParamValue("string"), '__internal_multi': ParamValue(True), '__internal_files': ParamValue(['../rougail-tests/structures/60_6family_dynamic_sub_dynamic_empty/rougail/00-base.yml']), '__internal_attribute': ParamValue("default"), '__internal_variable': ParamValue("rougail.dyn{{ identifier }}.var"), '__.var': ParamOption(option_2, notraisepropertyerror=True)})), properties=frozenset({"mandatory", "standard"}), informations={'ymlfiles': ['../rougail-tests/structures/60_6family_dynamic_sub_dynamic_empty/rougail/00-base.yml'], 'type': 'string'})
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
from tiramisu import *
|
||||
from tiramisu.setting import ALLOWED_LEADER_PROPERTIES
|
||||
from re import compile as re_compile
|
||||
from rougail.tiramisu import func, dict_env, load_functions, ConvertDynOptionDescription
|
||||
load_functions('../rougail-tests/funcs/test.py')
|
||||
try:
|
||||
groups.namespace
|
||||
except:
|
||||
groups.addgroup('namespace')
|
||||
ALLOWED_LEADER_PROPERTIES.add("basic")
|
||||
ALLOWED_LEADER_PROPERTIES.add("standard")
|
||||
ALLOWED_LEADER_PROPERTIES.add("advanced")
|
||||
option_4 = StrOption(name="address", doc="{{ identifier }} address", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['../rougail-tests/structures/60_8family_dynamic_same_name_1/rougail/00-base.yml'], 'type': 'string'})
|
||||
optiondescription_3 = OptionDescription(name="https_proxy", doc="{{ identifier }} Proxy", children=[option_4], properties=frozenset({"basic"}), informations={'ymlfiles': ['../rougail-tests/structures/60_8family_dynamic_same_name_1/rougail/00-base.yml']})
|
||||
option_6 = StrOption(name="address", doc="{{ identifier }} address", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['../rougail-tests/structures/60_8family_dynamic_same_name_1/rougail/00-base.yml'], 'type': 'string'})
|
||||
optiondescription_5 = ConvertDynOptionDescription(name="{{ identifier }}_proxy", doc="{{ identifier }} Proxy", identifiers=["HTTPS", "SOCKS"], children=[option_6], properties=frozenset({"basic"}), informations={'ymlfiles': ['../rougail-tests/structures/60_8family_dynamic_same_name_1/rougail/00-base.yml']})
|
||||
optiondescription_2 = OptionDescription(name="manual", doc="manual", children=[optiondescription_3, optiondescription_5], properties=frozenset({"basic"}), informations={'ymlfiles': ['../rougail-tests/structures/60_8family_dynamic_same_name_1/rougail/00-base.yml']})
|
||||
optiondescription_1 = OptionDescription(name="rougail", doc="Rougail", group_type=groups.namespace, children=[optiondescription_2], properties=frozenset({"basic"}), informations={'ymlfiles': ['']})
|
||||
option_0 = OptionDescription(name="baseoption", doc="baseoption", children=[optiondescription_1])
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
from tiramisu import *
|
||||
from tiramisu.setting import ALLOWED_LEADER_PROPERTIES
|
||||
from re import compile as re_compile
|
||||
from rougail.tiramisu import func, dict_env, load_functions, ConvertDynOptionDescription
|
||||
load_functions('../rougail-tests/funcs/test.py')
|
||||
try:
|
||||
groups.namespace
|
||||
except:
|
||||
groups.addgroup('namespace')
|
||||
ALLOWED_LEADER_PROPERTIES.add("basic")
|
||||
ALLOWED_LEADER_PROPERTIES.add("standard")
|
||||
ALLOWED_LEADER_PROPERTIES.add("advanced")
|
||||
option_4 = StrOption(name="address", doc="{{ identifier }} address", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['tests/errors/80unknown_default_variable_same_name_1/rougail/00-base.yml'], 'type': 'string'})
|
||||
optiondescription_3 = OptionDescription(name="https_proxy", doc="{{ identifier }} Proxy", children=[option_4], properties=frozenset({"basic"}), informations={'ymlfiles': ['tests/errors/80unknown_default_variable_same_name_1/rougail/00-base.yml']})
|
||||
option_6 = StrOption(name="address", doc="{{ identifier }} address", properties=frozenset({"basic", "mandatory"}), informations={'ymlfiles': ['tests/errors/80unknown_default_variable_same_name_1/rougail/00-base.yml'], 'type': 'string'})
|
||||
optiondescription_5 = ConvertDynOptionDescription(name="{{ identifier }}_proxy", doc="{{ identifier }} Proxy", identifiers=["HTTPS", "SOCKS"], children=[option_6], properties=frozenset({"basic"}), informations={'ymlfiles': ['tests/errors/80unknown_default_variable_same_name_1/rougail/00-base.yml']})
|
||||
optiondescription_2 = OptionDescription(name="manual", doc="manual", children=[optiondescription_3, optiondescription_5], properties=frozenset({"basic"}), informations={'ymlfiles': ['tests/errors/80unknown_default_variable_same_name_1/rougail/00-base.yml']})
|
||||
optiondescription_1 = OptionDescription(name="rougail", doc="Rougail", group_type=groups.namespace, children=[optiondescription_2], properties=frozenset({"basic"}), informations={'ymlfiles': ['']})
|
||||
option_0 = OptionDescription(name="baseoption", doc="baseoption", children=[optiondescription_1])
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
version: 1.1
|
||||
manual:
|
||||
|
||||
use_for_https:
|
||||
description: Also use this proxy for HTTPS
|
||||
default: true
|
||||
|
||||
"{{ identifier }}_proxy":
|
||||
description: "{{ identifier }} Proxy"
|
||||
dynamic:
|
||||
- HTTPS
|
||||
- SOCKS
|
||||
hidden:
|
||||
variable: rougail.manual.use_for_https
|
||||
|
||||
address:
|
||||
description: "{{ identifier }} address"
|
||||
default:
|
||||
variable: rougail.manual.http_proxy.address
|
||||
|
||||
port:
|
||||
description: "{{ identifier }} port"
|
||||
default:
|
||||
variable: rougail.manual.http_proxy.port
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
version: 1.1
|
||||
manual:
|
||||
|
||||
use_for_https:
|
||||
description: Also use this proxy for HTTPS
|
||||
default: true
|
||||
|
||||
"{{ identifier }}_proxy":
|
||||
description: "{{ identifier }} Proxy"
|
||||
dynamic:
|
||||
- HTTPS
|
||||
- SOCKS
|
||||
hidden:
|
||||
variable: rougail.manual.use_for_https
|
||||
|
||||
address:
|
||||
description: "{{ identifier }} address"
|
||||
default:
|
||||
variable: rougail.manual.http_proxy.address
|
||||
|
||||
port:
|
||||
description: "{{ identifier }} port"
|
||||
default:
|
||||
variable: rougail.manual.http_proxy.port
|
||||
|
|
@ -49,7 +49,7 @@ test_ok -= excludes
|
|||
test_raise -= excludes
|
||||
# test_ok = ['04_5validators_multi3']
|
||||
#test_ok = []
|
||||
# test_raise = ['80unknown_default_variable_inside_dynamic_family']
|
||||
# test_raise = ['22_0calculation_variable_leader_follower_multi']
|
||||
#test_raise = []
|
||||
test_multi = True
|
||||
#test_multi = False
|
||||
|
|
@ -134,7 +134,7 @@ def save(test_dir, eolobj, multi=False, namespace=False, error=False):
|
|||
tiramisu_objects = fh.read()
|
||||
with open(tiramisu_file, 'r') as fh:
|
||||
tiramisu_objects_ori = fh.read()
|
||||
assert tiramisu_objects == tiramisu_objects_ori, f'{test_dir} -> {tiramisu_file}'
|
||||
assert tiramisu_objects == tiramisu_objects_ori
|
||||
if isdir(tiramisu_tmp_dir):
|
||||
rmtree(tiramisu_tmp_dir)
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ def test_dictionary_namespace(test_dir):
|
|||
assert getcwd() == ORI_DIR
|
||||
|
||||
|
||||
def test_error_dictionary_namespace(test_dir_error):
|
||||
def test_error_dictionary(test_dir_error):
|
||||
assert getcwd() == ORI_DIR
|
||||
test_dir_ = join(errors_dirs, test_dir_error)
|
||||
errno = []
|
||||
|
|
@ -202,28 +202,3 @@ def test_error_dictionary_namespace(test_dir_error):
|
|||
if isdir(tiramisu_tmp_dir):
|
||||
rmtree(tiramisu_tmp_dir)
|
||||
assert getcwd() == ORI_DIR
|
||||
|
||||
|
||||
def test_error_dictionary(test_dir_error):
|
||||
assert getcwd() == ORI_DIR
|
||||
test_dir_ = join(errors_dirs, test_dir_error)
|
||||
if isfile(join(test_dir_, 'force_namespace')):
|
||||
return
|
||||
errno = []
|
||||
rougailconfig = RougailConfig.copy()
|
||||
rougailconfig['main_namespace'] = None
|
||||
eolobj = load_rougail_object(test_dir_, rougailconfig, namespace=False)
|
||||
if eolobj is None:
|
||||
return
|
||||
for i in listdir(test_dir_):
|
||||
if i.startswith('errno_'):
|
||||
errno.append(int(i.split('_')[1]))
|
||||
if not errno:
|
||||
errno.append(0)
|
||||
with raises(DictConsistencyError) as err:
|
||||
save(test_dir_, eolobj, error=True)
|
||||
assert err.value.errno in errno, f'expected errno: {errno}, errno: {err.value.errno}, msg: {err}'
|
||||
tiramisu_tmp_dir = dirname(get_tiramisu_filename(test_dir_, 'tmp', False, True))
|
||||
if isdir(tiramisu_tmp_dir):
|
||||
rmtree(tiramisu_tmp_dir)
|
||||
assert getcwd() == ORI_DIR
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import logging
|
|||
|
||||
from rougail import Rougail, RougailConfig
|
||||
from rougail.error import DictConsistencyError
|
||||
from tiramisu.error import ConflictError
|
||||
|
||||
from rougail_tests.utils import config_to_dict
|
||||
|
||||
|
|
@ -96,74 +95,3 @@ def test_namespace():
|
|||
'ns2.var2': 'NS2',
|
||||
'ns3.var1': 'NS3',
|
||||
'ns3.var2': 'NS3'}
|
||||
#
|
||||
#
|
||||
#def test_duplicate_0():
|
||||
# rougailconfig = RougailConfig.copy()
|
||||
# rougailconfig['main_namespace'] = None
|
||||
# rougailconfig['dictionaries_dir'] = ['tests/duplicates/0/']
|
||||
# eolobj = Rougail(rougailconfig=rougailconfig)
|
||||
# cfg = eolobj.run()
|
||||
# cfg.value.get()
|
||||
# cfg.option('od.od_val1').value.get()
|
||||
# cfg.option('od.od_val2').value.get()
|
||||
|
||||
|
||||
def test_duplicate_1():
|
||||
rougailconfig = RougailConfig.copy()
|
||||
rougailconfig['main_namespace'] = None
|
||||
rougailconfig['dictionaries_dir'] = ['tests/duplicates/1/']
|
||||
eolobj = Rougail(rougailconfig=rougailconfig)
|
||||
cfg = eolobj.run()
|
||||
with raises(ConflictError):
|
||||
cfg.value.get()
|
||||
with raises(ConflictError):
|
||||
cfg.option('od.od_val').value.get()
|
||||
|
||||
|
||||
def test_duplicate_2():
|
||||
rougailconfig = RougailConfig.copy()
|
||||
rougailconfig['main_namespace'] = None
|
||||
rougailconfig['dictionaries_dir'] = ['tests/duplicates/2/']
|
||||
eolobj = Rougail(rougailconfig=rougailconfig)
|
||||
cfg = eolobj.run()
|
||||
with raises(ConflictError):
|
||||
cfg.value.get()
|
||||
with raises(ConflictError):
|
||||
cfg.option('od.od_val').value.get()
|
||||
|
||||
|
||||
def test_duplicate_3():
|
||||
rougailconfig = RougailConfig.copy()
|
||||
rougailconfig['main_namespace'] = None
|
||||
rougailconfig['dictionaries_dir'] = ['tests/duplicates/3/']
|
||||
eolobj = Rougail(rougailconfig=rougailconfig)
|
||||
cfg = eolobj.run()
|
||||
with raises(ConflictError):
|
||||
cfg.value.get()
|
||||
with raises(ConflictError):
|
||||
cfg.option('od.od_val').value.get()
|
||||
#
|
||||
#
|
||||
#def test_duplicate_4():
|
||||
# rougailconfig = RougailConfig.copy()
|
||||
# rougailconfig['main_namespace'] = None
|
||||
# rougailconfig['dictionaries_dir'] = ['tests/duplicates/4/']
|
||||
# eolobj = Rougail(rougailconfig=rougailconfig)
|
||||
# cfg = eolobj.run()
|
||||
# with raises(ConflictError):
|
||||
# cfg.value.get()
|
||||
# with raises(ConflictError):
|
||||
# cfg.option('od.od_val').value.get()
|
||||
|
||||
|
||||
def test_duplicate_5():
|
||||
rougailconfig = RougailConfig.copy()
|
||||
rougailconfig['main_namespace'] = None
|
||||
rougailconfig['dictionaries_dir'] = ['tests/duplicates/5/']
|
||||
eolobj = Rougail(rougailconfig=rougailconfig)
|
||||
cfg = eolobj.run()
|
||||
with raises(ConflictError):
|
||||
cfg.value.get()
|
||||
with raises(ConflictError):
|
||||
cfg.option('od.od_val').value.get()
|
||||
|
|
|
|||
Loading…
Reference in a new issue