Author: tomaz
Date: Sun Jun 26 10:24:39 2011
New Revision: 1139754

URL: http://svn.apache.org/viewvc?rev=1139754&view=rev
Log:
1. Add an implementation of LazyList with tests
2. Modify S3 and CloudFiles storage driver to return LazyList instead of a
normal list in the list_container_objects_method

Code has been contributed by Danny Clark and Wiktor Kolodziej.

This change is part of LIBCLOUD-78.

Added:
    
libcloud/trunk/test/storage/fixtures/cloudfiles/list_container_objects_not_exhausted1.json
    
libcloud/trunk/test/storage/fixtures/cloudfiles/list_container_objects_not_exhausted2.json
    
libcloud/trunk/test/storage/fixtures/s3/list_container_objects_not_exhausted1.xml
    
libcloud/trunk/test/storage/fixtures/s3/list_container_objects_not_exhausted2.xml
    libcloud/trunk/test/test_types.py
Modified:
    libcloud/trunk/libcloud/common/types.py
    libcloud/trunk/libcloud/storage/drivers/cloudfiles.py
    libcloud/trunk/libcloud/storage/drivers/s3.py
    libcloud/trunk/test/storage/test_cloudfiles.py
    libcloud/trunk/test/storage/test_s3.py

Modified: libcloud/trunk/libcloud/common/types.py
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/libcloud/common/types.py?rev=1139754&r1=1139753&r2=1139754&view=diff
==============================================================================
--- libcloud/trunk/libcloud/common/types.py (original)
+++ libcloud/trunk/libcloud/common/types.py Sun Jun 26 10:24:39 2011
@@ -17,9 +17,11 @@ __all__ = [
     "LibcloudError",
     "MalformedResponseError",
     "InvalidCredsError",
-    "InvalidCredsException"
+    "InvalidCredsException",
+    "LazyList"
     ]
 
+
 class LibcloudError(Exception):
     """The base class for other libcloud exceptions"""
 
@@ -30,9 +32,10 @@ class LibcloudError(Exception):
     def __str__(self):
         return ("<LibcloudError in "
                 + repr(self.driver)
-                +" "
+                + " "
                 + repr(self.value) + ">")
 
+
 class MalformedResponseError(LibcloudError):
     """Exception for the cases when a provider returns a malformed
     response, e.g. you request JSON and provider returns
@@ -51,6 +54,7 @@ class MalformedResponseError(LibcloudErr
                 + ">: "
                 + repr(self.body))
 
+
 class InvalidCredsError(LibcloudError):
     """Exception used when invalid credentials are used on a provider."""
 
@@ -58,8 +62,53 @@ class InvalidCredsError(LibcloudError):
                  driver=None):
         self.value = value
         self.driver = driver
+
     def __str__(self):
         return repr(self.value)
 
+
 # Deprecated alias of L{InvalidCredsError}
 InvalidCredsException = InvalidCredsError
+
+
+class LazyList(object):
+
+    def __init__(self, get_more, value_dict=None):
+        self._data = []
+        self._last_key = None
+        self._exhausted = False
+        self._all_loaded = False
+        self._get_more = get_more
+        self._value_dict = value_dict or {}
+
+    def __iter__(self):
+        if not self._all_loaded:
+            self._load_all()
+
+        data = self._data
+        for i in data:
+            yield i
+
+    def __getitem__(self, index):
+        if index >= len(self._data) and not self._all_loaded:
+            self._load_all()
+
+        return self._data[index]
+
+    def __len__(self):
+        self._load_all()
+        return len(self._data)
+
+    def __repr__(self):
+        self._load_all()
+        repr_string = ', ' .join([repr(item) for item in self._data])
+        repr_string = '[%s]' % (repr_string)
+        return repr_string
+
+    def _load_all(self):
+        while not self._exhausted:
+            newdata, self._last_key, self._exhausted = \
+                     self._get_more(last_key=self._last_key,
+                                    value_dict=self._value_dict)
+            self._data.extend(newdata)
+        self._all_loaded = True

Modified: libcloud/trunk/libcloud/storage/drivers/cloudfiles.py
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/libcloud/storage/drivers/cloudfiles.py?rev=1139754&r1=1139753&r2=1139754&view=diff
==============================================================================
--- libcloud/trunk/libcloud/storage/drivers/cloudfiles.py (original)
+++ libcloud/trunk/libcloud/storage/drivers/cloudfiles.py Sun Jun 26 10:24:39 
2011
@@ -33,6 +33,7 @@ from libcloud.storage.types import Conta
 from libcloud.storage.types import ObjectDoesNotExistError
 from libcloud.storage.types import ObjectHashMismatchError
 from libcloud.storage.types import InvalidContainerNameError
+from libcloud.common.types import LazyList
 
 from libcloud.common.rackspace import (
     AUTH_HOST_US, AUTH_HOST_UK, RackspaceBaseConnection)
@@ -161,15 +162,8 @@ class CloudFilesStorageDriver(StorageDri
         raise LibcloudError('Unexpected status code: %s' % (response.status))
 
     def list_container_objects(self, container):
-        response = self.connection.request('/%s' % (container.name))
-
-        if response.status == httplib.NO_CONTENT:
-            # Empty or inexistent container
-            return []
-        elif response.status == httplib.OK:
-            return self._to_object_list(json.loads(response.body), container)
-
-        raise LibcloudError('Unexpected status code: %s' % (response.status))
+        value_dict = { 'container': container }
+        return LazyList(get_more=self._get_more, value_dict=value_dict)
 
     def get_container(self, container_name):
         response = self.connection.request('/%s' % (container_name),
@@ -354,6 +348,30 @@ class CloudFilesStorageDriver(StorageDri
 
         raise LibcloudError('Unexpected status code: %s' % (response.status))
 
+    def _get_more(self, last_key, value_dict):
+        container = value_dict['container']
+        params = {}
+
+        if last_key:
+            params['marker'] = last_key
+
+        response = self.connection.request('/%s' % (container.name),
+                                          params=params)
+
+        if response.status == httplib.NO_CONTENT:
+            # Empty or inexistent container
+            return [], None, True
+        elif response.status == httplib.OK:
+            objects = self._to_object_list(json.loads(response.body), 
container)
+
+            # TODO: Is this really needed?
+            if len(objects) == 0:
+                return [], None, True
+
+            return objects, objects[-1].name, False
+
+        raise LibcloudError('Unexpected status code: %s' % (response.status))
+
     def _put_object(self, container, object_name, upload_func,
                     upload_func_kwargs, extra=None, file_path=None,
                     iterator=None, verify_hash=True):

Modified: libcloud/trunk/libcloud/storage/drivers/s3.py
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/libcloud/storage/drivers/s3.py?rev=1139754&r1=1139753&r2=1139754&view=diff
==============================================================================
--- libcloud/trunk/libcloud/storage/drivers/s3.py (original)
+++ libcloud/trunk/libcloud/storage/drivers/s3.py Sun Jun 26 10:24:39 2011
@@ -35,6 +35,7 @@ from libcloud.storage.types import Inval
 from libcloud.storage.types import ContainerDoesNotExistError
 from libcloud.storage.types import ObjectDoesNotExistError
 from libcloud.storage.types import ObjectHashMismatchError
+from libcloud.common.types import LazyList
 
 # How long before the token expires
 EXPIRATION_SECONDS = 15 * 60
@@ -173,14 +174,8 @@ class S3StorageDriver(StorageDriver):
                             driver=self)
 
     def list_container_objects(self, container):
-        response = self.connection.request('/%s' % (container.name))
-        if response.status == httplib.OK:
-            objects = self._to_objs(obj=response.object,
-                                       xpath='Contents', container=container)
-            return objects
-
-        raise LibcloudError('Unexpected status code: %s' % (response.status),
-                            driver=self)
+        value_dict = { 'container': container }
+        return LazyList(get_more=self._get_more, value_dict=value_dict)
 
     def get_container(self, container_name):
         # This is very inefficient, but afaik it's the only way to do it
@@ -328,6 +323,32 @@ class S3StorageDriver(StorageDriver):
         name = urllib.quote(name)
         return name
 
+    def _get_more(self, last_key, value_dict):
+        container = value_dict['container']
+        params = {}
+
+        if last_key:
+            params['marker'] = last_key
+
+        response = self.connection.request('/%s' % (container.name),
+                                           params=params)
+
+        if response.status == httplib.OK:
+            objects = self._to_objs(obj=response.object,
+                                       xpath='Contents', container=container)
+            is_truncated = 
response.object.findtext(fixxpath(xpath='IsTruncated',
+                                                   
namespace=NAMESPACE)).lower()
+            exhausted = (is_truncated == 'false')
+
+            if (len(objects) > 0):
+                last_key = objects[-1].name
+            else:
+                last_key = None
+            return objects, last_key, exhausted
+
+        raise LibcloudError('Unexpected status code: %s' % (response.status),
+                            driver=self)
+
     def _put_object(self, container, object_name, upload_func,
                     upload_func_kwargs, extra=None, file_path=None,
                     iterator=None, verify_hash=True, storage_class=None):

Added: 
libcloud/trunk/test/storage/fixtures/cloudfiles/list_container_objects_not_exhausted1.json
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/test/storage/fixtures/cloudfiles/list_container_objects_not_exhausted1.json?rev=1139754&view=auto
==============================================================================
--- 
libcloud/trunk/test/storage/fixtures/cloudfiles/list_container_objects_not_exhausted1.json
 (added)
+++ 
libcloud/trunk/test/storage/fixtures/cloudfiles/list_container_objects_not_exhausted1.json
 Sun Jun 26 10:24:39 2011
@@ -0,0 +1,11 @@
+[
+    {"name":"foo-test-1","hash":"16265549b5bda64ecdaa5156de4c97cc",
+     "bytes":1160520,"content_type":"application/zip",
+     "last_modified":"2011-01-25T22:01:50.351810"},
+     {"name":"foo-test-2","hash":"16265549b5bda64ecdaa5156de4c97bb",
+     "bytes":1160520,"content_type":"application/zip",
+     "last_modified":"2011-01-25T22:01:50.351810"},
+    {"name":"foo-test-3","hash":"16265549b5bda64ecdaa5156de4c97ee",
+      "bytes":1160520,"content_type":"application/zip",
+      "last_modified":"2011-01-25T22:01:46.549890"}
+]

Added: 
libcloud/trunk/test/storage/fixtures/cloudfiles/list_container_objects_not_exhausted2.json
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/test/storage/fixtures/cloudfiles/list_container_objects_not_exhausted2.json?rev=1139754&view=auto
==============================================================================
--- 
libcloud/trunk/test/storage/fixtures/cloudfiles/list_container_objects_not_exhausted2.json
 (added)
+++ 
libcloud/trunk/test/storage/fixtures/cloudfiles/list_container_objects_not_exhausted2.json
 Sun Jun 26 10:24:39 2011
@@ -0,0 +1,8 @@
+[
+    {"name":"foo-test-4","hash":"16265549b5bda64ecdaa5156de4c97cc",
+     "bytes":1160520,"content_type":"application/zip",
+     "last_modified":"2011-01-25T22:01:50.351810"},
+     {"name":"foo-test-5","hash":"16265549b5bda64ecdaa5156de4c97bb",
+     "bytes":1160520,"content_type":"application/zip",
+     "last_modified":"2011-01-25T22:01:50.351810"}
+]

Added: 
libcloud/trunk/test/storage/fixtures/s3/list_container_objects_not_exhausted1.xml
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/test/storage/fixtures/s3/list_container_objects_not_exhausted1.xml?rev=1139754&view=auto
==============================================================================
--- 
libcloud/trunk/test/storage/fixtures/s3/list_container_objects_not_exhausted1.xml
 (added)
+++ 
libcloud/trunk/test/storage/fixtures/s3/list_container_objects_not_exhausted1.xml
 Sun Jun 26 10:24:39 2011
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+    <ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/";>
+    <Name>test_container</Name>
+    <Prefix></Prefix>
+    <Marker></Marker>
+    <MaxKeys>1000</MaxKeys>
+    <IsTruncated>true</IsTruncated>
+    <Contents>
+        <Key>1.zip</Key>
+        <LastModified>2011-04-09T19:05:18.000Z</LastModified>
+        <ETag>"4397da7a7649e8085de9916c240e8166"</ETag>
+        <Size>1234567</Size>
+        <Owner>
+            <ID>65a011niqo39cdf8ec533ec3d1ccaafsa932</ID>
+        </Owner>
+        <StorageClass>STANDARD</StorageClass>
+    </Contents>
+    <Contents>
+        <Key>2.zip</Key>
+        <LastModified>2011-04-09T19:05:18.000Z</LastModified>
+        <ETag>"4397da7a7649e8085de9916c240e8166"</ETag>
+        <Size>1234567</Size>
+        <Owner>
+            <ID>65a011niqo39cdf8ec533ec3d1ccaafsa932</ID>
+        </Owner>
+        <StorageClass>STANDARD</StorageClass>
+    </Contents>
+    <Contents>
+        <Key>3.zip</Key>
+        <LastModified>2011-04-09T19:05:18.000Z</LastModified>
+        <ETag>"4397da7a7649e8085de9916c240e8166"</ETag>
+        <Size>1234567</Size>
+        <Owner>
+            <ID>65a011niqo39cdf8ec533ec3d1ccaafsa932</ID>
+        </Owner>
+        <StorageClass>STANDARD</StorageClass>
+    </Contents>
+</ListBucketResult>

Added: 
libcloud/trunk/test/storage/fixtures/s3/list_container_objects_not_exhausted2.xml
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/test/storage/fixtures/s3/list_container_objects_not_exhausted2.xml?rev=1139754&view=auto
==============================================================================
--- 
libcloud/trunk/test/storage/fixtures/s3/list_container_objects_not_exhausted2.xml
 (added)
+++ 
libcloud/trunk/test/storage/fixtures/s3/list_container_objects_not_exhausted2.xml
 Sun Jun 26 10:24:39 2011
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+    <ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/";>
+    <Name>test_container</Name>
+    <Prefix></Prefix>
+    <Marker></Marker>
+    <MaxKeys>3</MaxKeys>
+    <IsTruncated>false</IsTruncated>
+    <Contents>
+        <Key>4.zip</Key>
+        <LastModified>2011-04-09T19:05:18.000Z</LastModified>
+        <ETag>"4397da7a7649e8085de9916c240e8166"</ETag>
+        <Size>1234567</Size>
+        <Owner>
+            <ID>65a011niqo39cdf8ec533ec3d1ccaafsa932</ID>
+        </Owner>
+        <StorageClass>STANDARD</StorageClass>
+    </Contents>
+    <Contents>
+        <Key>5.zip</Key>
+        <LastModified>2011-04-09T19:05:18.000Z</LastModified>
+        <ETag>"4397da7a7649e8085de9916c240e8166"</ETag>
+        <Size>1234567</Size>
+        <Owner>
+            <ID>65a011niqo39cdf8ec533ec3d1ccaafsa932</ID>
+        </Owner>
+        <StorageClass>STANDARD</StorageClass>
+    </Contents>
+</ListBucketResult>

Modified: libcloud/trunk/test/storage/test_cloudfiles.py
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/test/storage/test_cloudfiles.py?rev=1139754&r1=1139753&r2=1139754&view=diff
==============================================================================
--- libcloud/trunk/test/storage/test_cloudfiles.py (original)
+++ libcloud/trunk/test/storage/test_cloudfiles.py Sun Jun 26 10:24:39 2011
@@ -90,6 +90,18 @@ class CloudFilesTests(unittest.TestCase)
         self.assertEqual(obj.size, 1160520)
         self.assertEqual(obj.container.name, 'test_container')
 
+    def test_list_container_objects_iterator(self):
+        CloudFilesMockHttp.type = 'ITERATOR'
+        container = Container(
+            name='test_container', extra={}, driver=self.driver)
+        objects = self.driver.list_container_objects(container=container)
+        self.assertEqual(len(objects), 5)
+
+        obj = [o for o in objects if o.name == 'foo-test-1'][0]
+        self.assertEqual(obj.hash, '16265549b5bda64ecdaa5156de4c97cc')
+        self.assertEqual(obj.size, 1160520)
+        self.assertEqual(obj.container.name, 'test_container')
+
     def test_get_container(self):
         container = self.driver.get_container(container_name='test_container')
         self.assertEqual(container.name, 'test_container')
@@ -485,8 +497,12 @@ class CloudFilesMockHttp(StorageMockHttp
         headers = copy.deepcopy(self.base_headers)
         if method == 'GET':
             # list_container_objects
-            body = self.fixtures.load('list_container_objects.json')
-            status_code = httplib.OK
+            if url.find('marker') == -1:
+                body = self.fixtures.load('list_container_objects.json')
+                status_code = httplib.OK
+            else:
+                body = ''
+                status_code = httplib.NO_CONTENT
         elif method == 'HEAD':
             # get_container
             body = self.fixtures.load('list_container_objects_empty.json')
@@ -496,6 +512,22 @@ class CloudFilesMockHttp(StorageMockHttp
                            })
         return (status_code, body, headers, httplib.responses[httplib.OK])
 
+    def _v1_MossoCloudFS_test_container_ITERATOR(self, method, url, body, 
headers):
+        headers = copy.deepcopy(self.base_headers)
+        # list_container_objects
+        if url.find('foo-test-3') != -1:
+            body = 
self.fixtures.load('list_container_objects_not_exhausted2.json')
+            status_code = httplib.OK
+        elif url.find('foo-test-5') != -1:
+            body = ''
+            status_code = httplib.NO_CONTENT
+        else:
+            # First request
+            body = 
self.fixtures.load('list_container_objects_not_exhausted1.json')
+            status_code = httplib.OK
+
+        return (status_code, body, headers, httplib.responses[httplib.OK])
+
     def _v1_MossoCloudFS_test_container_not_found(
         self, method, url, body, headers):
         # test_get_container_not_found

Modified: libcloud/trunk/test/storage/test_s3.py
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/test/storage/test_s3.py?rev=1139754&r1=1139753&r2=1139754&view=diff
==============================================================================
--- libcloud/trunk/test/storage/test_s3.py (original)
+++ libcloud/trunk/test/storage/test_s3.py Sun Jun 26 10:24:39 2011
@@ -105,6 +105,20 @@ class S3Tests(unittest.TestCase):
         self.assertEqual(obj.container.name, 'test_container')
         self.assertTrue('owner' in obj.meta_data)
 
+    def test_list_container_objects_iterator_has_more(self):
+        S3MockHttp.type = 'ITERATOR'
+        container = Container(name='test_container', extra={},
+                              driver=self.driver)
+        objects = self.driver.list_container_objects(container=container)
+
+        obj = [o for o in objects if o.name == '1.zip'][0]
+        self.assertEqual(obj.hash, '4397da7a7649e8085de9916c240e8166')
+        self.assertEqual(obj.size, 1234567)
+        self.assertEqual(obj.container.name, 'test_container')
+
+        self.assertTrue(obj in objects)
+        self.assertEqual(len(objects), 5)
+
     def test_get_container_doesnt_exist(self):
         S3MockHttp.type = 'list_containers'
         try:
@@ -459,6 +473,19 @@ class S3MockHttp(StorageMockHttp):
                 self.base_headers,
                 httplib.responses[httplib.OK])
 
+    def _test_container_ITERATOR(self, method, url, body, headers):
+        if url.find('3.zip') == -1:
+            # First part of the response (first 3 objects)
+            body = 
self.fixtures.load('list_container_objects_not_exhausted1.xml')
+        else:
+            body = 
self.fixtures.load('list_container_objects_not_exhausted2.xml')
+
+        return (httplib.OK,
+                body,
+                self.base_headers,
+                httplib.responses[httplib.OK])
+
+
     def _test2_test_list_containers(self, method, url, body, headers):
         # test_get_object
         body = self.fixtures.load('list_containers.xml')

Added: libcloud/trunk/test/test_types.py
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/test/test_types.py?rev=1139754&view=auto
==============================================================================
--- libcloud/trunk/test/test_types.py (added)
+++ libcloud/trunk/test/test_types.py Sun Jun 26 10:24:39 2011
@@ -0,0 +1,112 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+import unittest
+
+from libcloud.common.types import LazyList
+
+
+class TestLazyList(unittest.TestCase):
+    def setUp(self):
+        super(TestLazyList, self).setUp
+        self._get_more_counter = 0
+
+    def tearDown(self):
+        super(TestLazyList, self).tearDown
+
+    def test_init(self):
+        data = [1, 2, 3, 4, 5]
+        ll = LazyList(get_more=self._get_more_exhausted)
+        ll_list = list(ll)
+        self.assertEqual(ll_list, data)
+
+    def test_iterator(self):
+        data = [1, 2, 3, 4, 5]
+        ll = LazyList(get_more=self._get_more_exhausted)
+        for i, d in enumerate(ll):
+            self.assertEqual(d, data[i])
+
+    def test_empty_list(self):
+        ll = LazyList(get_more=self._get_more_empty)
+
+        self.assertEqual(list(ll), [])
+        self.assertEqual(len(ll), 0)
+        self.assertTrue(10 not in ll)
+
+    def test_iterator_not_exhausted(self):
+        data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+        ll = LazyList(get_more=self._get_more_not_exhausted)
+        number_of_iterations = 0
+        for i, d in enumerate(ll):
+            self.assertEqual(d, data[i])
+            number_of_iterations += 1
+        self.assertEqual(number_of_iterations, 10)
+
+    def test_len(self):
+        ll = LazyList(get_more=self._get_more_not_exhausted)
+        ll = LazyList(get_more=self._get_more_not_exhausted)
+
+        self.assertEqual(len(ll), 10)
+
+    def test_contains(self):
+        ll = LazyList(get_more=self._get_more_not_exhausted)
+
+        self.assertTrue(40 not in ll)
+        self.assertTrue(1 in ll)
+        self.assertTrue(5 in ll)
+        self.assertTrue(10 in ll)
+
+    def test_indexing(self):
+        ll = LazyList(get_more=self._get_more_not_exhausted)
+
+        self.assertEqual(ll[0], 1)
+        self.assertEqual(ll[9], 10)
+        self.assertEqual(ll[-1], 10)
+
+        try:
+            ll[11]
+        except IndexError:
+            pass
+        else:
+            self.fail('Exception was not thrown')
+
+    def test_repr(self):
+        ll1 = LazyList(get_more=self._get_more_empty)
+        ll2 = LazyList(get_more=self._get_more_exhausted)
+        ll3 = LazyList(get_more=self._get_more_not_exhausted)
+
+        self.assertEqual(repr(ll1), '[]')
+        self.assertEqual(repr(ll2), '[1, 2, 3, 4, 5]')
+        self.assertEqual(repr(ll3), '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]')
+
+    def _get_more_empty(self, last_key, value_dict):
+        return [], None, True
+
+    def _get_more_exhausted(self, last_key, value_dict):
+        data = [1, 2, 3, 4, 5]
+        return data, 5, True
+
+    def _get_more_not_exhausted(self, last_key, value_dict):
+        self._get_more_counter += 1
+        if not last_key:
+            data, last_key, exhausted = [1, 2, 3, 4, 5], 5, False
+        else:
+            data, last_key, exhausted = [6, 7, 8, 9, 10], 10, True
+
+        return data, last_key, exhausted
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())


Reply via email to