Added even more docstrings everywhere
continuous-integration/drone the build failed Details

pull/15/head
Jef Roosens 2021-04-26 17:45:19 +02:00
parent ecfa6fe7b7
commit 83ea06f016
Signed by: Jef Roosens
GPG Key ID: B580B976584B5F30
10 changed files with 87 additions and 38 deletions

View File

@ -1,4 +1,8 @@
# vim: ft=cfg # vim: ft=cfg
[flake8] [flake8]
inline-quotes = double
max-complexity = 7 max-complexity = 7
docstring-convention=google
# Required to be compatible with black
extend-ignore = E203,W503
inline-quotes = double

View File

@ -0,0 +1 @@
"""Module containing all app code."""

View File

@ -1,13 +1,13 @@
"""The main entrypoint of the program.""" """The main entrypoint of the program."""
import argparse import argparse
import sys import sys
from parser import read_specs_file 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): def except_hook(ext_type, value, traceback):
""" """Make errors not show the stracktrace to stdout.
Make errors not show the stracktrace to stdout.
Todo: Todo:
* Replace this with proper error handling * Replace this with proper error handling
@ -59,9 +59,9 @@ if __name__ == "__main__":
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: elif not specs:
# TODO replace this with error handling system # TODO replace this with error handling system
print("No specs, exiting.") print("No specs, exiting.")
sys.exit(0) sys.exit(0)
for spec in specs: for spec in specs:
spec.backup() spec.backup()

View File

@ -1,9 +1,14 @@
"""Module handling IFTTT notifications."""
from typing import List from typing import List
import os import os
import requests import requests
class Notifier: class Notifier:
"""A notifier object that can send IFTTT notifications."""
# (positive, negative) # (positive, negative)
_EVENTS = { _EVENTS = {
"backup": ( "backup": (
@ -15,24 +20,40 @@ class Notifier:
"Couldn't restore {name}.", "Couldn't restore {name}.",
), ),
} }
"""The message content for a given event."""
# Placeholder # Placeholder
def __init__( def __init__(
self, title: str, events: List[str], endpoint: str, api_key: str = None self, title: str, events: List[str], endpoint: str, api_key: str = None
): ):
"""Initialize a new Notifier object.
Args:
title: the notification title to use
events: the event types that should trigger a notification (should
be one of the keys in _EVENTS).
endpoint: IFTTT endpoint name
api_key: your IFTTT API key. If not provided, it will be read from
the IFTTT_API_KEY environment variable.
Todo:
* Read the API key on init
"""
self.title = title self.title = title
self.events = events self.events = events
self.endpoint = endpoint self.endpoint = endpoint
self.api_key = api_key self.api_key = api_key
def notify(self, category: str, name: str, status_code: int): def notify(self, category: str, name: str, status_code: int):
""" """Send an IFTTT notification.
Args: Args:
category: type of notify (e.g. backup or restore) category: type of notify (should be one of the keys in _EVENTS).
Only if the category was passed during initialization will the
notification be sent.
name: name of the spec name: name of the spec
status_code: exit code of the command status_code: exit code of the command
""" """
event = "{}_{}".format( event = "{}_{}".format(
category, "success" if status_code == 0 else "failure" category, "success" if status_code == 0 else "failure"
) )

View File

@ -1,4 +1,6 @@
"""Handles parsing a config file from disk.""" """Handles parsing a config file from disk."""
import yaml import yaml
from pathlib import Path from pathlib import Path
from typing import List, Union from typing import List, Union
@ -7,8 +9,7 @@ import skeleton
def read_specs_file(path: Union[str, Path]) -> List[Spec]: def read_specs_file(path: Union[str, Path]) -> List[Spec]:
""" """Read a config file and merge it with the skeleton.
Read a config file and merge it with the skeleton.
Args: Args:
path: path to the yaml config file path: path to the yaml config file

View File

@ -6,8 +6,7 @@ class InvalidKeyError(Exception):
"""Thrown when a config file contains an invalid key.""" """Thrown when a config file contains an invalid key."""
def __init__(self, key: str): def __init__(self, key: str):
""" """Create a new InvalidKeyError object with the given key.
Create a new InvalidKeyError object with the given key.
Args: Args:
key: the invalid key key: the invalid key
@ -21,8 +20,7 @@ class MissingKeyError(Exception):
"""Thrown when a required key is missing from a config.""" """Thrown when a required key is missing from a config."""
def __init__(self, key: str): def __init__(self, key: str):
""" """Create a new MissingKeyError object with the given key.
Create a new MissingKeyError object with the given key.
Args: Args:
key: the invalid key key: the invalid key
@ -33,8 +31,7 @@ class MissingKeyError(Exception):
def merge(*dicts: [Dict]) -> Dict: def merge(*dicts: [Dict]) -> Dict:
""" """Merge multiple dicts into one.
Merge multiple dicts into one.
It reads the dicts from left to right, always preferring the "right" It reads the dicts from left to right, always preferring the "right"
dictionary's values. Therefore, the dictionaries should be sorted from dictionary's values. Therefore, the dictionaries should be sorted from
@ -78,8 +75,7 @@ def merge(*dicts: [Dict]) -> Dict:
def merge_with_skeleton(data: Dict, skel: Dict) -> Dict: def merge_with_skeleton(data: Dict, skel: Dict) -> Dict:
""" """Merge a dictionary with a skeleton containing default values.
Merge a dictionary with a skeleton containing default values.
The skeleton not only defines what the default values are, but also 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 enforces a certain shape. This allows us to define a config file using a

View File

@ -1,4 +1,4 @@
"""Module defining a Container-based spec.""" """Module defining a container-based spec."""
from .spec import Spec from .spec import Spec
from typing import Union from typing import Union
from pathlib import Path from pathlib import Path
@ -46,6 +46,7 @@ class ContainerSpec(Spec):
self.command = command self.command = command
def backup(self): def backup(self):
"""Create a new backup."""
# Remove excess backups # Remove excess backups
self.remove_backups() self.remove_backups()

View File

@ -1,3 +1,6 @@
"""Module defining a directory-based spec."""
from .spec import Spec from .spec import Spec
from pathlib import Path from pathlib import Path
from typing import Union from typing import Union
@ -23,6 +26,18 @@ class DirectorySpec(Spec):
extension: str, extension: str,
notify=None, notify=None,
): ):
"""
Initialize a new DirectorySpec object.
Args:
name: name of the spec
source: what directory to back up
destination: where to store the backup
limit: how many backups to keep
command: what command to use to create the backup
extension: extension of the backup files
notify: a Notifier object that handles sending notifications
"""
super().__init__(name, destination, limit, extension, notify) super().__init__(name, destination, limit, extension, notify)
self.source = source if type(source) == Path else Path(source) self.source = source if type(source) == Path else Path(source)
@ -36,6 +51,7 @@ class DirectorySpec(Spec):
self.command = command self.command = command
def backup(self): def backup(self):
"""Create a new backup."""
# Remove excess backups # Remove excess backups
self.remove_backups() self.remove_backups()

View File

@ -1,3 +1,6 @@
"""This module contains the base Spec class."""
from pathlib import Path from pathlib import Path
from typing import Union, Dict from typing import Union, Dict
import skeleton import skeleton
@ -7,9 +10,7 @@ import inspect
class Spec: class Spec:
""" """Base class for all other spec types."""
Base class for all other spec types.
"""
_SKEL = { _SKEL = {
"destination": None, "destination": None,
@ -31,23 +32,25 @@ class Spec:
extension: str, extension: str,
notify=None, notify=None,
): ):
""" """Initialize a new Spec object.
This initializer usually gets called by a subclass's init instead of
directly.
Args: Args:
name: name of the spec name: name of the spec
destination: directory where the backups shall reside destination: directory where the backups shall reside
limit: max amount of backups limit: max amount of backups
notifier: notifier object notifier: notifier object
""" """
self.name = name self.name = name
self.destination = ( self.destination = Path(destination)
destination if type(destination) == Path else Path(destination)
)
# Create destination if non-existent # Create destination if non-existent
try: try:
self.destination.mkdir(parents=True, exist_ok=True) self.destination.mkdir(parents=True, exist_ok=True)
# TODO just make this some checks in advance
except FileExistsError: except FileExistsError:
raise NotADirectoryError( raise NotADirectoryError(
"{} already exists, but isn't a directory.".format( "{} already exists, but isn't a directory.".format(
@ -61,8 +64,7 @@ class Spec:
@classmethod @classmethod
def skeleton(cls: "Spec") -> Dict: def skeleton(cls: "Spec") -> Dict:
""" """Return the skeleton for the given class.
Return the skeleton for the given class.
It works by inspecting the inheritance tree and merging the skeleton It works by inspecting the inheritance tree and merging the skeleton
for each of the parents. for each of the parents.
@ -78,10 +80,7 @@ class Spec:
) )
def remove_backups(self): def remove_backups(self):
""" """Remove all backups exceeding the limit."""
Remove all backups exceeding the limit
"""
files = sorted( files = sorted(
self.destination.glob("*." + self.extension), self.destination.glob("*." + self.extension),
key=os.path.getmtime, key=os.path.getmtime,
@ -93,13 +92,22 @@ class Spec:
path.unlink() path.unlink()
def backup(self): def backup(self):
"""Create a new backup.
This function should be implemented by the subclasses.
"""
raise NotImplementedError() raise NotImplementedError()
def restore(self): def restore(self):
"""Restore a given backup (NOT IMPLEMENTED).
This function should be implemented by the subclasses.
"""
raise NotImplementedError() raise NotImplementedError()
@classmethod @classmethod
def from_dict(cls, name, obj: Dict, defaults: Dict) -> "Spec": def from_dict(cls, name, obj: Dict, defaults: Dict) -> "Spec":
"""Create the class given a dictionary (e.g. from a config)."""
# Combine defaults with skeleton, creating new skeleton # Combine defaults with skeleton, creating new skeleton
skel = skeleton.merge(cls.skeleton(), defaults) skel = skeleton.merge(cls.skeleton(), defaults)
@ -109,4 +117,7 @@ class Spec:
return cls(name, **obj) return cls(name, **obj)
def to_dict(self): def to_dict(self):
"""Export the class as a dictionary.
This function should be imnplemented by the subclasses."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -6,9 +6,7 @@ import subprocess
class VolumeSpec(Spec): class VolumeSpec(Spec):
""" """A spec for backing up a Docker volume."""
A spec for backing up a Docker volume.
"""
_SKEL = { _SKEL = {
"volume": None, "volume": None,