Repository: garnaat/placebo

Issue:
Optional prefix for saved response filenames
I noticed that each time Placebo writes a new response JSON file, it adds a `_<number>` suffix before the `.json`, and simply increments the suffix until it finds one that doesn't exist yet. This is better than overwriting previous runs, but doesn't give any way to distinguish multiple runs.

What would be really nice, is if you could optionally set a `prefix` argument on a `Pill` object. This prefix would be applied to all filenames written by the pill, like so: `prefix.iam.ListUserPolicies_1.json`.

I can think of two good use cases for this:

1. Define the prefix uniquely on each execution, using a command line argument, timestamp, or some other generated value. Now individual executions can be reloaded and replayed, or even diffed for comparison.
2. For an application that uses multiple Boto sessions, define a different prefix for each session. For example, I can use `base` for my initial credentials session, `mfa` for my new session after MFA authentication, and `<rolename>` for a session created using an IAM role.  I can even combine that with a timestamp (as in case 1) to separate multiple runs.

Discussion:

Patch:
diff --git a/placebo/__init__.py b/placebo/__init__.py
index 43e34d6..8dc9470 100644
--- a/placebo/__init__.py
+++ b/placebo/__init__.py
@@ -15,7 +15,7 @@
 from placebo.pill import Pill
 
 
-def attach(session, data_path, debug=False):
-    pill = Pill(debug=debug)
+def attach(session, data_path, prefix=None, debug=False):
+    pill = Pill(prefix=prefix, debug=debug)
     pill.attach(session, data_path)
     return pill
diff --git a/placebo/pill.py b/placebo/pill.py
index 472402e..2c0cdd2 100644
--- a/placebo/pill.py
+++ b/placebo/pill.py
@@ -35,10 +35,11 @@ class Pill(object):
 
     clients = []
 
-    def __init__(self, debug=False):
+    def __init__(self, prefix=None, debug=False):
         if debug:
             self._set_logger(__name__, logging.DEBUG)
         self.filename_re = re.compile(r'.*\..*_(?P<index>\d+).json')
+        self.prefix = prefix
         self._uuid = str(uuid.uuid4())
         self._data_path = None
         self._mode = None
@@ -52,6 +53,10 @@ class Pill(object):
     def mode(self):
         return self._mode
 
+    @property
+    def data_path(self):
+        return self._data_path
+
     def _set_logger(self, logger_name, level=logging.INFO):
         """
         Convenience function to quickly configure full debug output
@@ -178,6 +183,8 @@ class Pill(object):
 
     def get_new_file_path(self, service, operation):
         base_name = '{}.{}'.format(service, operation)
+        if self.prefix:
+            base_name = '{}.{}'.format(self.prefix, base_name)
         LOG.debug('get_new_file_path: %s', base_name)
         index = 0
         glob_pattern = os.path.join(self._data_path, base_name + '*')
@@ -194,6 +201,8 @@ class Pill(object):
 
     def get_next_file_path(self, service, operation):
         base_name = '{}.{}'.format(service, operation)
+        if self.prefix:
+            base_name = '{}.{}'.format(self.prefix, base_name)
         LOG.debug('get_next_file_path: %s', base_name)
         next_file = None
         while next_file is None:


Test patch:
diff --git a/tests/unit/responses/saved/foo.ec2.DescribeAddresses_1.json b/tests/unit/responses/saved/foo.ec2.DescribeAddresses_1.json
new file mode 100644
index 0000000..a907bf1
--- /dev/null
+++ b/tests/unit/responses/saved/foo.ec2.DescribeAddresses_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "Addresses": [
+            {
+                "PublicIp": "42.43.44.45",
+                "Domain": "vpc",
+                "AllocationId": "eipalloc-87654321"
+            }
+        ],
+        "ResponseMetadata": {
+            "HTTPStatusCode": 200,
+            "RequestId": "b0fe7bb2-6552-4ea8-8a36-9685044048ab"
+        }
+    }
+}
diff --git a/tests/unit/test_canned.py b/tests/unit/test_canned.py
index 913459d..c25905a 100644
--- a/tests/unit/test_canned.py
+++ b/tests/unit/test_canned.py
@@ -58,3 +58,23 @@ class TestPlacebo(unittest.TestCase):
         self.assertEqual(len(result['KeyPairs']), 2)
         self.assertEqual(result['KeyPairs'][0]['KeyName'], 'FooBar')
         self.assertEqual(result['KeyPairs'][1]['KeyName'], 'FieBaz')
+
+    def test_prefix_new_file_path(self):
+        self.pill.prefix = 'foo'
+        service = 'ec2'
+        operation = 'DescribeAddresses'
+        filename = '{}.{}.{}_2.json'.format(self.pill.prefix, service,
+                                            operation)
+        target = os.path.join(self.data_path, filename)
+        self.assertEqual(self.pill.get_new_file_path(service, operation),
+                         target)
+
+    def test_prefix_next_file_path(self):
+        self.pill.prefix = 'foo'
+        service = 'ec2'
+        operation = 'DescribeAddresses'
+        filename = '{}.{}.{}_1.json'.format(self.pill.prefix, service,
+                                            operation)
+        target = os.path.join(self.data_path, filename)
+        self.assertEqual(self.pill.get_next_file_path(service, operation),
+                         target)