diff --git a/hyperglass/configuration/.gitignore b/hyperglass/configuration/.gitignore deleted file mode 100644 index 4ecc56a..0000000 --- a/hyperglass/configuration/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.DS_Store -*.toml -*.yaml -*.test -configuration_old \ No newline at end of file diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index 929f62f..c4debb2 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -6,7 +6,7 @@ from hyperglass.state import use_state from hyperglass.defaults.directives import init_builtin_directives # Local -from .main import init_params, init_devices, init_ui_params, init_directives +from .validate import init_params, init_devices, init_ui_params, init_directives __all__ = ("init_user_config",) diff --git a/hyperglass/configuration/collect.py b/hyperglass/configuration/load.py similarity index 96% rename from hyperglass/configuration/collect.py rename to hyperglass/configuration/load.py index e7f83cd..999e93c 100644 --- a/hyperglass/configuration/collect.py +++ b/hyperglass/configuration/load.py @@ -5,6 +5,7 @@ import typing as t from pathlib import Path # Project +from hyperglass.log import log from hyperglass.util import run_coroutine_in_new_thread from hyperglass.settings import Settings from hyperglass.constants import CONFIG_EXTENSIONS @@ -66,6 +67,7 @@ def load_dsl(path: Path, *, empty_allowed: bool) -> LoadedConfig: raise ConfigError( "'{!s}' exists, but it is empty and is required to start hyperglass.".format(path), ) + log.debug("Loaded configuration from {!s}", path) return data or {} @@ -102,6 +104,8 @@ def load_python(path: Path, *, empty_allowed: bool) -> LoadedConfig: if data is None and empty_allowed is False: raise ConfigError(f"'{path!s} exists', but variable or function 'main' is an invalid type") + + log.debug("Loaded configuration from {!s}", path) return data or {} diff --git a/hyperglass/configuration/markdown.py b/hyperglass/configuration/markdown.py index 873e671..3df2762 100644 --- a/hyperglass/configuration/markdown.py +++ b/hyperglass/configuration/markdown.py @@ -1,61 +1,26 @@ """Markdown processing utility functions.""" -# Project -from hyperglass.log import log +# Standard Library +import typing as t +from pathlib import Path + +if t.TYPE_CHECKING: + # Project + from hyperglass.models import HyperglassModel -def _get_file(path_obj): - """Read a file. +def get_markdown(config: "HyperglassModel", default: str, params: t.Dict[str, t.Any]) -> str: + """Get markdown file if specified, or use default.""" - Arguments: - path_obj {Path} -- Path to file. - - Returns: - {str} -- File contents - """ - with path_obj.open("r") as raw_file: - return raw_file.read() - - -def format_markdown(content, params): - """Format content with config parameters. - - Arguments: - content {str} -- Unformatted content - - Returns: - {str} -- Formatted content - """ - try: - fmt = content.format(**params) - except KeyError: - fmt = content - return fmt - - -def get_markdown(config_path, default, params): - """Get markdown file if specified, or use default. - - Format the content with config parameters. - - Arguments: - config_path {object} -- content config - default {str} -- default content - - Returns: - {str} -- Formatted content - """ - log.trace(f"Getting Markdown content for '{params['title']}'") - - if config_path.enable and config_path.file is not None: - md = _get_file(config_path.file) + if config.enable and config.file is not None: + # with config_path.file + if hasattr(config, "file") and isinstance(config.file, Path): + with config.file.open("r") as config_file: + md = config_file.read() else: md = default - log.trace(f"Unformatted Content for '{params['title']}':\n{md}") - - md_fmt = format_markdown(md, params) - - log.trace(f"Formatted Content for '{params['title']}':\n{md_fmt}") - - return md_fmt + try: + return md.format(**params) + except KeyError: + return md diff --git a/hyperglass/configuration/main.py b/hyperglass/configuration/validate.py similarity index 52% rename from hyperglass/configuration/main.py rename to hyperglass/configuration/validate.py index 6d1c49a..40d5a50 100644 --- a/hyperglass/configuration/main.py +++ b/hyperglass/configuration/validate.py @@ -1,26 +1,20 @@ -"""Import configuration files and returns default values if undefined.""" +"""Import configuration files and run validation.""" -# Standard Library -import typing as t -from pathlib import Path # Third Party -import yaml from pydantic import ValidationError # Project from hyperglass.log import log, enable_file_logging, enable_syslog_logging -from hyperglass.settings import Settings from hyperglass.models.ui import UIParameters -from hyperglass.util.files import check_path from hyperglass.models.directive import Directive, Directives -from hyperglass.exceptions.private import ConfigError, ConfigMissing +from hyperglass.exceptions.private import ConfigError, ConfigInvalid from hyperglass.models.config.params import Params from hyperglass.models.config.devices import Devices # Local +from .load import load_config from .markdown import get_markdown -from .validation import validate_config __all__ = ( "init_params", @@ -29,93 +23,12 @@ __all__ = ( "init_ui_params", ) -# Project Directories -CONFIG_PATH = Settings.app_path -CONFIG_FILES = ( - ("hyperglass.yaml", False), - ("devices.yaml", True), - ("directives.yaml", False), -) - - -def _check_config_files(directory: Path): - """Verify config files exist and are readable.""" - - files = () - - for file in CONFIG_FILES: - file_name, required = file - file_path = directory / file_name - - checked = check_path(file_path) - - if checked is None and required: - raise ConfigMissing(missing_item=str(file_path)) - - if checked is None and not required: - log.warning( - "'{f}' was not found, but is not required to run hyperglass. " - + "Defaults will be used.", - f=str(file_path), - ) - files += (checked,) - - return files - - -CONFIG_MAIN, CONFIG_DEVICES, CONFIG_DIRECTIVES = _check_config_files(CONFIG_PATH) - - -def _config_required(config_path: Path) -> t.Dict[str, t.Any]: - try: - with config_path.open("r") as cf: - config = yaml.safe_load(cf) - - except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: - raise ConfigError(message="Error reading YAML file: '{e}'", e=yaml_error) - - if config is None: - raise ConfigMissing(missing_item=config_path.name) - - return config - - -def _config_optional(config_path: Path) -> t.Dict[str, t.Any]: - - config = {} - - if config_path is None: - return config - - else: - try: - with config_path.open("r") as cf: - config = yaml.safe_load(cf) or {} - - except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: - raise ConfigError(message="Error reading YAML file: '{e}'", e=yaml_error) - - return config - - -def _get_directives(data: t.Dict[str, t.Any]) -> "Directives": - directives = () - for name, directive in data.items(): - try: - directives += (Directive(id=name, **directive),) - except ValidationError as err: - raise ConfigError( - message="Validation error in directive '{d}': '{e}'", d=name, e=err - ) from err - return Directives(*directives) - def init_params() -> "Params": """Validate & initialize configuration parameters.""" - user_config = _config_optional(CONFIG_MAIN) + user_config = load_config("config", required=False) # Map imported user configuration to expected schema. - log.debug("Unvalidated configuration from {}: {}", CONFIG_MAIN, user_config) - params = validate_config(config=user_config, importer=Params) + params = Params(**user_config) # Set up file logging once configuration parameters are initialized. enable_file_logging( @@ -160,27 +73,35 @@ def init_params() -> "Params": def init_directives() -> "Directives": """Validate & initialize directives.""" # Map imported user directives to expected schema. - _user_directives = _config_optional(CONFIG_DIRECTIVES) - log.debug("Unvalidated directives from {!s}: {}", CONFIG_DIRECTIVES, _user_directives) - return _get_directives(_user_directives) + directives = load_config("directives", required=False) + try: + directives = ( + Directive(id=name, **directive) + for name, directive in load_config("directives", required=False).items() + ) + + except ValidationError as err: + raise ConfigInvalid(errors=err.errors()) from err + + return Directives(*directives) def init_devices() -> "Devices": """Validate & initialize devices.""" - devices_config = _config_required(CONFIG_DEVICES) - log.debug("Unvalidated devices from {!s}: {!r}", CONFIG_DEVICES, devices_config) + devices_config = load_config("devices", required=True) items = [] + # Support first matching main key name. for key in ("main", "devices", "routers"): if key in devices_config: items = devices_config[key] break if len(items) < 1: - raise ConfigError("No devices are defined in devices.yaml") + raise ConfigError("No devices are defined in devices file") devices = Devices(*items) - log.info("Initialized devices {!r}", devices) + log.debug("Initialized devices {!r}", devices) return devices diff --git a/hyperglass/configuration/validation.py b/hyperglass/configuration/validation.py deleted file mode 100644 index e4aa0cd..0000000 --- a/hyperglass/configuration/validation.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Post-Validation Validation. - -Some validations need to occur across multiple config files. -""" -# Standard Library -from typing import Any, Dict, List, Union, TypeVar - -# Third Party -from pydantic import ValidationError - -# Project -from hyperglass.exceptions.private import ConfigInvalid - -Importer = TypeVar("Importer") - - -def validate_config(config: Union[Dict[str, Any], List[Any]], importer: Importer) -> Importer: - """Validate a config dict against a model.""" - validated = None - try: - if isinstance(config, Dict): - validated = importer(**config) - elif isinstance(config, List): - validated = importer(config) - except ValidationError as err: - raise ConfigInvalid(errors=err.errors()) from None - - return validated