Skip to content
This repository has been archived by the owner on Dec 7, 2021. It is now read-only.

Commit

Permalink
Merge pull request #2 from sralloza/check-settings
Browse files Browse the repository at this point in the history
Improve settings
  • Loading branch information
sralloza authored Jun 17, 2020
2 parents b84da23 + 9ac9cbb commit a0d6c5f
Show file tree
Hide file tree
Showing 4 changed files with 327 additions and 16 deletions.
16 changes: 16 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[run]
branch = True

source = backup_to_cloud, tests, ./

omit =
backup_to_cloud/_version.py
.venv/*
setup.py
versioneer.py
launcher.py

[report]
exclude_lines =
if __name__ == .__main__.:
if __name__ == "__main__":
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
python -m pip install --upgrade pip
pip install black pytest pytest-cov
pip install -r requirements.txt
pip install -e .
- name: Lint with black
run: |
black --diff --check backup_to_cloud
Expand Down
111 changes: 95 additions & 16 deletions backup_to_cloud/settings.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
from enum import Enum
from typing import Dict
import warnings

from ruamel.yaml import YAML

from .paths import SETTINGS_PATH
from .utils import log


class SettingsError(Exception):
pass


class SettingsWarning(Warning):
pass


class EntryType(Enum):
multiple_files = "multiple-files"
single_file = "single-file"


REQUIRED_ATTRS = {"name", "type", "root_path"}
VALID_TYPES = {x.value for x in EntryType}
ATTRS_TYPES = {
"name": str,
"type": str,
"root_path": str,
"cloud_folder_id": str,
"zip": bool,
"zipname": str,
"filter": str,
}
VALID_ATTRS = set(ATTRS_TYPES.keys())


class BackupEntry:
def __init__(
self,
Expand All @@ -26,11 +47,21 @@ def __init__(
"""Represents an entry in .settings.yml
Args:
name (str): name of the entry.
type (str): type of the entry. Must be declared as EntryType.
path (str): path of the entry. Can be glob.
cloud_folder_id (str, optional): Id of the folder to save in google drvie.
If None, the files will be uploaded to the root folder. Defaults to None.
name (str): the name of the entry. It is irrelevant, only representative.
type (str): the entry type. Right now it can be `single-file` or
`multiple-files`.
root_path (str): if type is `single-file`, it represents the path of the
file. If type is `multiple-files`, it represents the root folder where
the sistem will start listing files.
filter (str): if the type is `multiple-files`, this regex filter will
be applied to every file located below `root-path`. The search it's
recursively. For example, to select all pdf files, use `filter=.py`.
By default is `'.'`, which is a regex for match anything. It is
encouraged to check the regex before creating the first backup.
To check the regex check README). Defaults to '.'.
cloud_folder_id (str, optional): id of the folder to save the file(s)
into. If is not present or is None, the files will be stored in
the root folder (`Drive`). Defaults to None.
zip (bool, optional): If True and type is folder, all files will be uploaded as
zip. Defaults to False.
zipname (str, optional): if zip, this is the file name. Defaults to None.
Expand All @@ -45,16 +76,64 @@ def __init__(
self.filter = filter

def __repr__(self):
return vars(self).__repr__()
attrs = vars(self).__repr__()
return f"BackupEntry(attrs={attrs})"


def get_settings():
def fmt(kw: Dict[str, str]) -> Dict[str, str]:
return {k.replace("-", "_"): v for k, v in kw.items()}

settings_dict = YAML(typ="safe").load(SETTINGS_PATH.read_text())
try:
return [BackupEntry(name=k, **fmt(v)) for k, v in settings_dict.items()]
except TypeError as exc:
log(str(exc))
raise exc

entries = []
for name, yaml_entry in settings_dict.items():
result = check_yaml_entry(name=name, **yaml_entry)
entries.append(BackupEntry(**result))
return entries


def check_yaml_entry(**yaml_entry):
result = {}
keys = set()
for key, value in yaml_entry.items():
key = key.replace("-", "_")
if key not in VALID_ATTRS:
msg = f"{key!r} is not a valid attribute {VALID_ATTRS}"
raise SettingsError(msg)

result[key] = value
keys.add(key)

if REQUIRED_ATTRS - keys:
missing = REQUIRED_ATTRS - keys
name = result.get("name", "null")
msg = f"Missing required attributes in query {name}: {missing!r}"
raise SettingsError(msg)

if result["type"] not in VALID_TYPES:
valid_types = ", ".join(VALID_TYPES)
msg = f"{result['type']!r} is not a valid Entrytype ({valid_types})"
raise TypeError(msg)

def check_attr(key, required):
attr_type = ATTRS_TYPES[key]

if result.get(key) or required:
if not isinstance(result[key], attr_type):
real_type = type(result.get(key)).__name__
msg = ""
if not required:
msg += "If defined, "
msg += f"{key!r} must be {attr_type.__name__!r}, not {real_type!r}"

raise TypeError(msg)

for attribute in VALID_ATTRS - REQUIRED_ATTRS:
check_attr(attribute, False)

for attribute in REQUIRED_ATTRS:
check_attr(attribute, True)

if result.get("zip"):
if not result.get("zipname"):
raise SettingsError("Must provide 'zipname' if zip=True")

return result
215 changes: 215 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
from enum import Enum
from itertools import permutations
from unittest import mock

import pytest
from ruamel.yaml import YAML

from backup_to_cloud.settings import (
ATTRS_TYPES,
REQUIRED_ATTRS,
VALID_ATTRS,
BackupEntry,
EntryType,
SettingsError,
check_yaml_entry,
get_settings,
)


class TestBackupEntry:
def test_init_min(self):
be = BackupEntry("<name>", "single-file", "<root-path>")
assert be.name == "<name>"
assert be.type == EntryType.single_file
assert be.root_path == "<root-path>"
assert be.folder is None
assert be.zip is False
assert be.zipname is None
assert be.filter == "."

def test_init_all(self):
be = BackupEntry(
"<name>",
"multiple-files",
"<root-path>",
"<cloud-folder-id>",
True,
"<zipname>",
"<filter>",
)
assert be.name == "<name>"
assert be.type == EntryType.multiple_files
assert be.root_path == "<root-path>"
assert be.folder == "<cloud-folder-id>"
assert be.zip is True
assert be.zipname == "<zipname>"
assert be.filter == "<filter>"

def test_init_type_error(self):
with pytest.raises(ValueError, match="'invalid-type' is not a valid EntryType"):
BackupEntry("<name>", "invalid-type", "<root-path>")

def test_repr(self):
be = BackupEntry("<name>", "single-file", "<root-path>")
attrs = vars(be)
assert repr(be) == "BackupEntry(attrs=%s)" % attrs


class TestGetSettings:
@pytest.fixture(autouse=True)
def mocks(self):
self.set_path_m = mock.patch("backup_to_cloud.settings.SETTINGS_PATH").start()
self.check_m = mock.patch("backup_to_cloud.settings.check_yaml_entry").start()
self.be_m = mock.patch("backup_to_cloud.settings.BackupEntry").start()
yield
mock.patch.stopall()

def test_get_settings(self):
self.check_m.return_value = {"a": 1, "b": 2, "c": 3}
self.set_path_m.read_text.return_value = (
"name1:\n type: <type1>\n zip: true\n root-path: <path1>\n "
"zipname: <zipname1>\n cloud-folder-id: <folder-id1>\n filter:"
" .\n\nname2:\n type: <type2>\n zip: false\n root-path: "
"<path2>\n zipname: <zipname2>\n cloud-folder-id: <folder-id2>\n "
' filter: "*.pdf"\n'
)
dict1 = {
"name": "name1",
"type": "<type1>",
"zip": True,
"root-path": "<path1>",
"zipname": "<zipname1>",
"cloud-folder-id": "<folder-id1>",
"filter": ".",
}

dict2 = {
"name": "name2",
"type": "<type2>",
"zip": False,
"root-path": "<path2>",
"zipname": "<zipname2>",
"cloud-folder-id": "<folder-id2>",
"filter": "*.pdf",
}

settings = get_settings()

self.check_m.assert_any_call(**dict1)
self.check_m.assert_any_call(**dict2)
self.be_m.assert_any_call(a=1, b=2, c=3)
self.be_m.assert_any_call(a=1, b=2, c=3)

assert settings == [self.be_m.return_value] * 2


class TestCheckYamlEntry:
@pytest.fixture
def attrs(self):
"""Basic attributes of BackupEntry."""
attrs = {"name": "name", "root-path": "/home/test", "type": "single-file"}
yield attrs

def test_ok_basic(self, attrs):
result = check_yaml_entry(**attrs)
assert result == {"name": "name", "root_path": "/home/test", "type": "single-file"}

# def test_error_no_name(self, attrs):
# attrs.pop("name")
# with pytest.raises(TypeError, match=r"check_yaml_entry\(\) [\s\w]+: 'name'"):
# check_yaml_entry(**attrs)

def test_invalid_attrs(self, attrs):
attrs["invalid"] = True
with pytest.raises(SettingsError, match=r"'invalid' is not a valid attribute"):
check_yaml_entry(**attrs)

@pytest.mark.parametrize("missing", REQUIRED_ATTRS)
def test_missing_attrs(self, attrs, missing):
name = "null" if missing == "name" else "name"
msg = f"Missing required attributes in query {name}: {set([missing])!r}"
del attrs[missing.replace("_", "-")]
with pytest.raises(SettingsError, match=msg):
check_yaml_entry(**attrs)

@pytest.mark.parametrize("missing", permutations(REQUIRED_ATTRS))
def test_missing_multiple_attrs(self, attrs, missing):
set_missing = list(permutations(missing))
set_missing = [
"{" + "%s" % ", ".join(repr(k) for k in x) + "}" for x in set_missing
]
name = "null" if "name" in missing else "name"
set_missing = f"({'|'.join(set_missing)})"
msg = f"Missing required attributes in query {name}: {set_missing}"

for key in missing:
del attrs[key.replace("_", "-")]
with pytest.raises(SettingsError, match=msg):
check_yaml_entry(**attrs)

def test_ok_zipped_folder(self):
attrs = {
"name": "name",
"root-path": "/home/test",
"type": "multiple-files",
"zip": True,
"zipname": "a.zip",
}
result = check_yaml_entry(**attrs)
assert result == {
"name": "name",
"root_path": "/home/test",
"type": "multiple-files",
"zip": True,
"zipname": "a.zip",
}

def test_error_zipped_folder(self):
attrs = {
"name": "name",
"root-path": "/home/test",
"type": "multiple-files",
"zip": True,
}

with pytest.raises(SettingsError, match="Must provide 'zipname'"):
check_yaml_entry(**attrs)

def test_ok_unzipped_folder(self):
attrs = {
"name": "name",
"root-path": "/home/test",
"type": "multiple-files",
"zip": False,
"zipname": "a.zip",
}
result = check_yaml_entry(**attrs)
assert result == {
"name": "name",
"root_path": "/home/test",
"type": "multiple-files",
"zip": False,
"zipname": "a.zip",
}

def test_invalid_entry_type(self, attrs):
attrs["type"] = "invalid-type"
with pytest.raises(TypeError, match="'invalid-type' is not a valid Entrytype"):
check_yaml_entry(**attrs)

@pytest.mark.parametrize("attribute", ATTRS_TYPES.keys() - {"type"})
def test_invalid_types(self, attrs, attribute):
for attr in ATTRS_TYPES.keys():
if attr == attribute:
continue
if attr in attrs:
continue
attrs[attr] = ATTRS_TYPES[attr](2)
attrs[attribute] = 1 + 2j

match = f"{attribute!r} must be {ATTRS_TYPES[attribute].__name__!r}, not 'complex'"
if attribute not in REQUIRED_ATTRS:
match ="If defined, "+match
with pytest.raises(TypeError, match=match):
check_yaml_entry(**attrs)

0 comments on commit a0d6c5f

Please sign in to comment.