Configuration support for additional attic prune flags: keep_within, keep_hourly, keep_yearly, and prefix.
This commit is contained in:
parent
b94c106a36
commit
056ed7184b
9 changed files with 341 additions and 124 deletions
|
@ -1,3 +1,4 @@
|
||||||
syntax: glob
|
syntax: glob
|
||||||
*.pyc
|
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
|
8
NEWS
Normal file
8
NEWS
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
0.0.2
|
||||||
|
|
||||||
|
* Configuration support for additional attic prune flags: keep_within, keep_hourly, keep_yearly,
|
||||||
|
and prefix.
|
||||||
|
|
||||||
|
0.0.1
|
||||||
|
|
||||||
|
* Initial release.
|
|
@ -5,6 +5,10 @@ import subprocess
|
||||||
|
|
||||||
|
|
||||||
def create_archive(excludes_filename, verbose, source_directories, repository):
|
def create_archive(excludes_filename, verbose, source_directories, repository):
|
||||||
|
'''
|
||||||
|
Given an excludes filename, a vebosity flag, a space-separated list of source directories, and
|
||||||
|
a local or remote repository path, create an attic archive.
|
||||||
|
'''
|
||||||
sources = tuple(source_directories.split(' '))
|
sources = tuple(source_directories.split(' '))
|
||||||
|
|
||||||
command = (
|
command = (
|
||||||
|
@ -22,13 +26,40 @@ def create_archive(excludes_filename, verbose, source_directories, repository):
|
||||||
subprocess.check_call(command)
|
subprocess.check_call(command)
|
||||||
|
|
||||||
|
|
||||||
def prune_archives(repository, verbose, keep_daily, keep_weekly, keep_monthly):
|
def make_prune_flags(retention_config):
|
||||||
|
'''
|
||||||
|
Given a retention config dict mapping from option name to value, tranform it into an iterable of
|
||||||
|
command-line name-value flag pairs.
|
||||||
|
|
||||||
|
For example, given a retention config of:
|
||||||
|
|
||||||
|
{'keep_weekly': 4, 'keep_monthly': 6}
|
||||||
|
|
||||||
|
This will be returned as an iterable of:
|
||||||
|
|
||||||
|
(
|
||||||
|
('--keep-weekly', '4'),
|
||||||
|
('--keep-monthly', '6'),
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
return (
|
||||||
|
('--' + option_name.replace('_', '-'), str(retention_config[option_name]))
|
||||||
|
for option_name, value in retention_config.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def prune_archives(verbose, repository, retention_config):
|
||||||
|
'''
|
||||||
|
Given a verbosity flag, a local or remote repository path, and a retention config dict, prune
|
||||||
|
attic archives according the the retention policy specified in that configuration.
|
||||||
|
'''
|
||||||
command = (
|
command = (
|
||||||
'attic', 'prune',
|
'attic', 'prune',
|
||||||
repository,
|
repository,
|
||||||
'--keep-daily', str(keep_daily),
|
) + tuple(
|
||||||
'--keep-weekly', str(keep_weekly),
|
element
|
||||||
'--keep-monthly', str(keep_monthly),
|
for pair in make_prune_flags(retention_config)
|
||||||
|
for element in pair
|
||||||
) + (('--verbose',) if verbose else ())
|
) + (('--verbose',) if verbose else ())
|
||||||
|
|
||||||
subprocess.check_call(command)
|
subprocess.check_call(command)
|
||||||
|
|
|
@ -38,8 +38,8 @@ def main():
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
location_config, retention_config = parse_configuration(args.config_filename)
|
location_config, retention_config = parse_configuration(args.config_filename)
|
||||||
|
|
||||||
create_archive(args.excludes_filename, args.verbose, *location_config)
|
create_archive(args.excludes_filename, args.verbose, **location_config)
|
||||||
prune_archives(location_config.repository, args.verbose, *retention_config)
|
prune_archives(args.verbose, location_config['repository'], retention_config)
|
||||||
except (ValueError, IOError, CalledProcessError) as error:
|
except (ValueError, IOError, CalledProcessError) as error:
|
||||||
print(error, file=sys.stderr)
|
print(error, file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from collections import namedtuple
|
from collections import OrderedDict, namedtuple
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Python 2
|
# Python 2
|
||||||
|
@ -8,58 +8,121 @@ except ImportError:
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SECTION_LOCATION = 'location'
|
Section_format = namedtuple('Section_format', ('name', 'options'))
|
||||||
CONFIG_SECTION_RETENTION = 'retention'
|
Config_option = namedtuple('Config_option', ('name', 'value_type', 'required'))
|
||||||
|
|
||||||
CONFIG_FORMAT = {
|
|
||||||
CONFIG_SECTION_LOCATION: ('source_directories', 'repository'),
|
def option(name, value_type=str, required=True):
|
||||||
CONFIG_SECTION_RETENTION: ('keep_daily', 'keep_weekly', 'keep_monthly'),
|
'''
|
||||||
|
Given a config file option name, an expected type for its value, and whether it's required,
|
||||||
|
return a Config_option capturing that information.
|
||||||
|
'''
|
||||||
|
return Config_option(name, value_type, required)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_FORMAT = (
|
||||||
|
Section_format(
|
||||||
|
'location',
|
||||||
|
(
|
||||||
|
option('source_directories'),
|
||||||
|
option('repository'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Section_format(
|
||||||
|
'retention',
|
||||||
|
(
|
||||||
|
option('keep_within', required=False),
|
||||||
|
option('keep_hourly', int, required=False),
|
||||||
|
option('keep_daily', int, required=False),
|
||||||
|
option('keep_weekly', int, required=False),
|
||||||
|
option('keep_monthly', int, required=False),
|
||||||
|
option('keep_yearly', int, required=False),
|
||||||
|
option('prefix', required=False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_configuration_format(parser, config_format):
|
||||||
|
'''
|
||||||
|
Given an open ConfigParser and an expected config file format, validate that the parsed
|
||||||
|
configuration file has the expected sections, that any required options are present in those
|
||||||
|
sections, and that there aren't any unexpected options.
|
||||||
|
|
||||||
|
Raise ValueError if anything is awry.
|
||||||
|
'''
|
||||||
|
section_names = parser.sections()
|
||||||
|
required_section_names = tuple(section.name for section in config_format)
|
||||||
|
|
||||||
|
if set(section_names) != set(required_section_names):
|
||||||
|
raise ValueError(
|
||||||
|
'Expected config sections {} but found sections: {}'.format(
|
||||||
|
', '.join(required_section_names),
|
||||||
|
', '.join(section_names)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for section_format in config_format:
|
||||||
|
option_names = parser.options(section_format.name)
|
||||||
|
expected_options = section_format.options
|
||||||
|
|
||||||
|
unexpected_option_names = set(option_names) - set(option.name for option in expected_options)
|
||||||
|
|
||||||
|
if unexpected_option_names:
|
||||||
|
raise ValueError(
|
||||||
|
'Unexpected options found in config section {}: {}'.format(
|
||||||
|
section_format.name,
|
||||||
|
', '.join(sorted(unexpected_option_names)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
missing_option_names = tuple(
|
||||||
|
option.name for option in expected_options if option.required
|
||||||
|
if option.name not in option_names
|
||||||
|
)
|
||||||
|
|
||||||
|
if missing_option_names:
|
||||||
|
raise ValueError(
|
||||||
|
'Required options missing from config section {}: {}'.format(
|
||||||
|
section_format.name,
|
||||||
|
', '.join(missing_option_names)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_section_options(parser, section_format):
|
||||||
|
'''
|
||||||
|
Given an open ConfigParser and an expected section format, return the option values from that
|
||||||
|
section as a dict mapping from option name to value. Omit those options that are not present in
|
||||||
|
the parsed options.
|
||||||
|
|
||||||
|
Raise ValueError if any option values cannot be coerced to the expected Python data type.
|
||||||
|
'''
|
||||||
|
type_getter = {
|
||||||
|
str: parser.get,
|
||||||
|
int: parser.getint,
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationConfig = namedtuple('LocationConfig', CONFIG_FORMAT[CONFIG_SECTION_LOCATION])
|
return OrderedDict(
|
||||||
RetentionConfig = namedtuple('RetentionConfig', CONFIG_FORMAT[CONFIG_SECTION_RETENTION])
|
(option.name, type_getter[option.value_type](section_format.name, option.name))
|
||||||
|
for option in section_format.options
|
||||||
|
if parser.has_option(section_format.name, option.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_configuration(config_filename):
|
def parse_configuration(config_filename):
|
||||||
'''
|
'''
|
||||||
Given a config filename of the expected format, return the parse configuration as a tuple of
|
Given a config filename of the expected format, return the parsed configuration as a tuple of
|
||||||
(LocationConfig, RetentionConfig).
|
(location config, retention config) where each config is a dict of that section's options.
|
||||||
|
|
||||||
Raise IOError if the file cannot be read, or ValueError if the format is not as expected.
|
Raise IOError if the file cannot be read, or ValueError if the format is not as expected.
|
||||||
'''
|
'''
|
||||||
parser = ConfigParser()
|
parser = ConfigParser()
|
||||||
parser.readfp(open(config_filename))
|
parser.readfp(open(config_filename))
|
||||||
section_names = parser.sections()
|
|
||||||
expected_section_names = CONFIG_FORMAT.keys()
|
|
||||||
|
|
||||||
if set(section_names) != set(expected_section_names):
|
validate_configuration_format(parser, CONFIG_FORMAT)
|
||||||
raise ValueError(
|
|
||||||
'Expected config sections {} but found sections: {}'.format(
|
|
||||||
', '.join(expected_section_names),
|
|
||||||
', '.join(section_names)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for section_name in section_names:
|
return tuple(
|
||||||
option_names = parser.options(section_name)
|
parse_section_options(parser, section_format)
|
||||||
expected_option_names = CONFIG_FORMAT[section_name]
|
for section_format in CONFIG_FORMAT
|
||||||
|
|
||||||
if set(option_names) != set(expected_option_names):
|
|
||||||
raise ValueError(
|
|
||||||
'Expected options {} in config section {} but found options: {}'.format(
|
|
||||||
', '.join(expected_option_names),
|
|
||||||
section_name,
|
|
||||||
', '.join(option_names)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
LocationConfig(*(
|
|
||||||
parser.get(CONFIG_SECTION_LOCATION, option_name)
|
|
||||||
for option_name in CONFIG_FORMAT[CONFIG_SECTION_LOCATION]
|
|
||||||
)),
|
|
||||||
RetentionConfig(*(
|
|
||||||
parser.getint(CONFIG_SECTION_RETENTION, option_name)
|
|
||||||
for option_name in CONFIG_FORMAT[CONFIG_SECTION_RETENTION]
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
from atticmatic import attic as module
|
from atticmatic import attic as module
|
||||||
|
@ -52,7 +54,32 @@ def test_create_archive_with_verbose_should_call_attic_with_verbose_parameters()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
BASE_PRUNE_FLAGS = (
|
||||||
|
('--keep-daily', '1'),
|
||||||
|
('--keep-weekly', '2'),
|
||||||
|
('--keep-monthly', '3'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_prune_flags_should_return_flags_from_config():
|
||||||
|
retention_config = OrderedDict(
|
||||||
|
(
|
||||||
|
('keep_daily', 1),
|
||||||
|
('keep_weekly', 2),
|
||||||
|
('keep_monthly', 3),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = module.make_prune_flags(retention_config)
|
||||||
|
|
||||||
|
assert tuple(result) == BASE_PRUNE_FLAGS
|
||||||
|
|
||||||
|
|
||||||
def test_prune_archives_should_call_attic_with_parameters():
|
def test_prune_archives_should_call_attic_with_parameters():
|
||||||
|
retention_config = flexmock()
|
||||||
|
flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return(
|
||||||
|
BASE_PRUNE_FLAGS,
|
||||||
|
)
|
||||||
insert_subprocess_mock(
|
insert_subprocess_mock(
|
||||||
(
|
(
|
||||||
'attic', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly',
|
'attic', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly',
|
||||||
|
@ -61,15 +88,17 @@ def test_prune_archives_should_call_attic_with_parameters():
|
||||||
)
|
)
|
||||||
|
|
||||||
module.prune_archives(
|
module.prune_archives(
|
||||||
repository='repo',
|
|
||||||
verbose=False,
|
verbose=False,
|
||||||
keep_daily=1,
|
repository='repo',
|
||||||
keep_weekly=2,
|
retention_config=retention_config,
|
||||||
keep_monthly=3
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_prune_archives_with_verbose_should_call_attic_with_verbose_parameters():
|
def test_prune_archives_with_verbose_should_call_attic_with_verbose_parameters():
|
||||||
|
retention_config = flexmock()
|
||||||
|
flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return(
|
||||||
|
BASE_PRUNE_FLAGS,
|
||||||
|
)
|
||||||
insert_subprocess_mock(
|
insert_subprocess_mock(
|
||||||
(
|
(
|
||||||
'attic', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly',
|
'attic', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly',
|
||||||
|
@ -80,7 +109,5 @@ def test_prune_archives_with_verbose_should_call_attic_with_verbose_parameters()
|
||||||
module.prune_archives(
|
module.prune_archives(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
verbose=True,
|
verbose=True,
|
||||||
keep_daily=1,
|
retention_config=retention_config,
|
||||||
keep_weekly=2,
|
|
||||||
keep_monthly=3
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,84 +1,166 @@
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
from nose.tools import assert_raises
|
from nose.tools import assert_raises
|
||||||
|
|
||||||
from atticmatic import config as module
|
from atticmatic import config as module
|
||||||
|
|
||||||
|
|
||||||
def insert_mock_parser(section_names):
|
def test_option_should_create_config_option():
|
||||||
|
option = module.option('name', bool, required=False)
|
||||||
|
|
||||||
|
assert option == module.Config_option('name', bool, False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_option_should_create_config_option_with_defaults():
|
||||||
|
option = module.option('name')
|
||||||
|
|
||||||
|
assert option == module.Config_option('name', str, True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_configuration_format_with_valid_config_should_not_raise():
|
||||||
|
parser = flexmock()
|
||||||
|
parser.should_receive('sections').and_return(('section', 'other'))
|
||||||
|
parser.should_receive('options').with_args('section').and_return(('stuff',))
|
||||||
|
parser.should_receive('options').with_args('other').and_return(('such',))
|
||||||
|
config_format = (
|
||||||
|
module.Section_format(
|
||||||
|
'section',
|
||||||
|
options=(
|
||||||
|
module.Config_option('stuff', str, required=True),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
module.Section_format(
|
||||||
|
'other',
|
||||||
|
options=(
|
||||||
|
module.Config_option('such', str, required=True),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
module.validate_configuration_format(parser, config_format)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_configuration_format_with_missing_section_should_raise():
|
||||||
|
parser = flexmock()
|
||||||
|
parser.should_receive('sections').and_return(('section',))
|
||||||
|
config_format = (
|
||||||
|
module.Section_format('section', options=()),
|
||||||
|
module.Section_format('missing', options=()),
|
||||||
|
)
|
||||||
|
|
||||||
|
with assert_raises(ValueError):
|
||||||
|
module.validate_configuration_format(parser, config_format)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_configuration_format_with_extra_section_should_raise():
|
||||||
|
parser = flexmock()
|
||||||
|
parser.should_receive('sections').and_return(('section', 'extra'))
|
||||||
|
config_format = (
|
||||||
|
module.Section_format('section', options=()),
|
||||||
|
)
|
||||||
|
|
||||||
|
with assert_raises(ValueError):
|
||||||
|
module.validate_configuration_format(parser, config_format)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_configuration_format_with_missing_required_option_should_raise():
|
||||||
|
parser = flexmock()
|
||||||
|
parser.should_receive('sections').and_return(('section',))
|
||||||
|
parser.should_receive('options').with_args('section').and_return(('option',))
|
||||||
|
config_format = (
|
||||||
|
module.Section_format(
|
||||||
|
'section',
|
||||||
|
options=(
|
||||||
|
module.Config_option('option', str, required=True),
|
||||||
|
module.Config_option('missing', str, required=True),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with assert_raises(ValueError):
|
||||||
|
module.validate_configuration_format(parser, config_format)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_configuration_format_with_missing_optional_option_should_not_raise():
|
||||||
|
parser = flexmock()
|
||||||
|
parser.should_receive('sections').and_return(('section',))
|
||||||
|
parser.should_receive('options').with_args('section').and_return(('option',))
|
||||||
|
config_format = (
|
||||||
|
module.Section_format(
|
||||||
|
'section',
|
||||||
|
options=(
|
||||||
|
module.Config_option('option', str, required=True),
|
||||||
|
module.Config_option('missing', str, required=False),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
module.validate_configuration_format(parser, config_format)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_configuration_format_with_extra_option_should_raise():
|
||||||
|
parser = flexmock()
|
||||||
|
parser.should_receive('sections').and_return(('section',))
|
||||||
|
parser.should_receive('options').with_args('section').and_return(('option', 'extra'))
|
||||||
|
config_format = (
|
||||||
|
module.Section_format(
|
||||||
|
'section',
|
||||||
|
options=(module.Config_option('option', str, required=True),),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with assert_raises(ValueError):
|
||||||
|
module.validate_configuration_format(parser, config_format)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_section_options_should_return_section_options():
|
||||||
|
parser = flexmock()
|
||||||
|
parser.should_receive('get').with_args('section', 'foo').and_return('value')
|
||||||
|
parser.should_receive('getint').with_args('section', 'bar').and_return(1)
|
||||||
|
parser.should_receive('has_option').with_args('section', 'foo').and_return(True)
|
||||||
|
parser.should_receive('has_option').with_args('section', 'bar').and_return(True)
|
||||||
|
|
||||||
|
section_format = module.Section_format(
|
||||||
|
'section',
|
||||||
|
(
|
||||||
|
module.Config_option('foo', str, required=True),
|
||||||
|
module.Config_option('bar', int, required=True),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
config = module.parse_section_options(parser, section_format)
|
||||||
|
|
||||||
|
assert config == OrderedDict(
|
||||||
|
(
|
||||||
|
('foo', 'value'),
|
||||||
|
('bar', 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def insert_mock_parser():
|
||||||
parser = flexmock()
|
parser = flexmock()
|
||||||
parser.should_receive('readfp')
|
parser.should_receive('readfp')
|
||||||
parser.should_receive('sections').and_return(section_names)
|
|
||||||
flexmock(module).open = lambda filename: None
|
flexmock(module).open = lambda filename: None
|
||||||
flexmock(module).ConfigParser = parser
|
flexmock(module).ConfigParser = parser
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def test_parse_configuration_should_return_config_data():
|
def test_parse_configuration_should_return_section_configs():
|
||||||
section_names = (module.CONFIG_SECTION_LOCATION, module.CONFIG_SECTION_RETENTION)
|
parser = insert_mock_parser()
|
||||||
parser = insert_mock_parser(section_names)
|
mock_module = flexmock(module)
|
||||||
|
mock_module.should_receive('validate_configuration_format').with_args(
|
||||||
|
parser, module.CONFIG_FORMAT,
|
||||||
|
).once()
|
||||||
|
mock_section_configs = (flexmock(), flexmock())
|
||||||
|
|
||||||
for section_name in section_names:
|
for section_format, section_config in zip(module.CONFIG_FORMAT, mock_section_configs):
|
||||||
parser.should_receive('options').with_args(section_name).and_return(
|
mock_module.should_receive('parse_section_options').with_args(
|
||||||
module.CONFIG_FORMAT[section_name],
|
parser, section_format,
|
||||||
)
|
).and_return(section_config).once()
|
||||||
|
|
||||||
expected_config = (
|
section_configs = module.parse_configuration('filename')
|
||||||
module.LocationConfig(flexmock(), flexmock()),
|
|
||||||
module.RetentionConfig(flexmock(), flexmock(), flexmock()),
|
|
||||||
)
|
|
||||||
sections = (
|
|
||||||
(module.CONFIG_SECTION_LOCATION, expected_config[0], 'get'),
|
|
||||||
(module.CONFIG_SECTION_RETENTION, expected_config[1], 'getint'),
|
|
||||||
)
|
|
||||||
|
|
||||||
for section_name, section_config, method_name in sections:
|
assert section_configs == mock_section_configs
|
||||||
for index, option_name in enumerate(module.CONFIG_FORMAT[section_name]):
|
|
||||||
(
|
|
||||||
parser.should_receive(method_name).with_args(section_name, option_name)
|
|
||||||
.and_return(section_config[index])
|
|
||||||
)
|
|
||||||
|
|
||||||
config = module.parse_configuration(flexmock())
|
|
||||||
|
|
||||||
assert config == expected_config
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_configuration_with_missing_section_should_raise():
|
|
||||||
insert_mock_parser((module.CONFIG_SECTION_LOCATION,))
|
|
||||||
|
|
||||||
with assert_raises(ValueError):
|
|
||||||
module.parse_configuration(flexmock())
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_configuration_with_extra_section_should_raise():
|
|
||||||
insert_mock_parser((module.CONFIG_SECTION_LOCATION, module.CONFIG_SECTION_RETENTION, 'extra'))
|
|
||||||
|
|
||||||
with assert_raises(ValueError):
|
|
||||||
module.parse_configuration(flexmock())
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_configuration_with_missing_option_should_raise():
|
|
||||||
section_names = (module.CONFIG_SECTION_LOCATION, module.CONFIG_SECTION_RETENTION)
|
|
||||||
parser = insert_mock_parser(section_names)
|
|
||||||
|
|
||||||
for section_name in section_names:
|
|
||||||
parser.should_receive('options').with_args(section_name).and_return(
|
|
||||||
module.CONFIG_FORMAT[section_name][:-1],
|
|
||||||
)
|
|
||||||
|
|
||||||
with assert_raises(ValueError):
|
|
||||||
module.parse_configuration(flexmock())
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_configuration_with_extra_option_should_raise():
|
|
||||||
section_names = (module.CONFIG_SECTION_LOCATION, module.CONFIG_SECTION_RETENTION)
|
|
||||||
parser = insert_mock_parser(section_names)
|
|
||||||
|
|
||||||
for section_name in section_names:
|
|
||||||
parser.should_receive('options').with_args(section_name).and_return(
|
|
||||||
module.CONFIG_FORMAT[section_name] + ('extra',),
|
|
||||||
)
|
|
||||||
|
|
||||||
with assert_raises(ValueError):
|
|
||||||
module.parse_configuration(flexmock())
|
|
||||||
|
|
|
@ -7,6 +7,11 @@ repository: user@backupserver:sourcehostname.attic
|
||||||
|
|
||||||
[retention]
|
[retention]
|
||||||
# Retention policy for how many backups to keep in each category.
|
# Retention policy for how many backups to keep in each category.
|
||||||
|
# See https://attic-backup.org/usage.html#attic-prune for details.
|
||||||
|
#keep_within: 3h
|
||||||
|
#keep_hourly: 24
|
||||||
keep_daily: 7
|
keep_daily: 7
|
||||||
keep_weekly: 4
|
keep_weekly: 4
|
||||||
keep_monthly: 6
|
keep_monthly: 6
|
||||||
|
keep_yearly: 1
|
||||||
|
#prefix: sourcehostname
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='atticmatic',
|
name='atticmatic',
|
||||||
version='0.0.1',
|
version='0.0.2',
|
||||||
description='A wrapper script for Attic backup software that creates and prunes backups',
|
description='A wrapper script for Attic backup software that creates and prunes backups',
|
||||||
author='Dan Helfman',
|
author='Dan Helfman',
|
||||||
author_email='witten@torsion.org',
|
author_email='witten@torsion.org',
|
||||||
|
|
Loading…
Reference in a new issue