Compare commits

...
23 Commits
Author SHA1 Message Date
Dan d4f52e3137 Update AUTHORS with recent changes + sort. 2018-01-02 20:33:27 -08:00
Dan 8b2ebdc5f7 Simplifying example. 2018-01-02 20:31:06 -08:00
Dan HelfmanandGitea a00407256d Merge branch 'keep_minutely' of thomasleveil/borgmatic into master 2018-01-03 04:30:13 +00:00
Thomas LEVEIL 24b5eccefc add support for Borg --keep-minutely prune option 2018-01-03 00:13:44 +01:00
Dan 815fb39a05 Declare dependency on pykwalify 1.6 or above, as older versions yield "Unknown key: version" rule errors. 2017-11-26 10:30:31 -08:00
Dan 24c196d2a4 Script to find unsupported Borg options in borgmatic, to assist with #13. 2017-11-11 16:07:54 -08:00
Dan 3e26e70d0c Fix for incorrect /etc/borgmatic.d/ configuration path probing on macOS. 2017-11-10 21:33:29 -08:00
Dan 5ce25e2790 Re-fixing logo image. 2017-11-04 11:55:48 -07:00
Dan 8243552c8c Fixing PNG path. 2017-11-04 11:54:29 -07:00
Dan 425e27dee5 Add "ssh_command" to configuration for specifying a custom SSH command or options. 2017-11-03 22:01:04 -07:00
Dan 9ec9269a18 Link to repository encryption section of Borg Quick Start. 2017-11-03 20:28:31 -07:00
Dan bf5cbd1deb Mentioning use of BORG_PASSPHRASE environment variable. 2017-11-03 20:27:21 -07:00
Dan 4c09cbf1a4 Releasing. 2017-11-02 22:38:16 -07:00
Dan fc077af4ce Mentioning borgmatic --config option in docs. 2017-11-02 22:28:53 -07:00
Dan ca4312bb85 Support for Borg --remote-ratelimit for limiting upload rate. And log Borg commands. 2017-11-02 22:22:40 -07:00
Dan fc3b1fccba Support for Borg --files-cache option for setting the files cache operation mode. 2017-11-02 22:03:11 -07:00
Dan f83346b9b3 Support for using tilde in repository paths to reference home directory. 2017-11-02 21:34:04 -07:00
Dan 63c7241aec Typo in comment. 2017-10-31 22:21:49 -07:00
Dan fd77dc579e Pass through several more Unix signals that Borg happens to consume. 2017-10-31 22:10:00 -07:00
Dan f017ed648f Disabling code coverage on this one-line functions. 2017-10-31 22:01:18 -07:00
Dan 27a6745743 Passing the Unix SIGTERM signal through to child processes like Borg. 2017-10-31 21:58:35 -07:00
Dan 95be0c8e46 Removing broken download URL. 2017-10-29 21:42:01 -07:00
Dan 17a774ba7e Removing .hg* files. 2017-10-29 20:32:21 -07:00
18 changed files with 230 additions and 75 deletions
-9
View File
@@ -1,9 +0,0 @@
syntax: glob
*.egg-info
*.pyc
*.swp
.cache
.coverage
.tox
build
dist
-40
View File
@@ -1,40 +0,0 @@
467d3a3ce9185e84ee51ca9156499162efd94f9a 0.0.2
7730ae34665c0dedf46deab90b32780abf6dbaff 0.0.3
4bb2e81fc77038be4499b7ea6797ab7d109460e0 0.0.4
b31d51b633701554e84f996cc0c73bad2990780b 0.0.5
b31d51b633701554e84f996cc0c73bad2990780b 0.0.5
aa8a807f4ba28f0652764ed14713ffea2fd6922d 0.0.5
aa8a807f4ba28f0652764ed14713ffea2fd6922d 0.0.5
569aef47a9b25c55b13753f94706f5d330219995 0.0.5
569aef47a9b25c55b13753f94706f5d330219995 0.0.5
a03495a8e8b471da63b5e2ae79d3ff9065839c2a 0.0.5
7ea93ca83f426ec0a608a68580c72c0775b81f86 0.0.6
cf4c7065f0711deda1cba878398bc05390e2c3f9 0.0.7
38d72677343f0a5d6845f4ac50d6778397083d45 0.1.0
ac5dfa01e9d14d09845f5e94c2c679e21c5eb2f9 0.1.1
ac5dfa01e9d14d09845f5e94c2c679e21c5eb2f9 0.1.1
7b6c87dca7ea312b2257ac1b46857b3f8c56b39c 0.1.1
83067f995dd391e38544a7722dc3b254b59c5521 0.1.2
acc7fb61566fe8028c179f43ecc735c851220b06 0.1.3
6dda59c12de88f060eb7244e6d330173985a9639 0.1.4
6dda59c12de88f060eb7244e6d330173985a9639 0.1.4
e58246fc92bb22c2b2fd8b86a1227de69d2d0315 0.1.4
0afff209b902698c2266986129d6dc9f5f913101 0.1.5
4c63f3d90ec2bf6af1714a3acec84654a7c9edf3 0.1.6
5a458ebef804be14e30d7375e3e9fbc26aedb80d 0.1.7
977f19c2f6a515be6c5ef69cf17b0e0989532209 github/yaml_config_files
0000000000000000000000000000000000000000 github/yaml_config_files
28434dd0440cc8da44c2f3e9bd7e9402a59c3b40 github/master
dbc96d3f83bd5570b6826537616d4160b3374836 0.1.8
0e1fbee9358de4f062fa9539e1355db83db70caa 1.0.0
de2d7721cdec93a52d20222a9ddd579ed93c1017 1.0.1
9603d13910b32d57a887765cab694ac5d0acc1f4 1.0.2
32c6341dda9fad77a3982641bce8a3a45821842e 1.0.3
5a003056a8ff4709c5bd4d6d33354199423f8a1d 1.1.0
7d3d11eff6c0773883c48f221431f157bc7995eb 1.1.1
f052a77a8ad5a0fea7fa86a902e0e401252f7d80 1.1.2
3f838f661546e04529b453aa443529b432afc243 1.1.3
3d605962d891731a0f372b903b556ac7a8c8359f 1.1.4
64ca13bfe050f656b44ed2eb1c3db045bfddd133 1.1.5
4daa944c122c572b9b56bfcac3f4e2869181c630 1.1.6
ec7949a14a2051616f7cdcb8e05555f02e024ae8 1.1.7
+2 -1
View File
@@ -2,7 +2,8 @@ Dan Helfman <witten@torsion.org>: Main developer
Alexander Görtz: Python 3 compatibility
Henning Schroeder: Copy editing
Johannes Feichtner: Support for user hooks
Michele Lazzeri: Custom archive names
Robin `ypid` Schneider: Support additional options of Borg
Scott Squires: Custom archive names
Johannes Feichtner: Support for user hooks
Thomas LÉVEIL: Support for a keep_minutely prune option
+17
View File
@@ -1,3 +1,20 @@
1.1.12.dev0
* #45: Declare dependency on pykwalify 1.6 or above, as older versions yield "Unknown key: version"
rule errors.
1.1.11
* #25: Add "ssh_command" to configuration for specifying a custom SSH command or options.
* Fix for incorrect /etc/borgmatic.d/ configuration path probing on macOS. This problem manifested
as an error on startup: "[Errno 2] No such file or directory: '/etc/borgmatic.d'".
1.1.10
* Pass several Unix signals through to child processes like Borg. This means that Borg now properly
shuts down if borgmatic is terminated (e.g. due to a system suspend).
* #29: Support for using tilde in repository paths to reference home directory.
* #42: Support for Borg --files-cache option for setting the files cache operation mode.
* #44: Support for Borg --remote-ratelimit for limiting upload rate.
* Log invoked Borg commands when at highest verbosity level.
1.1.9
* #16, #38: Support for user-defined hooks before/after backup, or on error.
* #33: Improve clarity of logging spew at high verbosity levels.
+12 -6
View File
@@ -1,4 +1,4 @@
<img src="https://projects.torsion.org/witten/borgmatic/raw/master/static/borgmatic.png" width="150px" style="float: right; padding-left: 1em;">
<img src="https://projects.torsion.org/witten/borgmatic/raw/branch/master/static/borgmatic.png" width="150px" style="float: right; padding-left: 1em;">
## Overview
@@ -49,11 +49,13 @@ on [GitHub](https://github.com/witten/borgmatic) for convenience.
To get up and running, follow the [Borg Quick
Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) to create
a repository on a local or remote host. Note that if you plan to run
borgmatic on a schedule with cron, and you encrypt your Borg repository with
a passphrase instead of a key file, you'll need to set the borgmatic
`encryption_passphrase` configuration variable. See the repository encryption
section of the Quick Start for more info.
a repository on a local or remote host. Note that if you plan to run borgmatic
on a schedule with cron, and you encrypt your Borg repository with a
passphrase instead of a key file, you'll either need to set the borgmatic
`encryption_passphrase` configuration variable or set the `BORG_PASSPHRASE`
environment variable. See the [repository encryption
section](https://borgbackup.readthedocs.io/en/latest/quickstart.html#repository-encryption)
of the Quick Start for more info.
If the repository is on a remote host, make sure that your local root user has
key-based ssh access to the desired user account on the remote host.
@@ -113,6 +115,10 @@ When you set up multiple configuration files like this, borgmatic will run
each one in turn from a single borgmatic invocation. This includes, by
default, the traditional /etc/borgmatic/config.yaml as well.
And if you need even more customizability, you can specify alternate
configuration paths on the command-line with borgmatic's `--config` option.
See `borgmatic --help` for more information.
## Upgrading
+5
View File
@@ -1,3 +1,4 @@
import logging
import os
import subprocess
@@ -8,6 +9,9 @@ from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
DEFAULT_CHECKS = ('repository', 'archives')
logger = logging.getLogger(__name__)
def _parse_checks(consistency_config):
'''
Given a consistency config with a "checks" list, transform it to a tuple of named checks to run.
@@ -79,6 +83,7 @@ def check_archives(verbosity, repository, consistency_config, remote_path=None):
# The check command spews to stdout/stderr even without the verbose flag. Suppress it.
stdout = None if verbosity_flags else open(os.devnull, 'w')
logger.debug(' '.join(full_command))
subprocess.check_call(full_command, stdout=stdout, stderr=subprocess.STDOUT)
if 'extract' in checks:
+17 -4
View File
@@ -1,5 +1,6 @@
import glob
import itertools
import logging
import os
import subprocess
import tempfile
@@ -7,12 +8,18 @@ import tempfile
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
def initialize(storage_config):
passphrase = storage_config.get('encryption_passphrase')
logger = logging.getLogger(__name__)
def initialize_environment(storage_config):
passphrase = storage_config.get('encryption_passphrase')
if passphrase:
os.environ['BORG_PASSPHRASE'] = passphrase
ssh_command = storage_config.get('ssh_command')
if ssh_command:
os.environ['BORG_RSH'] = ssh_command
def _expand_directory(directory):
'''
@@ -81,9 +88,13 @@ def create_archive(
)
compression = storage_config.get('compression', None)
compression_flags = ('--compression', compression) if compression else ()
remote_rate_limit = storage_config.get('remote_rate_limit', None)
remote_rate_limit_flags = ('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ()
umask = storage_config.get('umask', None)
umask_flags = ('--umask', str(umask)) if umask else ()
one_file_system_flags = ('--one-file-system',) if location_config.get('one_file_system') else ()
files_cache = location_config.get('files_cache')
files_cache_flags = ('--files-cache', files_cache) if files_cache else ()
remote_path = location_config.get('remote_path')
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
verbosity_flags = {
@@ -99,7 +110,9 @@ def create_archive(
repository=repository,
archive_name_format=archive_name_format,
),
) + sources + exclude_flags + compression_flags + one_file_system_flags + \
remote_path_flags + umask_flags + verbosity_flags
) + sources + exclude_flags + compression_flags + remote_rate_limit_flags + \
one_file_system_flags + files_cache_flags + remote_path_flags + umask_flags + \
verbosity_flags
logger.debug(' '.join(full_command))
subprocess.check_call(full_command)
+5
View File
@@ -1,9 +1,13 @@
import logging
import sys
import subprocess
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
logger = logging.getLogger(__name__)
def extract_last_archive_dry_run(verbosity, repository, remote_path=None):
'''
Perform an extraction dry-run of just the most recent archive. If there are no archives, skip
@@ -37,4 +41,5 @@ def extract_last_archive_dry_run(verbosity, repository, remote_path=None):
),
) + remote_path_flags + verbosity_flags + list_flag
logger.debug(' '.join(full_extract_command))
subprocess.check_call(full_extract_command)
+5
View File
@@ -1,8 +1,12 @@
import logging
import subprocess
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
logger = logging.getLogger(__name__)
def _make_prune_flags(retention_config):
'''
Given a retention config dict mapping from option name to value, tranform it into an iterable of
@@ -48,4 +52,5 @@ def prune_archives(verbosity, repository, retention_config, remote_path=None):
for element in pair
) + remote_path_flags + verbosity_flags
logger.debug(' '.join(full_command))
subprocess.check_call(full_command)
+5 -2
View File
@@ -8,6 +8,7 @@ import sys
from borgmatic.borg import check, create, prune
from borgmatic.commands import hook
from borgmatic.config import collect, convert, validate
from borgmatic.signals import configure_signals
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS, verbosity_to_log_level
@@ -93,10 +94,11 @@ def run_configuration(config_filename, args): # pragma: no cover
try:
remote_path = location.get('remote_path')
create.initialize(storage)
create.initialize_environment(storage)
hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup')
for repository in location['repositories']:
for unexpanded_repository in location['repositories']:
repository = os.path.expanduser(unexpanded_repository)
if args.prune:
logger.info('{}: Pruning archives'.format(repository))
prune.prune_archives(args.verbosity, repository, retention, remote_path=remote_path)
@@ -120,6 +122,7 @@ def run_configuration(config_filename, args): # pragma: no cover
def main(): # pragma: no cover
try:
configure_signals()
args = parse_arguments(*sys.argv[1:])
logging.basicConfig(level=verbosity_to_log_level(args.verbosity), format='%(message)s')
+5 -3
View File
@@ -11,13 +11,15 @@ def collect_config_filenames(config_paths):
files. This is non-recursive, so any directories within the given directories are ignored.
Return paths even if they don't exist on disk, so the user can find out about missing
configuration paths. However, skip /etc/borgmatic.d if it's missing, so the user doesn't have to
create it unless they need it.
configuration paths. However, skip a default config path if it's missing, so the user doesn't
have to create a default config path unless they need it.
'''
real_default_config_paths = set(map(os.path.realpath, DEFAULT_CONFIG_PATHS))
for path in config_paths:
exists = os.path.exists(path)
if os.path.realpath(path) in DEFAULT_CONFIG_PATHS and not exists:
if os.path.realpath(path) in real_default_config_paths and not exists:
continue
if not os.path.isdir(path) or not exists:
+21 -2
View File
@@ -22,6 +22,13 @@ map:
type: bool
desc: Stay in same file system (do not cross mount points).
example: true
files_cache:
type: scalar
desc: |
Mode in which to operate the files cache. See
https://borgbackup.readthedocs.io/en/stable/usage/create.html#description for
details.
example: ctime,size,inode
remote_path:
type: scalar
desc: Alternate Borg remote executable. Defaults to "borg".
@@ -31,8 +38,8 @@ map:
seq:
- type: scalar
desc: |
Paths to local or remote repositories (required). Multiple repositories are
backed up to in sequence.
Paths to local or remote repositories (required). Tildes are expanded. Multiple
repositories are backed up to in sequence.
example:
- user@backupserver:sourcehostname.borg
exclude_patterns:
@@ -85,6 +92,14 @@ map:
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-create for details.
Defaults to no compression.
example: lz4
remote_rate_limit:
type: int
desc: Remote network upload rate limit in kiBytes/second.
example: 100
ssh_command:
type: scalar
desc: Command to use instead of just "ssh". This can be used to specify ssh options.
example: ssh -i /path/to/private/key
umask:
type: scalar
desc: Umask to be used for borg create.
@@ -107,6 +122,10 @@ map:
type: scalar
desc: Keep all archives within this time interval.
example: 3H
keep_minutely:
type: int
desc: Number of minutely archives to keep.
example: 60
keep_hourly:
type: int
desc: Number of hourly archives to keep.
+18
View File
@@ -0,0 +1,18 @@
import os
import signal
def _handle_signal(signal_number, frame): # pragma: no cover
'''
Send the signal to all processes in borgmatic's process group, which includes child process.
'''
os.killpg(os.getpgrp(), signal_number)
def configure_signals(): # pragma: no cover
'''
Configure borgmatic's signal handlers to pass relevant signals through to any child processes
like Borg. Note that SIGINT gets passed through even without these changes.
'''
for signal_number in (signal.SIGHUP, signal.SIGTERM, signal.SIGUSR1, signal.SIGUSR2):
signal.signal(signal_number, _handle_signal)
@@ -44,6 +44,8 @@ def test_parse_configuration_transforms_file_into_mapping():
- hostname.borg
retention:
keep_minutely: 60
keep_hourly: 24
keep_daily: 7
consistency:
@@ -57,7 +59,7 @@ def test_parse_configuration_transforms_file_into_mapping():
assert result == {
'location': {'source_directories': ['/home', '/etc'], 'repositories': ['hostname.borg']},
'retention': {'keep_daily': 7},
'retention': {'keep_daily': 7, 'keep_hourly': 24, 'keep_minutely': 60},
'consistency': {'checks': ['repository', 'archives']},
}
+53 -4
View File
@@ -6,24 +6,36 @@ from borgmatic.borg import create as module
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
def test_initialize_with_passphrase_should_set_environment():
def test_initialize_environment_with_passphrase_should_set_environment():
orig_environ = os.environ
try:
os.environ = {}
module.initialize({'encryption_passphrase': 'pass'})
module.initialize_environment({'encryption_passphrase': 'pass'})
assert os.environ.get('BORG_PASSPHRASE') == 'pass'
finally:
os.environ = orig_environ
def test_initialize_without_passphrase_should_not_set_environment():
def test_initialize_environment_with_ssh_command_should_set_environment():
orig_environ = os.environ
try:
os.environ = {}
module.initialize({})
module.initialize_environment({'ssh_command': 'ssh -C'})
assert os.environ.get('BORG_RSH') == 'ssh -C'
finally:
os.environ = orig_environ
def test_initialize_environment_without_configuration_should_not_set_environment():
orig_environ = os.environ
try:
os.environ = {}
module.initialize_environment({})
assert os.environ.get('BORG_PASSPHRASE') == None
assert os.environ.get('BORG_RSH') == None
finally:
os.environ = orig_environ
@@ -231,6 +243,24 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
)
def test_create_archive_with_remote_rate_limit_calls_borg_with_remote_ratelimit_parameters():
flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar'])
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--remote-ratelimit', '100'))
module.create_archive(
verbosity=None,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={'remote_rate_limit': 100},
)
def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_parameters():
flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar'])
flexmock(module).should_receive('_write_exclude_file').and_return(None)
@@ -250,6 +280,25 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
)
def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters():
flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar'])
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--files-cache', 'ctime,size'))
module.create_archive(
verbosity=None,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'files_cache': 'ctime,size',
'exclude_patterns': None,
},
storage_config={},
)
def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters():
flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar'])
flexmock(module).should_receive('_write_exclude_file').and_return(None)
@@ -58,6 +58,19 @@ def test_collect_config_filenames_skips_etc_borgmatic_dot_d_if_it_does_not_exist
assert config_filenames == ('config.yaml',)
def test_collect_config_filenames_skips_non_canonical_etc_borgmatic_dot_d_if_it_does_not_exist():
config_paths = ('config.yaml', '/etc/../etc/borgmatic.d')
mock_path = flexmock(module.os.path)
mock_path.should_receive('exists').with_args('config.yaml').and_return(True)
mock_path.should_receive('exists').with_args('/etc/../etc/borgmatic.d').and_return(False)
mock_path.should_receive('isdir').with_args('config.yaml').and_return(False)
mock_path.should_receive('isdir').with_args('/etc/../etc/borgmatic.d').and_return(True)
config_filenames = tuple(module.collect_config_filenames(config_paths))
assert config_filenames == ('config.yaml',)
def test_collect_config_filenames_includes_other_directory_if_it_does_not_exist():
config_paths = ('config.yaml', '/my/directory')
mock_path = flexmock(module.os.path)
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# For each Borg sub-command that borgmatic uses, print out the Borg flags that borgmatic does not
# appear to support yet. This script isn't terribly robust. It's intended as a basic tool to ferret
# out unsupported Borg options so that they can be considered for addition to borgmatic.
# Generate a sample borgmatic configuration with all options set.
generate-borgmatic-config --destination temp.yaml
# For each sub-command (prune, create, and check), collect the Borg command-line flags that result
# from running borgmatic with the generated configuration. Then, collect the full set of available
# Borg flags as reported by "borg --help" for that sub-command. Finally, compare the two lists of
# flags to determine which Borg flags borgmatic doesn't yet support.
for sub_command in prune create check; do
echo "********** borg $sub_command **********"
for line in $(borgmatic --config temp.yaml --$sub_command -v 2 2>&1 | grep "borg $sub_command") ; do
echo "$line" | grep '^-' >> borgmatic_borg_flags
done
sort borgmatic_borg_flags > borgmatic_borg_flags.sorted
mv borgmatic_borg_flags.sorted borgmatic_borg_flags
for line in $(borg $sub_command --help | awk -v RS= '/^usage:/') ; do
# Exclude a bunch of flags that borgmatic actually supports, but don't get exercised by the
# generated sample config, and also flags that don't make sense to support.
echo "$line" | grep -- -- | sed -r 's/(\[|\])//g' \
| grep -v '^-h$' \
| grep -v '^--archives-only$' \
| grep -v '^--repository-only$' \
| grep -v '^--stats$' \
| grep -v '^--list$' \
| grep -v '^--critical$' \
| grep -v '^--error$' \
| grep -v '^--warning$' \
| grep -v '^--info$' \
| grep -v '^--debug$' \
>> all_borg_flags
done
sort all_borg_flags > all_borg_flags.sorted
mv all_borg_flags.sorted all_borg_flags
comm -13 borgmatic_borg_flags all_borg_flags
rm ./*_borg_flags
done
rm temp.yaml
+2 -3
View File
@@ -1,7 +1,7 @@
from setuptools import setup, find_packages
VERSION = '1.1.9'
VERSION = '1.1.12.dev0'
setup(
@@ -11,7 +11,6 @@ setup(
author='Dan Helfman',
author_email='witten@torsion.org',
url='https://torsion.org/borgmatic',
download_url='https://projects.torsion.org/witten/borgmatic/archive/%s.tar.gz' % VERSION,
classifiers=(
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
@@ -33,7 +32,7 @@ setup(
'atticmatic',
],
install_requires=(
'pykwalify',
'pykwalify>=1.6.0',
'ruamel.yaml<=0.15',
'setuptools',
),