diff --git a/app/__main__.py b/app/__main__.py index fec66e1..b4cd3a0 100644 --- a/app/__main__.py +++ b/app/__main__.py @@ -1,4 +1,3 @@ -"""The main entrypoint of the program.""" import argparse import sys from parser import read_specs_file @@ -6,62 +5,55 @@ from parser import read_specs_file # This just displays the error type and message, not the stack trace def except_hook(ext_type, value, traceback): - """ - Make errors not show the stracktrace to stdout. - - Todo: - * Replace this with proper error handling - """ sys.stderr.write("{}: {}\n".format(ext_type.__name__, value)) # sys.excepthook = except_hook -if __name__ == "__main__": - # Define parser - parser = argparse.ArgumentParser( - description="Backup directories and Docker volumes." - ) - parser.add_argument( - "-f", - "--file", - action="append", - dest="file", - required=True, - help="File containing spec definitions.", - ) - parser.add_argument( - "-j", - "--json", - action="store_const", - const=True, - default=False, - help="Print out the parsed specs as JSON and exit", - ) - parser.add_argument( - "spec", nargs="*", help="The specs to process. Defaults to all." - ) +# Define parser +parser = argparse.ArgumentParser( + description="Backup directories and Docker volumes." +) +parser.add_argument( + "-f", + "--file", + action="append", + dest="file", + required=True, + help="File containing spec definitions.", +) +parser.add_argument( + "-j", + "--json", + action="store_const", + const=True, + default=False, + help="Print out the parsed specs as JSON and exit", +) +parser.add_argument( + "spec", nargs="*", help="The specs to process. Defaults to all." +) - # Parse arguments - args = parser.parse_args() - specs = sum((read_specs_file(path) for path in args.file), []) +# Parse arguments +args = parser.parse_args() +specs = sum([read_specs_file(path) for path in args.file], []) - # Filter specs if needed - if args.spec: - specs = list(filter(lambda s: s.name in args.spec, specs)) +# Filter specs if needed +if args.spec: + specs = list(filter(lambda s: s.name in args.spec, specs)) - # Dump parsed data as json - if args.json: - import json +# Dump parsed data as json +if args.json: + import json - # TODO replace this with error handling system - print(json.dumps([spec.to_dict() for spec in specs], indent=4)) + print(json.dumps([spec.to_dict() for spec in specs], indent=4)) - elif not specs: - # TODO replace this with error handling system - print("No specs, exiting.") - sys.exit(0) +else: + # Run the backups + if not specs: + print("No specs, exiting.") + sys.exit(0) - for spec in specs: - spec.backup() + for spec in specs: + spec.backup() diff --git a/app/parser.py b/app/parser.py index dcd9fb1..bbac40c 100644 --- a/app/parser.py +++ b/app/parser.py @@ -1,4 +1,3 @@ -"""Handles parsing a config file from disk.""" import yaml from pathlib import Path from typing import List, Union @@ -7,19 +6,9 @@ import skeleton def read_specs_file(path: Union[str, Path]) -> List[Spec]: - """ - Read a config file and merge it with the skeleton. - - Args: - path: path to the yaml config file - - Returns: - A list of specs, parsed from the config. - """ with open(path, "r") as yaml_file: data = yaml.safe_load(yaml_file) - # NOTE shouldn't this be defined as a top-level variable? categories = [ ("directories", DirectorySpec), ("volumes", VolumeSpec), @@ -34,7 +23,6 @@ def read_specs_file(path: Union[str, Path]) -> List[Spec]: # Check what defaults are present defaults = {} - if data.get("defaults"): if data["defaults"].get("all"): defaults = skeleton.merge(defaults, data["defaults"]["all"]) diff --git a/app/skeleton.py b/app/skeleton.py index 5391e1b..07c6afb 100644 --- a/app/skeleton.py +++ b/app/skeleton.py @@ -1,55 +1,21 @@ -"""Handles merging with the skeleton config.""" from typing import Dict class InvalidKeyError(Exception): - """Thrown when a config file contains an invalid key.""" - - def __init__(self, key: str): - """ - Create a new InvalidKeyError object with the given key. - - Args: - key: the invalid key - """ + def __init__(self, key): self.message = "Invalid key: {}".format(key) super().__init__(key) class MissingKeyError(Exception): - """Thrown when a required key is missing from a config.""" - - def __init__(self, key: str): - """ - Create a new MissingKeyError object with the given key. - - Args: - key: the invalid key - """ + def __init__(self, key): self.message = "Missing key: {}".format(key) super().__init__(key) def merge(*dicts: [Dict]) -> Dict: - """ - Merge multiple dicts into one. - - It reads the dicts from left to right, always preferring the "right" - dictionary's values. Therefore, the dictionaries should be sorted from - least important to most important (e.g. a default values skeleton should be - to the left of a dict of selected values). - - Args: - dicts: the dictionaries to merge - - Returns: - a new dictionary representing the merged dictionaries - - Todo: - * Make sure an infinite loop is not possible - """ # Base cases if len(dicts) == 0: return {} @@ -79,23 +45,10 @@ def merge(*dicts: [Dict]) -> Dict: def merge_with_skeleton(data: Dict, skel: Dict) -> Dict: """ - Merge a dictionary with a skeleton containing default values. - - The skeleton not only defines what the default values are, but also - enforces a certain shape. This allows us to define a config file using a - dictionary and parse it. - - Args: - data: dictionary containing the selected config values - skel: dictionary containing the skeleton (aka the def) - - Returns: - a new dictionary representing the two merged dictionaries - - Todo: - * Check if an infinite loop is possible - * Split info less complex functions + 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: @@ -113,7 +66,6 @@ def merge_with_skeleton(data: Dict, skel: Dict) -> Dict: # Error if value is not same type as default value elif type(data[key]) != type(value) and value is not None: - # TODO make this error message more verbose raise TypeError("Invalid value type") # Recurse into dicts diff --git a/app/specs/__init__.py b/app/specs/__init__.py index dd08c1c..0192b9e 100644 --- a/app/specs/__init__.py +++ b/app/specs/__init__.py @@ -1,7 +1,4 @@ -"""Parent module for the various spec types.""" from .spec import Spec from .directory import DirectorySpec from .volume import VolumeSpec from .container import ContainerSpec - -__all__ = ["Spec", "DirectorySpec", "VolumeSpec", "ContainerSpec"] diff --git a/app/specs/container.py b/app/specs/container.py index 76579f0..bef6f8c 100644 --- a/app/specs/container.py +++ b/app/specs/container.py @@ -1,4 +1,3 @@ -"""Module defining a Container-based spec.""" from .spec import Spec from typing import Union from pathlib import Path @@ -7,10 +6,11 @@ import subprocess class ContainerSpec(Spec): - """Spec for backing up via a container.""" + """ + A spec for backing up via a container. + """ _SKEL = {"container": None, "command": None, "mountpoint": "/from"} - """The skeleton for the ContainerSpec config.""" def __init__( self, @@ -23,22 +23,6 @@ class ContainerSpec(Spec): mountpoint: str, notify=None, ): - """ - Create a new ContainerSpec object. - - Args: - name: name of the spec (used as an identifier) - container: the Docker container to back up - destination: where to store the backups (gets created if - non-existent) - limit: max amount of backups to keep - command: command to run inside the container. This command should - perform a specified backup and output this data to stdout. This - output then gets piped to a backup file. - extension: the extension of the backup files. - mountpoint: - notify: notifier object (may be None) - """ super().__init__(name, destination, limit, extension, notify) self.container = container diff --git a/app/specs/directory.py b/app/specs/directory.py index 4fa0ffe..9747b5b 100644 --- a/app/specs/directory.py +++ b/app/specs/directory.py @@ -6,7 +6,9 @@ from datetime import datetime class DirectorySpec(Spec): - """A spec for backing up a local directory.""" + """ + A spec for backing up a local directory. + """ _SKEL = { "source": None, diff --git a/app/specs/spec.py b/app/specs/spec.py index 2669ccb..9b372ef 100644 --- a/app/specs/spec.py +++ b/app/specs/spec.py @@ -60,19 +60,7 @@ class Spec: self.extension = extension @classmethod - def skeleton(cls: "Spec") -> Dict: - """ - Return the skeleton for the given class. - - It works by inspecting the inheritance tree and merging the skeleton - for each of the parents. - - Args: - cls: the class to get the skeleton for - - Returns: - a dictionary containing the skeleton - """ + def skeleton(cls): return skeleton.merge( *[val._SKEL for val in reversed(inspect.getmro(cls)[:-1])] )