add config validator subcommand (#80)

* add config validator subcommand

* add --exit-code flag

* docs & faq update
This commit is contained in:
2023-01-09 17:22:29 +02:00
committed by GitHub
parent 1f07a89316
commit d942a70272
36 changed files with 1393 additions and 47 deletions

View File

@ -101,6 +101,7 @@ def _parser() -> argparse.ArgumentParser:
_set_repo_check_parser(subparsers)
_set_repo_clean_parser(subparsers)
_set_repo_config_parser(subparsers)
_set_repo_config_validate_parser(subparsers)
_set_repo_rebuild_parser(subparsers)
_set_repo_remove_unknown_parser(subparsers)
_set_repo_report_parser(subparsers)
@ -537,6 +538,25 @@ def _set_repo_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def _set_repo_config_validate_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for config validation subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-config-validate", aliases=["config-validate"], help="validate system configuration",
description="validate configuration and print found errors",
formatter_class=_formatter)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if configuration is invalid",
action="store_true")
parser.set_defaults(handler=handlers.Validate, lock=None, report=False, quiet=True, unsafe=True)
return parser
def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository rebuild subcommand

View File

@ -42,5 +42,6 @@ from ahriman.application.handlers.triggers import Triggers
from ahriman.application.handlers.unsafe_commands import UnsafeCommands
from ahriman.application.handlers.update import Update
from ahriman.application.handlers.users import Users
from ahriman.application.handlers.validate import Validate
from ahriman.application.handlers.versions import Versions
from ahriman.application.handlers.web import Web

View File

@ -0,0 +1,150 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import copy
from typing import Any, Callable, Dict, Optional, Type
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.configuration.schema import CONFIGURATION_SCHEMA, \
GITREMOTE_REMOTE_PULL_SCHEMA, GITREMOTE_REMOTE_PUSH_SCHEMA, \
REPORT_CONSOLE_SCHEMA, REPORT_EMAIL_SCHEMA, REPORT_HTML_SCHEMA, REPORT_TELEGRAM_SCHEMA,\
UPLOAD_GITHUB_SCHEMA, UPLOAD_RSYNC_SCHEMA, UPLOAD_S3_SCHEMA
from ahriman.core.configuration.validator import Validator
from ahriman.core.formatters import ValidationPrinter
class Validate(Handler):
"""
configuration validator handler
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration, *,
report: bool, unsafe: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
schema = Validate.schema(architecture, configuration)
validator = Validator(instance=configuration, schema=schema)
if validator.validate(configuration.dump()):
return # no errors found
for node, errors in validator.errors.items():
ValidationPrinter(node, errors).print(verbose=True)
# as we reach this part it means that we always have errors
Validate.check_if_empty(args.exit_code, True)
@staticmethod
def schema(architecture: str, configuration: Configuration) -> Dict[str, Any]:
"""
get schema with triggers
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
Returns:
Dict[str, Any]: configuration validation schema
"""
root = copy.deepcopy(CONFIGURATION_SCHEMA)
# that's actually bad design, but in order to validate built-in triggers we need to know which are set
Validate.schema_insert(architecture, configuration, root, "remote-pull", lambda _: GITREMOTE_REMOTE_PULL_SCHEMA)
Validate.schema_insert(architecture, configuration, root, "remote-push", lambda _: GITREMOTE_REMOTE_PUSH_SCHEMA)
report_schemas = {
"console": REPORT_CONSOLE_SCHEMA,
"email": REPORT_EMAIL_SCHEMA,
"html": REPORT_HTML_SCHEMA,
"telegram": REPORT_TELEGRAM_SCHEMA,
}
for schema_name, schema in report_schemas.items():
root[schema_name] = Validate.schema_erase_required(copy.deepcopy(schema))
Validate.schema_insert(architecture, configuration, root, "report", report_schemas.get)
upload_schemas = {
"github": UPLOAD_GITHUB_SCHEMA,
"rsync": UPLOAD_RSYNC_SCHEMA,
"s3": UPLOAD_S3_SCHEMA,
}
for schema_name, schema in upload_schemas.items():
root[schema_name] = Validate.schema_erase_required(copy.deepcopy(schema))
Validate.schema_insert(architecture, configuration, root, "upload", upload_schemas.get)
return root
@staticmethod
def schema_erase_required(schema: Dict[str, Any]) -> Dict[str, Any]:
"""
recursively remove required field from supplied cerberus schema
Args:
schema(Dict[str, Any]): source schema from which required field must be removed
Returns:
Dict[str, Any]: schema without required fields
"""
schema.pop("required", None)
for value in filter(lambda v: isinstance(v, dict), schema.values()):
Validate.schema_erase_required(value)
return schema
@staticmethod
def schema_insert(architecture: str, configuration: Configuration, root: Dict[str, Any], root_section: str,
schema_mapping: Callable[[str], Optional[Dict[str, Any]]]) -> Dict[str, Any]:
"""
insert child schema into the root schema based on mapping rules
Notes:
Actually it is a bad design, because we are reading triggers configuration from parsers which (basically)
don't know anything about triggers. But in order to validate built-in triggers we need to know which are set
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
root(Dict[str, Any]): root schema in which child schema will be inserted
root_section(str): section name in root schema
schema_mapping(Callable[[str], Optional[Dict[str, Any]]]): extractor for child schema based on trigger type
Returns:
Dict[str, Any]: modified root schema. Note, however, that schema will be modified in place
"""
if not configuration.has_section(root_section):
return root
targets = configuration.getlist(root_section, "target", fallback=[])
for target in targets:
section, schema_name = configuration.gettype(target, architecture)
if (schema := schema_mapping(schema_name)) is not None:
root[section] = copy.deepcopy(schema)
return root

View File

@ -0,0 +1,20 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.configuration.configuration import Configuration

View File

@ -24,7 +24,7 @@ import shlex
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Type
from typing import Any, Callable, Dict, List, Optional, Tuple, Type
from ahriman.core.exceptions import InitializeError
from ahriman.models.repository_paths import RepositoryPaths
@ -63,6 +63,7 @@ class Configuration(configparser.RawConfigParser):
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"]
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
converters: Dict[str, Callable[[str], Any]] # typing guard
def __init__(self, allow_no_value: bool = False) -> None:
"""
@ -74,7 +75,7 @@ class Configuration(configparser.RawConfigParser):
"""
configparser.RawConfigParser.__init__(self, allow_no_value=allow_no_value, converters={
"list": shlex.split,
"path": self.__convert_path,
"path": self._convert_path,
})
self.architecture: Optional[str] = None
self.path: Optional[Path] = None
@ -141,7 +142,7 @@ class Configuration(configparser.RawConfigParser):
"""
return f"{section}:{suffix}"
def __convert_path(self, value: str) -> Path:
def _convert_path(self, value: str) -> Path:
"""
convert string value to path object

View File

@ -0,0 +1,554 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# pylint: disable=too-many-lines
__all__ = [
"CONFIGURATION_SCHEMA",
"GITREMOTE_REMOTE_PULL_SCHEMA", "GITREMOTE_REMOTE_PUSH_SCHEMA",
"REPORT_CONSOLE_SCHEMA", "REPORT_EMAIL_SCHEMA", "REPORT_HTML_SCHEMA", "REPORT_TELEGRAM_SCHEMA",
"UPLOAD_GITHUB_SCHEMA", "UPLOAD_RSYNC_SCHEMA", "UPLOAD_S3_SCHEMA",
]
CONFIGURATION_SCHEMA = {
"settings": {
"type": "dict",
"schema": {
"include": {
"type": "path",
"coerce": "absolute_path",
"required": True,
"path_exists": True,
},
"database": {
"type": "path",
"coerce": "absolute_path",
"required": True,
},
"logging": {
"type": "path",
"coerce": "absolute_path",
"required": True,
"path_exists": True,
},
},
},
"alpm": {
"type": "dict",
"schema": {
"database": {
"type": "path",
"coerce": "absolute_path",
"required": True,
},
"mirror": {
"type": "string",
"required": True,
},
"repositories": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
"required": True,
"empty": False,
},
"root": {
"type": "path",
"coerce": "absolute_path",
"required": True,
"path_exists": True,
},
"use_ahriman_cache": {
"type": "boolean",
"coerce": "boolean",
"required": True,
},
},
},
"auth": {
"type": "dict",
"schema": {
"target": {
"type": "string",
"oneof": [
{"allowed": ["disabled"]},
{"allowed": ["configuration", "mapping"], "dependencies": ["salt"]},
{"allowed": ["oauth"], "dependencies": [
"client_id", "client_secret", "oauth_provider", "oauth_scopes", "salt"
]},
],
},
"allow_read_only": {
"type": "boolean",
"coerce": "boolean",
"required": True,
},
"client_id": {
"type": "string",
},
"client_secret": {
"type": "string",
},
"max_age": {
"type": "integer",
"coerce": "integer",
},
"oauth_provider": {
"type": "string",
},
"oauth_scopes": {
"type": "string",
},
"salt": {
"type": "string",
},
},
},
"build": {
"type": "dict",
"schema": {
"archbuild_flags": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
"build_command": {
"type": "string",
"required": True,
},
"ignore_packages": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
"makepkg_flags": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
"makechrootpkg_flags": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
"triggers": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
"vcs_allowed_age": {
"type": "integer",
"coerce": "integer",
},
},
},
"repository": {
"type": "dict",
"schema": {
"name": {
"type": "string",
"required": True,
},
"root": {
"type": "string",
"required": True,
},
},
},
"sign": {
"type": "dict",
"allow_unknown": True,
"keysrules": {
"type": "string",
"anyof_regex": ["^target$", "^key$", "^key_.*"],
},
"schema": {
"target": {
"type": "list",
"coerce": "list",
"oneof": [
{"allowed": []},
{"allowed": ["package", "repository"], "dependencies": ["key"]},
],
},
"key": {
"type": "string",
},
},
},
"web": {
"type": "dict",
"schema": {
"address": {
"type": "string",
},
"debug": {
"type": "boolean",
"coerce": "boolean",
},
"debug_check_host": {
"type": "boolean",
"coerce": "boolean",
},
"debug_allowed_hosts": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
"host": {
"type": "string",
},
"index_url": {
"type": "string",
},
"password": {
"type": "string",
},
"port": {
"type": "integer",
"coerce": "integer",
"min": 0,
"max": 65535,
},
"static_path": {
"type": "path",
"coerce": "absolute_path",
"required": True,
"path_exists": True,
},
"templates": {
"type": "path",
"coerce": "absolute_path",
"required": True,
"path_exists": True,
},
"unix_socket": {
"type": "path",
"coerce": "absolute_path",
},
"unix_socket_unsafe": {
"type": "boolean",
"coerce": "boolean",
},
"username": {
"type": "string",
},
},
},
"remote-pull": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
"remote-push": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
"report": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
"upload": {
"type": "dict",
"schema": {
"target": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
},
},
},
}
GITREMOTE_REMOTE_PULL_SCHEMA = {
"type": "dict",
"schema": {
"pull_url": {
"type": "string",
"required": True,
},
"pull_branch": {
"type": "string",
},
},
}
GITREMOTE_REMOTE_PUSH_SCHEMA = {
"type": "dict",
"schema": {
"commit_author": {
"type": "string",
},
"push_url": {
"type": "string",
"required": True,
},
"push_branch": {
"type": "string",
},
},
}
REPORT_CONSOLE_SCHEMA = {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["console"],
},
"use_utf": {
"type": "boolean",
"coerce": "boolean",
},
},
}
REPORT_EMAIL_SCHEMA = {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["email"],
},
"full_template_path": {
"type": "path",
"coerce": "absolute_path",
"path_exists": True,
},
"homepage": {
"type": "string",
},
"host": {
"type": "string",
"required": True,
},
"link_path": {
"type": "string",
"required": True,
},
"no_empty_report": {
"type": "boolean",
"coerce": "boolean",
},
"password": {
"type": "string",
},
"port": {
"type": "integer",
"coerce": "integer",
"required": True,
},
"receivers": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
"required": True,
"empty": False,
},
"sender": {
"type": "string",
"required": True,
},
"ssl": {
"type": "string",
"allowed": ["ssl", "starttls", "disabled"],
},
"template_path": {
"type": "path",
"coerce": "absolute_path",
"required": True,
"path_exists": True,
},
"user": {
"type": "string",
},
},
}
REPORT_HTML_SCHEMA = {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["html"],
},
"homepage": {
"type": "string",
},
"link_path": {
"type": "string",
"required": True,
},
"path": {
"type": "path",
"coerce": "absolute_path",
"required": True,
},
"template_path": {
"type": "path",
"coerce": "absolute_path",
"required": True,
"path_exists": True,
},
},
}
REPORT_TELEGRAM_SCHEMA = {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["telegram"],
},
"api_key": {
"type": "string",
"required": True,
},
"chat_id": {
"type": "string",
"required": True,
},
"homepage": {
"type": "string",
},
"link_path": {
"type": "string",
"required": True,
},
"template_path": {
"type": "path",
"coerce": "absolute_path",
"required": True,
"path_exists": True,
},
"template_type": {
"type": "string",
"allowed": ["MarkdownV2", "HTML", "Markdown"],
},
"timeout": {
"type": "integer",
"coerce": "integer",
},
},
}
UPLOAD_GITHUB_SCHEMA = {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["github"],
},
"owner": {
"type": "string",
"required": True,
},
"password": {
"type": "string",
"required": True,
},
"repository": {
"type": "string",
"required": True,
},
"timeout": {
"type": "integer",
"coerce": "integer",
},
"username": {
"type": "string",
},
},
}
UPLOAD_RSYNC_SCHEMA = {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["rsync"],
},
"command": {
"type": "list",
"coerce": "list",
"schema": {"type": "string"},
"required": True,
"empty": False,
},
"remote": {
"type": "string",
"required": True,
},
},
}
UPLOAD_S3_SCHEMA = {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["s3"],
},
"access_key": {
"type": "string",
"required": True,
},
"bucket": {
"type": "string",
"required": True,
},
"chunk_size": {
"type": "integer",
"coerce": "integer",
},
"region": {
"type": "string",
"required": True,
},
"secret_key": {
"type": "string",
"required": True,
},
},
}

View File

@ -0,0 +1,116 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from cerberus import TypeDefinition, Validator as RootValidator # type: ignore
from pathlib import Path
from typing import Any, List
from ahriman.core.configuration import Configuration
class Validator(RootValidator): # type: ignore
"""
class which defines custom validation methods for the service configuration
Attributes:
instance(Configuration): configuration instance
"""
types_mapping = RootValidator.types_mapping.copy()
types_mapping["path"] = TypeDefinition("path", (Path,), ())
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
default constructor
Args:
instance(Configuration): configuration instance used for extraction
*args(Any): positional arguments to be passed to base validator
**kwargs(): keyword arguments to be passed to base validator
"""
RootValidator.__init__(self, *args, **kwargs)
self.instance: Configuration = kwargs["instance"]
def _normalize_coerce_absolute_path(self, value: str) -> Path:
"""
extract path from string value
Args:
value(str): converting value
Returns:
Path: value converted to path instance according to configuration rules
"""
converted: Path = self.instance.converters["path"](value)
return converted
def _normalize_coerce_boolean(self, value: str) -> bool:
"""
extract boolean from string value
Args:
value(str): converting value
Returns:
bool: value converted to boolean according to configuration rules
"""
# pylint: disable=protected-access
converted: bool = self.instance._convert_to_boolean(value) # type: ignore
return converted
def _normalize_coerce_integer(self, value: str) -> int:
"""
extract integer from string value
Args:
value(str): converting value
Returns:
int: value converted to int according to configuration rules
"""
return int(value)
def _normalize_coerce_list(self, value: str) -> List[str]:
"""
extract string list from string value
Args:
value(str): converting value
Returns:
List[str]: value converted to string list instance according to configuration rules
"""
converted: List[str] = self.instance.converters["list"](value)
return converted
def _validate_path_exists(self, constraint: bool, field: str, value: Path) -> None:
"""
check if paths exists
Args:
constraint(bool): True in case if path must exist and False otherwise
field(str): field name to be checked
value(Path): value to be checked
Examples:
The rule's arguments are validated against this schema:
{"type": "boolean"}
"""
if constraint and not value.exists():
self._error(field, f"Path {value} must exist")

View File

@ -27,6 +27,7 @@ from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.patch_printer import PatchPrinter
from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.tree_printer import TreePrinter
from ahriman.core.formatters.validation_printer import ValidationPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter
from ahriman.core.formatters.version_printer import VersionPrinter

View File

@ -41,7 +41,8 @@ class Printer:
for prop in self.properties():
if not verbose and not prop.is_required:
continue
log_fn(f"\t{prop.name}{separator}{prop.value}")
indent = "\t" * prop.indent
log_fn(f"{indent}{prop.name}{separator}{prop.value}")
def properties(self) -> List[Property]:
"""

View File

@ -0,0 +1,77 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import Any, Dict, Generator, List, Union
from ahriman.core.formatters import StringPrinter
from ahriman.models.property import Property
class ValidationPrinter(StringPrinter):
"""
print content of the validation errors
Attributes:
node(str): root level name
errors(List[Union[str, Dict[str, Any]]]): validation errors
"""
def __init__(self, node: str, errors: List[Union[str, Dict[str, Any]]]) -> None:
"""
default constructor
Args:
node(str): root level name
errors(List[Union[str, Dict[str, Any]]]): validation errors
"""
StringPrinter.__init__(self, node)
self.node = node
self.errors = errors
@staticmethod
def get_error_messages(node: str, errors: List[Union[str, Dict[str, Any]]],
current_level: int = 1) -> Generator[Property, None, None]:
"""
extract default error message from cerberus class
Args:
node(str): current node level name
errors(List[Union[str, Dict[str, Any]]]): current node validation errors
current_level(int, optional): current level number (Default value = 1)
Yields:
Property: error messages from error tree
"""
for error in errors:
if not isinstance(error, str): # child nodes errors
for child_node, child_errors in error.items():
# increase indentation instead of nodes concatenations
# sometimes it is not only nodes, but rules themselves
yield from ValidationPrinter.get_error_messages(child_node, child_errors, current_level + 1)
else: # current node errors
yield Property(node, error, is_required=True, indent=current_level)
def properties(self) -> List[Property]:
"""
convert content into printable data
Returns:
List[Property]: list of content properties
"""
return list(self.get_error_messages(self.node, self.errors))

View File

@ -30,8 +30,10 @@ class Property:
name(str): name of the property
value(Any): property value
is_required(bool): if set to True then this property is required
indent(int): property indentation level
"""
name: str
value: Any
is_required: bool = field(default=False, kw_only=True)
indent: int = 1