Repository: box/box-python-sdk

Issue:
The get all users in enterprise API needs to be pageable
Right now it just return a list of :class:`User`

Discussion:

Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index b567bfa..9af5bb4 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -6,7 +6,17 @@ Release History
 Upcoming
 ++++++++
 
-1.3.2
+1.3.3 (2015-12-22)
+++++++++++++++++++
+
+- Added a new class, ``DeveloperTokenClient`` that makes it easy to get started using the SDK with a Box developer
+  token. It uses another new class, ``DeveloperTokenAuth`` for auth.
+
+**Bugfixes**
+
+- Added limit, offset, and filter_term parameters to ``client.users()`` to match up with the Box API.
+
+1.3.2 (2015-11-16)
 ++++++++++++++++++
 
 - Fix ``boxsdk.util.log.setup_logging()`` on Python 3.
diff --git a/boxsdk/client.py b/boxsdk/client.py
index 8a04d76..f84d9a3 100644
--- a/boxsdk/client.py
+++ b/boxsdk/client.py
@@ -100,21 +100,48 @@ def group(self, group_id):
         """
         return Group(session=self._session, object_id=group_id)
 
-    def users(self):
+    def users(self, limit=None, offset=0, filter_term=None):
         """
         Get a list of all users for the Enterprise along with their user_id, public_name, and login.
 
+        :param limit:
+            The maximum number of users to return. If not specified, the Box API will determine an appropriate limit.
+        :type limit:
+            `int` or None
+        :param offset:
+            The user index at which to start the response.
+        :type offset:
+            `int`
+        :param filter_term:
+            Filters the results to only users starting with the filter_term in either the name or the login.
+        :type filter_term:
+            `unicode` or None
         :return:
             The list of all users in the enterprise.
         :rtype:
             `list` of :class:`User`
         """
         url = '{0}/users'.format(API.BASE_API_URL)
-        box_response = self._session.get(url)
+        params = dict(offset=offset)
+        if limit is not None:
+            params['limit'] = limit
+        if filter_term is not None:
+            params['filter_term'] = filter_term
+        box_response = self._session.get(url, params=params)
         response = box_response.json()
         return [User(self._session, item['id'], item) for item in response['entries']]
 
-    def search(self, query, limit, offset, ancestor_folders=None, file_extensions=None, metadata_filters=None, result_type=None, content_types=None):
+    def search(
+            self,
+            query,
+            limit,
+            offset,
+            ancestor_folders=None,
+            file_extensions=None,
+            metadata_filters=None,
+            result_type=None,
+            content_types=None
+    ):
         """
         Search Box for items matching the given query.
 
@@ -155,14 +182,16 @@ def search(self, query, limit, offset, ancestor_folders=None, file_extensions=No
         :rtype:
             `list` of :class:`Item`
         """
-        return Search(self._session).search(query=query,
-                                            limit=limit,
-                                            offset=offset,
-                                            ancestor_folders=ancestor_folders,
-                                            file_extensions=file_extensions,
-                                            metadata_filters=metadata_filters,
-                                            result_type=result_type,
-                                            content_types=content_types)
+        return Search(self._session).search(
+            query=query,
+            limit=limit,
+            offset=offset,
+            ancestor_folders=ancestor_folders,
+            file_extensions=file_extensions,
+            metadata_filters=metadata_filters,
+            result_type=result_type,
+            content_types=content_types,
+        )
 
     def events(self):
         """
@@ -333,5 +362,8 @@ def with_shared_link(self, shared_link, shared_link_password):
 
 
 class DeveloperTokenClient(Client):
+    """
+    Box client subclass which authorizes with a developer token.
+    """
     def __init__(self, oauth=None, network_layer=None, session=None):
         super(DeveloperTokenClient, self).__init__(oauth or DeveloperTokenAuth(), network_layer, session)
diff --git a/setup.py b/setup.py
index 2d16b34..59885f9 100644
--- a/setup.py
+++ b/setup.py
@@ -61,7 +61,7 @@ def main():
         install_requires.append('ordereddict>=1.1')
     setup(
         name='boxsdk',
-        version='1.3.2',
+        version='1.3.3',
         description='Official Box Python SDK',
         long_description=open(join(base_dir, 'README.rst')).read(),
         author='Box',
@@ -69,7 +69,7 @@ def main():
         url='http://opensource.box.com',
         packages=find_packages(exclude=['demo', 'docs', 'test']),
         install_requires=install_requires,
-        extras_require={'jwt': jwt_requires, 'redis': redis_requires},
+        extras_require={'jwt': jwt_requires, 'redis': redis_requires, 'all': jwt_requires + redis_requires},
         tests_require=['pytest', 'pytest-xdist', 'mock', 'sqlalchemy', 'bottle', 'jsonpatch'],
         cmdclass={'test': PyTest},
         classifiers=CLASSIFIERS,


Test patch:
diff --git a/test/unit/test_client.py b/test/unit/test_client.py
index ef6ba70..81f2341 100644
--- a/test/unit/test_client.py
+++ b/test/unit/test_client.py
@@ -159,10 +159,40 @@ def test_factory_returns_the_correct_object(mock_client, test_class, factory_met
     assert obj.object_id == fake_id
 
 
-def test_users_return_the_correct_user_objects(mock_client, mock_box_session, users_response, user_id_1, user_id_2):
+@pytest.fixture(scope='module', params=(None, 'user1'))
+def users_filter_term(request):
+    return request.param
+
+
+@pytest.fixture(scope='module', params=(0, 10))
+def users_offset(request):
+    return request.param
+
+
+@pytest.fixture(scope='module', params=(0, 10))
+def users_limit(request):
+    return request.param
+
+
+def test_users_return_the_correct_user_objects(
+        mock_client,
+        mock_box_session,
+        users_response,
+        user_id_1,
+        user_id_2,
+        users_filter_term,
+        users_offset,
+        users_limit,
+):
     # pylint:disable=redefined-outer-name
     mock_box_session.get.return_value = users_response
-    users = mock_client.users()
+    users = mock_client.users(users_limit, users_offset, users_filter_term)
+    expected_params = {'offset': users_offset}
+    if users_limit is not None:
+        expected_params['limit'] = users_limit
+    if users_filter_term is not None:
+        expected_params['filter_term'] = users_filter_term
+    mock_box_session.get.assert_called_once_with('{0}/users'.format(API.BASE_API_URL), params=expected_params)
     assert users[0].object_id == user_id_1
     assert users[1].object_id == user_id_2