import yaml from pathlib import Path from specs import Spec from typing import List, Dict class InvalidKeyError(Exception): def __init__(self, key): message = "Invalid key: {}".format(key) super().__init__(key) class MissingKeyError(Exception): def __init__(self, key): message = "Missing key: {}".format(key) super().__init__(key) def parse_specs_file(path: Path) -> List[Spec]: """ Parse a YAML file defining backup specs. Args: path: path to the specs file Returns: A list of specs """ # Skeleton of a spec config # If a value is None, this means it doesn't have a default value and must be # defined spec_skel = { "source": None, "destination": None, "limit": None, "volume": False, "notify": { "title": "Backup Notification", "events": ["success"] } } # Read YAML file with open(path, "r") as yaml_file: data = yaml.load(yaml_file, Loader=yaml.Loader) # Check specs section exists if "specs" not in data: raise MissingKeyError("specs") # TODO check if only specs section exists specs = [] # Check format for each spec for key in data["specs"]: specs.append(Spec.from_dict(key, combine_with_skeleton( data["specs"][key], spec_skel) )) return specs def combine_with_skeleton(data: Dict, skel: Dict) -> Dict: """ Compare a dict with a given skeleton dict, and fill in default values where needed. """ # First, check for illegal keys for key in data: if key not in skel: raise InvalidKeyError(key) # Then, check the default values for key, value in skel.items(): if key not in data: # Raise error if there's not default value if value is None: raise MissingKeyError(key) # Replace with default value data[key] = value # Error if value is not same type as default value elif type(data[key]) != type(value) and value is not None: raise TypeError("Invalid value type") # Recurse into dicts elif type(value) == dict: data[key] = combine_with_skeleton(data[key], value) return data # Test cases if __name__ == "__main__": d1 = { "a": 5 } s1 = { "a": 7, "b": 2 } r1 = { "a": 5, "b": 2 } assert combine_with_skeleton(d1, s1) == r1