Skip to content

Commit

Permalink
Expression evaluation for retention periods
Browse files Browse the repository at this point in the history
  • Loading branch information
xolox committed Jul 8, 2016
1 parent f5c3c90 commit 5817d01
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 24 deletions.
18 changes: 12 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,10 @@ Please use the ``--dry-run`` option to test the effect of the specified rotation

"``-H``, ``--hourly=COUNT``","Set the number of hourly backups to preserve during rotation:

- If ``COUNT`` is an integer it gives the number of hourly backups to preserve,
- If ``COUNT`` is a number it gives the number of hourly backups to preserve,
starting from the most recent hourly backup and counting back in time.
- Alternatively you can provide an expression that will be evaluated to get
a number (e.g. if ``COUNT`` is ""7 \* 2"" the result would be 14).
- You can also pass ""always"" for ``COUNT``, in this case all hourly backups are
preserved.
- By default no hourly backups are preserved."
Expand Down Expand Up @@ -176,8 +178,8 @@ Please use the ``--dry-run`` option to test the effect of the specified rotation
remote system over SSH)."
"``-n``, ``--dry-run``","Don't make any changes, just print what would be done. This makes it easy
to evaluate the impact of a rotation scheme without losing any backups."
"``-v``, ``--verbose``",Make more noise (increase logging verbosity).
"``-q``, ``--quiet``",Make less noise (decrease logging verbosity).
"``-v``, ``--verbose``",Make more noise (increase logging verbosity). Can be repeated.
"``-q``, ``--quiet``",Make less noise (decrease logging verbosity). Can be repeated.
"``-h``, ``--help``","Show this message and exit.
"

Expand Down Expand Up @@ -216,9 +218,9 @@ make regular backups of:
ionice = idle
[/backups/server]
daily = 7
weekly = 4
monthly = 12
daily = 7 * 2
weekly = 4 * 2
monthly = 12 * 4
yearly = always
ionice = idle
Expand All @@ -234,6 +236,10 @@ make regular backups of:
monthly = 2
ionice = idle
As you can see in the retention periods of the directory ``/backups/server`` in
the example above you are allowed to use expressions that evaluate to a number
(instead of having to write out the literal number).

Here's an example of a configuration for two remote directories:

.. code-block:: ini
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ humanfriendly >= 1.44.5
naturalsort >= 1.4
property-manager >= 2.0
python-dateutil >= 2.2
simpleeval >= 0.8.7
six >= 1.9.0
verboselogs >= 1.4
47 changes: 31 additions & 16 deletions rotate_backups/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import collections
import datetime
import fnmatch
import numbers
import os
import re

Expand All @@ -26,12 +27,13 @@
from humanfriendly.text import compact, concatenate, split
from natsort import natsort
from property_manager import PropertyManager, key_property, required_property
from simpleeval import simple_eval
from six import string_types
from six.moves import configparser
from verboselogs import VerboseLogger

# Semi-standard module versioning.
__version__ = '3.3'
__version__ = '3.4'

# Initialize a logger for this module.
logger = VerboseLogger(__name__)
Expand Down Expand Up @@ -90,10 +92,8 @@ def coerce_location(value, **options):
:func:`~executor.contexts.create_context()`.
:returns: A :class:`Location` object.
"""
if isinstance(value, Location):
# Location objects pass through untouched.
return value
else:
# Location objects pass through untouched.
if not isinstance(value, Location):
# Other values are expected to be strings.
if not isinstance(value, string_types):
msg = "Expected Location object or string, got %s instead!"
Expand All @@ -104,25 +104,40 @@ def coerce_location(value, **options):
options['ssh_alias'] = ssh_alias
else:
directory = value
return Location(context=create_context(**options),
directory=parse_path(directory))
# Construct the location object.
value = Location(
context=create_context(**options),
directory=parse_path(directory),
)
return value


def coerce_retention_period(value):
"""
Coerce a retention period to a Python value.
:param value: A string containing an integer number or the text 'always'.
:returns: An integer number or the string 'always'.
:param value: A string containing the text 'always', a number or
an expression that can be evaluated to a number.
:returns: A number or the string 'always'.
:raises: :exc:`~exceptions.ValueError` when the string can't be coerced.
"""
value = value.strip()
if value.lower() == 'always':
return 'always'
elif value.isdigit():
return int(value)
else:
raise ValueError("Invalid retention period! (%s)" % value)
# Numbers pass through untouched.
if not isinstance(value, numbers.Number):
# Other values are expected to be strings.
if not isinstance(value, string_types):
msg = "Expected string, got %s instead!"
raise ValueError(msg % type(value))
# Check for the literal string `always'.
value = value.strip()
if value.lower() == 'always':
value = 'always'
else:
# Evaluate other strings as expressions.
value = simple_eval(value)
if not isinstance(value, numbers.Number):
msg = "Expected numeric result, got %s instead!"
raise ValueError(msg % type(value))
return value


def load_config_file(configuration_file=None):
Expand Down
4 changes: 3 additions & 1 deletion rotate_backups/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
Set the number of hourly backups to preserve during rotation:
- If COUNT is an integer it gives the number of hourly backups to preserve,
- If COUNT is a number it gives the number of hourly backups to preserve,
starting from the most recent hourly backup and counting back in time.
- Alternatively you can provide an expression that will be evaluated to get
a number (e.g. if COUNT is `7 * 2' the result would be 14).
- You can also pass `always' for COUNT, in this case all hourly backups are
preserved.
- By default no hourly backups are preserved.
Expand Down
11 changes: 10 additions & 1 deletion rotate_backups/tests.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Test suite for the `rotate-backups' Python package.
#
# Author: Peter Odding <peter@peterodding.com>
# Last Change: April 13, 2016
# Last Change: July 8, 2016
# URL: https://github.com/xolox/python-rotate-backups

"""Test suite for the `rotate-backups` package."""
Expand All @@ -22,6 +22,7 @@
from rotate_backups import (
RotateBackups,
coerce_location,
coerce_retention_period,
load_config_file,
)
from rotate_backups.cli import main
Expand Down Expand Up @@ -95,6 +96,14 @@ def setUp(self):
"""Enable verbose logging for the test suite."""
coloredlogs.install(level=logging.DEBUG)

def test_retention_period_coercion(self):
"""Test coercion of retention period expressions."""
assert coerce_retention_period('always') == 'always'
assert coerce_retention_period('Always') == 'always'
assert coerce_retention_period(42) == 42
assert coerce_retention_period('42') == 42
assert coerce_retention_period('21 * 2') == 42

def test_argument_validation(self):
"""Test argument validation."""
# Test that an invalid ionice scheduling class causes an error to be reported.
Expand Down

0 comments on commit 5817d01

Please sign in to comment.