Repository: auth0/auth0-python
Pull request #106 (2018-06-06 15:42:44)

Add pagination to Clients and Connections
### What
This PR adds pagination support on MGMT API entities that were missing.

### Scope
- [x] Clients 
- [x] Connections
- [x] Users (Already available)
- [x] Logs (Already available)

### Discussion
- While pagination is going to be enforced on new tenants, existing tenants can still call the endpoint without a `page` value and in turn, retrieve the full list of items. However for this specific SDK, the entities that already had pagination implemented had a default `page` value of `0`, meaning if a specific page is not asked, the first one is always returned. On the present PR the new entities supporting pagination don't have the same behavior. Instead, they skip sending the `page` parameter if no value is specified. **Should we maintain the same pagination behavior as other entities or avoid a possible breaking change?**

### How it was tested
I re-used the existing tests and added a few similar. I noticed the tests were actually checking the arguments with which the method was being called. One of the checks was that the arguments matched against a given (expected) dictionary value. This dictionary included keys with `None` value (A Python type similar to JS's `undefined`). My concern was that these keys were being set as query parameters anyway, sending invalid values on the server request. I added logging support (for testing purposes, not committed to this PR) and tried locally to see what was the actual outcome. Below is an example showing how the parameters are not added to the call if they are of type `None`.


```py
# test/management/test_clients.py
        c = Clients(domain='domain', token='jwttoken')
        c.all(page=7, per_page=25)

        args, kwargs = mock_instance.get.call_args

        self.assertEqual('https://domain/api/v2/clients', args[0])
        self.assertEqual(kwargs['params'], {'fields': None,
                                            'include_fields': 'true',
                                            'page': 7,
                                            'per_page': 25})

# The final request URL (from my terminal)
        GET /api/v2/clients?include_fields=true&page=7&per_page=25
```




Patch:
diff --git a/.gitignore b/.gitignore
index 01d7e2b..4714cf4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,5 +51,8 @@ docs/_build/
 .idea/
 *.iml 
 
+# VSCode
+.vscode/
+
 # OS-specific files
 .DS_Store
diff --git a/auth0/v3/management/clients.py b/auth0/v3/management/clients.py
index 143b0b4..04c48ce 100644
--- a/auth0/v3/management/clients.py
+++ b/auth0/v3/management/clients.py
@@ -24,7 +24,7 @@ class Clients(object):
             return url + '/' + id
         return url
 
-    def all(self, fields=[], include_fields=True):
+    def all(self, fields=[], include_fields=True, page=None, per_page=None, extra_params={}):
         """Retrieves a list of all the applications.
 
         Important: The client_secret and encryption_key attributes can only be
@@ -37,11 +37,21 @@ class Clients(object):
 
            include_fields (bool, optional): True if the fields specified are
               to be include in the result, False otherwise.
-        """
 
-        params = {'fields': ','.join(fields) or None,
-                  'include_fields': str(include_fields).lower()}
+           page (int): The result's page number (zero based).
 
+           per_page (int, optional): The amount of entries per page.
+
+           extra_params (dictionary, optional): The extra parameters to add to
+             the request. The fields, include_fields, page and per_page values
+             specified as parameters take precedence over the ones defined here.
+        """
+        params = extra_params
+        params['fields'] = ','.join(fields) or None
+        params['include_fields'] = str(include_fields).lower()
+        params['page'] = page
+        params['per_page'] = per_page
+        
         return self.client.get(self._url(), params=params)
 
     def create(self, body):
diff --git a/auth0/v3/management/connections.py b/auth0/v3/management/connections.py
index e9749c3..0206a72 100644
--- a/auth0/v3/management/connections.py
+++ b/auth0/v3/management/connections.py
@@ -23,7 +23,7 @@ class Connections(object):
             return url + '/' + id
         return url
 
-    def all(self, strategy=None, fields=[], include_fields=True):
+    def all(self, strategy=None, fields=[], include_fields=True, page=None, per_page=None, extra_params={}):
         """Retrieves all connections.
 
         Args:
@@ -37,13 +37,24 @@ class Connections(object):
            include_fields (bool, optional): True if the fields specified are
               to be include in the result, False otherwise.
 
+           page (int): The result's page number (zero based).
+
+           per_page (int, optional): The amount of entries per page.
+
+           extra_params (dictionary, optional): The extra parameters to add to
+             the request. The fields, include_fields, page and per_page values
+             specified as parameters take precedence over the ones defined here.
+
         Returns:
            A list of connection objects.
         """
-
-        params = {'strategy': strategy or None,
-                  'fields': ','.join(fields) or None,
-                  'include_fields': str(include_fields).lower()}
+        
+        params = extra_params
+        params['strategy'] = strategy or None
+        params['fields'] = ','.join(fields) or None
+        params['include_fields'] = str(include_fields).lower()
+        params['page'] = page
+        params['per_page'] = per_page
 
         return self.client.get(self._url(), params=params)
 


Test patch:
diff --git a/auth0/v3/test/management/test_clients.py b/auth0/v3/test/management/test_clients.py
index 09f00a5..32ca57e 100644
--- a/auth0/v3/test/management/test_clients.py
+++ b/auth0/v3/test/management/test_clients.py
@@ -10,21 +10,52 @@ class TestClients(unittest.TestCase):
         mock_instance = mock_rc.return_value
 
         c = Clients(domain='domain', token='jwttoken')
+        
+        # Default parameters are requested
         c.all()
 
         args, kwargs = mock_instance.get.call_args
 
         self.assertEqual('https://domain/api/v2/clients', args[0])
         self.assertEqual(kwargs['params'], {'fields': None,
-                                            'include_fields': 'true'})
+                                            'include_fields': 'true',
+                                            'page': None,
+                                            'per_page': None})
 
+        # Fields filter
         c.all(fields=['a', 'b'], include_fields=False)
 
         args, kwargs = mock_instance.get.call_args
 
         self.assertEqual('https://domain/api/v2/clients', args[0])
         self.assertEqual(kwargs['params'], {'fields': 'a,b',
-                                            'include_fields': 'false'})
+                                            'include_fields': 'false',
+                                            'page': None,
+                                            'per_page': None})
+
+        # Specific pagination
+        c.all(page=7, per_page=25)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/clients', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'include_fields': 'true',
+                                            'page': 7,
+                                            'per_page': 25})
+
+        # Extra parameters                                    
+        c.all(extra_params={'some_key':'some_value'})
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/clients', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'include_fields': 'true',
+                                            'page': None,
+                                            'per_page': None,
+                                            'some_key': 'some_value'})
+
 
     @mock.patch('auth0.v3.management.clients.RestClient')
     def test_create(self, mock_rc):
diff --git a/auth0/v3/test/management/test_connections.py b/auth0/v3/test/management/test_connections.py
index 8304c85..2b9a829 100644
--- a/auth0/v3/test/management/test_connections.py
+++ b/auth0/v3/test/management/test_connections.py
@@ -10,6 +10,7 @@ class TestConnection(unittest.TestCase):
         mock_instance = mock_rc.return_value
         mock_instance.get.return_value = {}
 
+        # Default parameters are requested
         c = Connections(domain='domain', token='jwttoken')
         c.all()
 
@@ -18,8 +19,11 @@ class TestConnection(unittest.TestCase):
         self.assertEqual('https://domain/api/v2/connections', args[0])
         self.assertEqual(kwargs['params'], {'fields': None,
                                             'strategy': None,
+                                            'page': None,
+                                            'per_page': None,
                                             'include_fields': 'true'})
 
+        # Fields filter
         c.all(fields=['a', 'b'], include_fields=False)
 
         args, kwargs = mock_instance.get.call_args
@@ -27,17 +31,49 @@ class TestConnection(unittest.TestCase):
         self.assertEqual('https://domain/api/v2/connections', args[0])
         self.assertEqual(kwargs['params'], {'fields': 'a,b',
                                             'strategy': None,
+                                            'page': None,
+                                            'per_page': None,
                                             'include_fields': 'false'})
 
-        c.all(fields=['a', 'b'], strategy='strategy', include_fields=True)
+        # Fields + strategy filter 
+        c.all(fields=['a', 'b'], strategy='auth0', include_fields=True)
 
         args, kwargs = mock_instance.get.call_args
 
         self.assertEqual('https://domain/api/v2/connections', args[0])
         self.assertEqual(kwargs['params'], {'fields': 'a,b',
-                                            'strategy': 'strategy',
+                                            'strategy': 'auth0',
+                                            'page': None,
+                                            'per_page': None,
                                             'include_fields': 'true'})
 
+        # Specific pagination
+        c.all(page=7, per_page=25)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/connections', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'strategy': None,
+                                            'page': 7,
+                                            'per_page': 25,
+                                            'include_fields': 'true'})
+
+        # Extra parameters                                    
+        c.all(extra_params={'some_key':'some_value'})
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/connections', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'strategy': None,
+                                            'page': None,
+                                            'per_page': None,
+                                            'include_fields': 'true',
+                                            'some_key': 'some_value'})
+
+
+
     @mock.patch('auth0.v3.management.connections.RestClient')
     def test_get(self, mock_rc):
         mock_instance = mock_rc.return_value