Added even more docstrings everywhere
continuous-integration/drone the build failed
Details
continuous-integration/drone the build failed
Details
parent
ecfa6fe7b7
commit
83ea06f016
6
.flake8
6
.flake8
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Module containing all app code."""
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue