From 3c2ad438c028e3e0639aac1757e7a9c6672e62ab Mon Sep 17 00:00:00 2001 From: Jim Madge Date: Fri, 27 Oct 2023 14:29:33 +0100 Subject: [PATCH] WIP: Add schema and from_file classmethod --- data_safe_haven/config/backend_settings.py | 83 ++++++++++++++-------- pyproject.toml | 1 + 2 files changed, 53 insertions(+), 31 deletions(-) diff --git a/data_safe_haven/config/backend_settings.py b/data_safe_haven/config/backend_settings.py index ac0858eead..3e7257b051 100644 --- a/data_safe_haven/config/backend_settings.py +++ b/data_safe_haven/config/backend_settings.py @@ -5,6 +5,7 @@ import appdirs import yaml +from schema import Schema, SchemaError from yaml.parser import ParserError from data_safe_haven.exceptions import ( @@ -14,6 +15,12 @@ from data_safe_haven.utility import LoggingSingleton +config_directory = pathlib.Path( + appdirs.user_config_dir(appname="data_safe_haven") +).resolve() +config_file_path = config_directory / "config.yaml" + + @dataclass class Context: admin_group_id: str @@ -35,35 +42,31 @@ class ContextSettings: ... """ - def __init__(self) -> None: + def __init__(self, settings_dict: dict[Any, Any]) -> None: self.logger = LoggingSingleton() - self._settings: dict[Any, Any] | None = None - - config_directory = pathlib.Path( - appdirs.user_config_dir(appname="data_safe_haven") - ).resolve() - self.config_file_path = config_directory / "config.yaml" + context_schema = Schema({ + "name": str, + "admin_group_id": str, + "location": str, + "subscription_name": str, + }) + + schema = Schema({ + "selected": str, + "contexts": Schema({ + str: context_schema, + }), + }) + + try: + self._settings: schema.validate(settings_dict) + except SchemaError as exc: + msg = f"Invalid context configuration file.\n{exc}" + raise DataSafeHavenParameterError(msg) @property def settings(self) -> dict[Any, Any]: - if not self._settings: - try: - with open(self.config_file_path, encoding="utf-8") as f_yaml: - settings = yaml.safe_load(f_yaml) - if isinstance(settings, dict): - self.logger.info( - f"Reading project settings from '[green]{self.config_file_path}[/]'." - ) - self._settings = settings - self._context = Context(**settings.get("contexts").get(self._selected)) - except FileNotFoundError as exc: - msg = f"Could not find file {self.config_file_path}.\n{exc}" - raise DataSafeHavenConfigError(msg) from exc - except ParserError as exc: - msg = f"Could not load settings from {self.config_file_path}.\n{exc}" - raise DataSafeHavenConfigError(msg) from exc - return self._settings @property @@ -71,7 +74,7 @@ def selected(self) -> str: if selected := self.settings.get("selected"): return selected else: - msg = f"Selected context is not defined in {self.config_file_path}." + msg = "Selected context is not defined." raise DataSafeHavenParameterError(msg) @selected.setter @@ -80,7 +83,7 @@ def selected(self, context_name: str) -> None: self.settings["selected"] = context_name self.logger.info(f"Switched context to {context_name}.") else: - msg = f"Context {context_name} is not defined in {self.config_file_path}." + msg = f"Context {context_name} is not defined." raise DataSafeHavenParameterError(msg) @property @@ -88,7 +91,7 @@ def context(self) -> Context: if context_dict := self.settings.get("contexts").get(self.selected): return Context(**context_dict) else: - msg = f"Context {self.selected} is not defined in {self.config_file_path}." + msg = f"Context {self.selected} is not defined." raise DataSafeHavenParameterError(msg) def update( @@ -118,13 +121,31 @@ def update( ) context_dict["subscription_name"] = subscription_name - def write(self) -> None: + def write(self, config_file_path: str = config_file_path) -> None: """Write settings to YAML file""" # Create the parent directory if it does not exist then write YAML - self.config_file_path.parent.mkdir(parents=True, exist_ok=True) + config_file_path.parent.mkdir(parents=True, exist_ok=True) - with open(self.config_file_path, "w", encoding="utf-8") as f_yaml: + with open(config_file_path, "w", encoding="utf-8") as f_yaml: yaml.dump(self.settings, f_yaml, indent=2) self.logger.info( - f"Saved context settings to '[green]{self.config_file_path}[/]'." + f"Saved context settings to '[green]{config_file_path}[/]'." ) + + @classmethod + def from_file(cls, config_file_path: str = config_file_path) -> None: + logger = LoggingSingleton() + try: + with open(config_file_path, encoding="utf-8") as f_yaml: + settings = yaml.safe_load(f_yaml) + if isinstance(settings, dict): + logger.info( + f"Reading project settings from '[green]{config_file_path}[/]'." + ) + return cls(settings) + except FileNotFoundError as exc: + msg = f"Could not find file {config_file_path}.\n{exc}" + raise DataSafeHavenConfigError(msg) from exc + except ParserError as exc: + msg = f"Could not load settings from {config_file_path}.\n{exc}" + raise DataSafeHavenConfigError(msg) from exc diff --git a/pyproject.toml b/pyproject.toml index 95afe80918..203b1ff2c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "pytz~=2022.7.0", "PyYAML~=6.0", "rich~=13.4.2", + "schema~=0.7.0", "simple-acme-dns~=1.2.0", "typer~=0.9.0", "websocket-client~=1.5.0",