Repository: boto/boto

Issue:
Add support for Listing Invalidations 
Current support of Invalidations is incomplete.  See:

http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/Actions_Invalidations.html



Discussion:

Resolving patch:
diff --git a/boto/cloudfront/__init__.py b/boto/cloudfront/__init__.py
--- a/boto/cloudfront/__init__.py
+++ b/boto/cloudfront/__init__.py
@@ -30,7 +30,7 @@
 from boto.cloudfront.identity import OriginAccessIdentity
 from boto.cloudfront.identity import OriginAccessIdentitySummary
 from boto.cloudfront.identity import OriginAccessIdentityConfig
-from boto.cloudfront.invalidation import InvalidationBatch
+from boto.cloudfront.invalidation import InvalidationBatch, InvalidationSummary, InvalidationListResultSet
 from boto.resultset import ResultSet
 from boto.cloudfront.exception import CloudFrontServerError
 
@@ -60,7 +60,8 @@ def _required_auth_capability(self):
 
     # Generics
 
-    def _get_all_objects(self, resource, tags):
+    def _get_all_objects(self, resource, tags, result_set_class=None,
+                         result_set_kwargs=None):
         if not tags:
             tags = [('DistributionSummary', DistributionSummary)]
         response = self.make_request('GET', '/%s/%s' % (self.Version,
@@ -69,7 +70,9 @@ def _get_all_objects(self, resource, tags):
         boto.log.debug(body)
         if response.status >= 300:
             raise CloudFrontServerError(response.status, response.reason, body)
-        rs = ResultSet(tags)
+        rs_class = result_set_class or ResultSet
+        rs_kwargs = result_set_kwargs or dict()
+        rs = rs_class(tags, **rs_kwargs)
         h = handler.XmlHandler(rs, self)
         xml.sax.parseString(body, h)
         return rs
@@ -269,3 +272,51 @@ def invalidation_request_status(self, distribution_id,
             return paths
         else:
             raise CloudFrontServerError(response.status, response.reason, body)
+
+    def get_invalidation_requests(self, distribution_id, marker=None,
+                                  max_items=None):
+        """
+        Get all invalidation requests for a given CloudFront distribution.
+        This returns an instance of an InvalidationListResultSet that
+        automatically handles all of the result paging, etc. from CF - you just
+        need to keep iterating until there are no more results.
+
+        :type distribution_id: string
+        :param distribution_id: The id of the CloudFront distribution
+
+        :type marker: string
+        :param marker: Use this only when paginating results and only in
+                       follow-up request after you've received a response where
+                       the results are truncated. Set this to the value of the
+                       Marker element in the response you just received.
+
+        :type max_items: int
+        :param max_items: Use this only when paginating results and only in a
+                          follow-up request to indicate the maximum number of
+                          invalidation requests you want in the response. You
+                          will need to pass the next_marker property from the
+                          previous InvalidationListResultSet response in the
+                          follow-up request in order to get the next 'page' of
+                          results.
+
+        :rtype: :class:`boto.cloudfront.invalidation.InvalidationListResultSet`
+        :returns: An InvalidationListResultSet iterator that lists invalidation
+                  requests for a given CloudFront distribution. Automatically
+                  handles paging the results.
+        """
+        uri = 'distribution/%s/invalidation' % distribution_id
+        params = dict()
+        if marker:
+            params['Marker'] = marker
+        if max_items:
+            params['MaxItems'] = max_items
+        if params:
+            uri += '?%s=%s' % params.popitem()
+            for k, v in params.items():
+                uri += '&%s=%s' % (k, v)
+        tags=[('InvalidationSummary', InvalidationSummary)]
+        rs_class = InvalidationListResultSet
+        rs_kwargs = dict(connection=self, distribution_id=distribution_id,
+                         max_items=max_items, marker=marker)
+        return self._get_all_objects(uri, tags, result_set_class=rs_class,
+                                     result_set_kwargs=rs_kwargs)
diff --git a/boto/cloudfront/invalidation.py b/boto/cloudfront/invalidation.py
--- a/boto/cloudfront/invalidation.py
+++ b/boto/cloudfront/invalidation.py
@@ -22,6 +22,9 @@
 import uuid
 import urllib
 
+from boto.resultset import ResultSet
+
+
 class InvalidationBatch(object):
     """A simple invalidation request.
         :see: http://docs.amazonwebservices.com/AmazonCloudFront/2010-08-01/APIReference/index.html?InvalidationBatchDatatype.html
@@ -44,6 +47,9 @@ def __init__(self, paths=None, connection=None, distribution=None, caller_refere
         else:
             self.connection = connection
 
+    def __repr__(self):
+        return '<InvalidationBatch: %s>' % self.id
+
     def add(self, path):
         """Add another path to this invalidation request"""
         return self.paths.append(path)
@@ -95,3 +101,116 @@ def endElement(self, name, value, connection):
         elif name == "CallerReference":
             self.caller_reference = value
         return None
+
+
+class InvalidationListResultSet(object):
+    """
+    A resultset for listing invalidations on a given CloudFront distribution.
+    Implements the iterator interface and transparently handles paging results
+    from CF so even if you have many thousands of invalidations on the
+    distribution you can iterate over all invalidations in a reasonably
+    efficient manner.
+    """
+    def __init__(self, markers=None, connection=None, distribution_id=None,
+                 invalidations=None, marker='', next_marker=None,
+                 max_items=None, is_truncated=False):
+        self.markers = markers or []
+        self.connection = connection
+        self.distribution_id = distribution_id
+        self.marker = marker
+        self.next_marker = next_marker
+        self.max_items = max_items
+        self.auto_paginate = max_items is None
+        self.is_truncated = is_truncated
+        self._inval_cache = invalidations or []
+
+    def __iter__(self):
+        """
+        A generator function for listing invalidation requests for a given
+        CloudFront distribution.
+        """
+        conn = self.connection
+        distribution_id = self.distribution_id
+        result_set = self
+        for inval in result_set._inval_cache:
+            yield inval
+        if not self.auto_paginate:
+            return
+        while result_set.is_truncated:
+            result_set = conn.get_invalidation_requests(distribution_id,
+                                                        marker=result_set.next_marker,
+                                                        max_items=result_set.max_items)
+            for i in result_set._inval_cache:
+                yield i
+
+    def startElement(self, name, attrs, connection):
+        for root_elem, handler in self.markers:
+            if name == root_elem:
+                obj = handler(connection, distribution_id=self.distribution_id)
+                self._inval_cache.append(obj)
+                return obj
+
+    def endElement(self, name, value, connection):
+        if name == 'IsTruncated':
+            self.is_truncated = self.to_boolean(value)
+        elif name == 'Marker':
+            self.marker = value
+        elif name == 'NextMarker':
+            self.next_marker = value
+        elif name == 'MaxItems':
+            self.max_items = int(value)
+
+    def to_boolean(self, value, true_value='true'):
+        if value == true_value:
+            return True
+        else:
+            return False
+
+class InvalidationSummary(object):
+    """
+    Represents InvalidationSummary complex type in CloudFront API that lists
+    the id and status of a given invalidation request.
+    """
+    def __init__(self, connection=None, distribution_id=None, id='',
+                 status=''):
+        self.connection = connection
+        self.distribution_id = distribution_id
+        self.id = id
+        self.status = status
+
+    def __repr__(self):
+        return '<InvalidationSummary: %s>' % self.id
+
+    def startElement(self, name, attrs, connection):
+        pass
+
+    def endElement(self, name, value, connection):
+        if name == 'Id':
+            self.id = value
+        elif name == 'Status':
+            self.status = value
+
+    def get_distribution(self):
+        """
+        Returns a Distribution object representing the parent CloudFront
+        distribution of the invalidation request listed in the
+        InvalidationSummary.
+
+        :rtype: :class:`boto.cloudfront.distribution.Distribution`
+        :returns: A Distribution object representing the parent CloudFront
+                  distribution  of the invalidation request listed in the
+                  InvalidationSummary
+        """
+        return self.connection.get_distribution_info(self.distribution_id)
+
+    def get_invalidation_request(self):
+        """
+        Returns an InvalidationBatch object representing the invalidation
+        request referred to in the InvalidationSummary.
+
+        :rtype: :class:`boto.cloudfront.invalidation.InvalidationBatch`
+        :returns: An InvalidationBatch object representing the invalidation
+                  request referred to by the InvalidationSummary
+        """
+        return self.connection.invalidation_request_status(
+            self.distribution_id, self.id)
diff --git a/boto/resultset.py b/boto/resultset.py
--- a/boto/resultset.py
+++ b/boto/resultset.py
@@ -102,6 +102,8 @@ def endElement(self, name, value, connection):
             self.bucket = value
         elif name == 'MaxUploads':
             self.max_uploads = int(value)
+        elif name == 'MaxItems':
+            self.max_items = int(value)
         elif name == 'Prefix':
             self.prefix = value
         elif name == 'return':


Test patch:
diff --git a/tests/unit/cloudfront/test_invalidation_list.py b/tests/unit/cloudfront/test_invalidation_list.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/cloudfront/test_invalidation_list.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+import random
+import string
+from tests.unit import unittest
+
+import mock
+import boto
+
+
+RESPONSE_TEMPLATE = r"""
+<InvalidationList>
+   <Marker/>
+   <NextMarker>%(next_marker)s</NextMarker>
+   <MaxItems>%(max_items)s</MaxItems>
+   <IsTruncated>%(is_truncated)s</IsTruncated>
+   %(inval_summaries)s
+</InvalidationList>
+"""
+
+INVAL_SUMMARY_TEMPLATE = r"""
+   <InvalidationSummary>
+      <Id>%(cfid)s</Id>
+      <Status>%(status)s</Status>
+   </InvalidationSummary>
+"""
+
+
+class CFInvalidationListTest(unittest.TestCase):
+
+    cloudfront = True
+
+    def setUp(self):
+        self.cf = boto.connect_cloudfront('aws.aws_access_key_id',
+                                          'aws.aws_secret_access_key')
+
+    def _get_random_id(self, length=14):
+        return ''.join([random.choice(string.ascii_letters) for i in
+                        range(length)])
+
+    def _group_iter(self, iterator, n):
+        accumulator = []
+        for item in iterator:
+            accumulator.append(item)
+            if len(accumulator) == n:
+                yield accumulator
+                accumulator = []
+        if len(accumulator) != 0:
+            yield accumulator
+
+    def _get_mock_responses(self, num, max_items):
+        max_items = min(max_items, 100)
+        cfid_groups = list(self._group_iter([self._get_random_id() for i in
+                                             range(num)], max_items))
+        cfg = dict(status='Completed', max_items=max_items, next_marker='')
+        responses = []
+        is_truncated = 'true'
+        for i, group in enumerate(cfid_groups):
+            next_marker = group[-1]
+            if (i + 1) == len(cfid_groups):
+                is_truncated = 'false'
+                next_marker = ''
+            invals = ''
+            cfg.update(dict(next_marker=next_marker,
+                            is_truncated=is_truncated))
+            for cfid in group:
+                cfg.update(dict(cfid=cfid))
+                invals += INVAL_SUMMARY_TEMPLATE % cfg
+            cfg.update(dict(inval_summaries=invals))
+            mock_response = mock.Mock()
+            mock_response.read.return_value = RESPONSE_TEMPLATE % cfg
+            mock_response.status = 200
+            responses.append(mock_response)
+        return responses
+
+    def test_manual_pagination(self, num_invals=30, max_items=4):
+        """
+        Test that paginating manually works properly
+        """
+        self.assertGreater(num_invals, max_items)
+        responses = self._get_mock_responses(num=num_invals,
+                                             max_items=max_items)
+        self.cf.make_request = mock.Mock(side_effect=responses)
+        ir = self.cf.get_invalidation_requests('dist-id-here',
+                                               max_items=max_items)
+        all_invals = list(ir)
+        self.assertEqual(len(all_invals), max_items)
+        while ir.is_truncated:
+            ir = self.cf.get_invalidation_requests('dist-id-here',
+                                                   marker=ir.next_marker,
+                                                   max_items=max_items)
+            invals = list(ir)
+            self.assertLessEqual(len(invals), max_items)
+            all_invals.extend(invals)
+        remainder = num_invals % max_items
+        if remainder != 0:
+            self.assertEqual(len(invals), remainder)
+        self.assertEqual(len(all_invals), num_invals)
+
+    def test_auto_pagination(self, num_invals=1024):
+        """
+        Test that auto-pagination works properly
+        """
+        max_items = 100
+        self.assertGreaterEqual(num_invals, max_items)
+        responses = self._get_mock_responses(num=num_invals,
+                                             max_items=max_items)
+        self.cf.make_request = mock.Mock(side_effect=responses)
+        ir = self.cf.get_invalidation_requests('dist-id-here')
+        self.assertEqual(len(ir._inval_cache), max_items)
+        self.assertEqual(len(list(ir)), num_invals)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/unit/cloudfront/test_signed_urls.py b/tests/unit/cloudfront/test_signed_urls.py
--- a/tests/unit/cloudfront/test_signed_urls.py
+++ b/tests/unit/cloudfront/test_signed_urls.py
@@ -8,6 +8,9 @@
 from boto.cloudfront.distribution import Distribution
 
 class CloudfrontSignedUrlsTest(unittest.TestCase):
+
+    cloudfront = True
+
     def setUp(self):
         self.pk_str = dedent("""
             -----BEGIN RSA PRIVATE KEY-----