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
[flake8]
inline-quotes = double
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."""
import argparse
import sys
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.
"""Make errors not show the stracktrace to stdout.
Todo:
* 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))
elif not specs:
# TODO replace this with error handling system
print("No specs, exiting.")
sys.exit(0)
# TODO replace this with error handling system
print("No specs, exiting.")
sys.exit(0)
for spec in specs:
spec.backup()

View File

@ -1,9 +1,14 @@
"""Module handling IFTTT notifications."""
from typing import List
import os
import requests
class Notifier:
"""A notifier object that can send IFTTT notifications."""
# (positive, negative)
_EVENTS = {
"backup": (
@ -15,24 +20,40 @@ class Notifier:
"Couldn't restore {name}.",
),
}
"""The message content for a given event."""
# Placeholder
def __init__(
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.events = events
self.endpoint = endpoint
self.api_key = api_key
def notify(self, category: str, name: str, status_code: int):
"""
"""Send an IFTTT notification.
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
status_code: exit code of the command
"""
event = "{}_{}".format(
category, "success" if status_code == 0 else "failure"
)

View File

@ -1,4 +1,6 @@
"""Handles parsing a config file from disk."""
import yaml
from pathlib import Path
from typing import List, Union
@ -7,8 +9,7 @@ import skeleton
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:
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."""
def __init__(self, key: str):
"""
Create a new InvalidKeyError object with the given key.
"""Create a new InvalidKeyError object with the given key.
Args:
key: the invalid key
@ -21,8 +20,7 @@ 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.
"""Create a new MissingKeyError object with the given key.
Args:
key: the invalid key
@ -33,8 +31,7 @@ class MissingKeyError(Exception):
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"
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:
"""
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
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 typing import Union
from pathlib import Path
@ -46,6 +46,7 @@ class ContainerSpec(Spec):
self.command = command
def backup(self):
"""Create a new backup."""
# Remove excess backups
self.remove_backups()

View File

@ -1,3 +1,6 @@
"""Module defining a directory-based spec."""
from .spec import Spec
from pathlib import Path
from typing import Union
@ -23,6 +26,18 @@ class DirectorySpec(Spec):
extension: str,
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)
self.source = source if type(source) == Path else Path(source)
@ -36,6 +51,7 @@ class DirectorySpec(Spec):
self.command = command
def backup(self):
"""Create a new backup."""
# Remove excess backups
self.remove_backups()

View File

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

View File

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