Repository: auth0/auth0-python
Pull request #141 (2018-10-10 18:54:09)

String Formatting Updated
BTW I have formatted string in the entire project.

vkmrishad: hi @joshcanhelp,

I can list some benefits of formatting:

1. Readability. The format string syntax is more readable, as it separates style from the data. Also, in Python, %s syntax will automatically coerce any non str types to str; while concatenation only works with str, and you can't concatenate str with int.
2. Performance. In Python str is immutable, so the left and right string have to be copied into the new string for every pair of concatenation. If you concatenate four strings of length 10, you will be copying (10+10) + ((10+10)+10) + (((10+10)+10)+10) = 90 characters, instead of just 40 characters. And things get quadratically worse as the number and size of the string increases. Java optimizes this case some of the times by transforming the series of concatenation to use StringBuilder, but CPython doesn't.
3. For some use cases, the logging library provides an API that uses the format string to create the log entry string lazily (logging.info("blah: %s", 4)). This is great for improved performance if the logging library decided that the current log entry will be discarded by a log filter, so it doesn't need to format the string.

joshcanhelp: Great response @vkmrishad, thank you! We'll get this reviewed ASAP 👍 
vkmrishad: @lbalmaceda and @joshcanhelp
Please review and merge this PR.
joshcanhelp: @vkmrishad - We'll get to it when we can. This is not a bug or a critical fix so it's not high priority. Thanks again for your contribution and we'll take a look when we can. 
vkmrishad: @joshcanhelp 
Yes, you are right. But after a while this PR will be useless due to conflict. I think its better to close this PR and I can reopen this with new changes when you are ready.

Thanks.
joshcanhelp: @vkmrishad - Understood. I'd prefer to keep it open so we don't lose track of it but will leave that up to you. 
vkmrishad: @joshcanhelp 
Sure, I am keeping this open

Patch:
diff --git a/.snyk b/.snyk
new file mode 100644
index 0000000..3b39db8
--- /dev/null
+++ b/.snyk
@@ -0,0 +1,12 @@
+# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
+version: v1.12.0
+# ignores vulnerabilities until expiry date; change duration by modifying expiry date
+ignore:
+  SNYK-PYTHON-REQUESTS-72435:
+    - '*':
+        reason: 'unaffected, only affects https->http authorization header redirection.'
+        expires: 2019-11-05T00:00:00.000Z
+  SNYK-PYTHON-REQUESTS-40470:
+    - '*':
+        reason: 'patched in latest python versions: https://bugs.python.org/issue27568'
+patch: {}
diff --git a/README.rst b/README.rst
index a12b374..ad79594 100644
--- a/README.rst
+++ b/README.rst
@@ -46,7 +46,7 @@ For python3, use the following command
     
     pip3 install auth0-python
 
-Python 3.2 and 3.3 have reached `end-of-life<https://en.wikipedia.org/wiki/CPython#Version_history>`_ and support will be removed in the near future.
+Python 3.2 and 3.3 have reached `EOL <https://en.wikipedia.org/wiki/CPython#Version_history>`_ and support will be removed in the near future.
 
 ====================
 Management SDK Usage
diff --git a/auth0/v3/authentication/authorize_client.py b/auth0/v3/authentication/authorize_client.py
index eb57bd9..f2bb4cb 100644
--- a/auth0/v3/authentication/authorize_client.py
+++ b/auth0/v3/authentication/authorize_client.py
@@ -28,5 +28,5 @@ class AuthorizeClient(AuthenticationBase):
         }
 
         return self.get(
-            'https://%s/authorize' % self.domain,
+            'https://{}/authorize'.format(self.domain),
             params=params)
diff --git a/auth0/v3/authentication/database.py b/auth0/v3/authentication/database.py
index da017ab..48aad9d 100644
--- a/auth0/v3/authentication/database.py
+++ b/auth0/v3/authentication/database.py
@@ -25,7 +25,7 @@ class Database(AuthenticationBase):
         """
         warnings.warn("/oauth/ro will be deprecated in future releases", DeprecationWarning)
         return self.post(
-            'https://%s/oauth/ro' % self.domain,
+            'https://{}/oauth/ro'.format(self.domain),
             data={
                 'client_id': client_id,
                 'username': username,
@@ -44,7 +44,7 @@ class Database(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/dbconnections/signup' % self.domain,
+            'https://{}/dbconnections/signup'.format(self.domain),
             data={
                 'client_id': client_id,
                 'email': email,
@@ -59,7 +59,7 @@ class Database(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/dbconnections/change_password' % self.domain,
+            'https://{}/dbconnections/change_password'.format(self.domain),
             data={
                 'client_id': client_id,
                 'email': email,
diff --git a/auth0/v3/authentication/delegated.py b/auth0/v3/authentication/delegated.py
index f0eec7a..4a91f79 100644
--- a/auth0/v3/authentication/delegated.py
+++ b/auth0/v3/authentication/delegated.py
@@ -39,7 +39,7 @@ class Delegated(AuthenticationBase):
                              'have a value')
 
         return self.post(
-            'https://%s/delegation' % self.domain,
+            'https://{}/delegation'.format(self.domain),
             headers={'Content-Type': 'application/json'},
             data=data
         )
diff --git a/auth0/v3/authentication/enterprise.py b/auth0/v3/authentication/enterprise.py
index 2fd87ad..c318075 100644
--- a/auth0/v3/authentication/enterprise.py
+++ b/auth0/v3/authentication/enterprise.py
@@ -26,7 +26,7 @@ class Enterprise(AuthenticationBase):
         """Returns the WS-Federation Metadata.
         """
 
-        url = 'https://%s/wsfed/FederationMetadata' \
+        url = 'https://{}/wsfed/FederationMetadata' \
               '/2007-06/FederationMetadata.xml'
 
-        return self.get(url=url % self.domain)
+        return self.get(url=url.format(self.domain))
diff --git a/auth0/v3/authentication/get_token.py b/auth0/v3/authentication/get_token.py
index 9d2179a..4064e76 100644
--- a/auth0/v3/authentication/get_token.py
+++ b/auth0/v3/authentication/get_token.py
@@ -38,7 +38,7 @@ class GetToken(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/oauth/token' % self.domain,
+            'https://{}/oauth/token'.format(self.domain),
             data={
                 'client_id': client_id,
                 'client_secret': client_secret,
@@ -75,7 +75,7 @@ class GetToken(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/oauth/token' % self.domain,
+            'https://{}/oauth/token'.format(self.domain),
             data={
                 'client_id': client_id,
                 'code_verifier': code_verifier,
@@ -110,7 +110,7 @@ class GetToken(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/oauth/token' % self.domain,
+            'https://{}/oauth/token'.format(self.domain),
             data={
                 'client_id': client_id,
                 'client_secret': client_secret,
@@ -157,7 +157,7 @@ class GetToken(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/oauth/token' % self.domain,
+            'https://{}/oauth/token'.format(self.domain),
             data={
                 'client_id': client_id,
                 'username': username,
@@ -191,7 +191,7 @@ class GetToken(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/oauth/token' % self.domain,
+            'https://{}/oauth/token'.format(self.domain),
             data={
                 'client_id': client_id,
                 'client_secret': client_secret,
diff --git a/auth0/v3/authentication/passwordless.py b/auth0/v3/authentication/passwordless.py
index 91b4738..45434aa 100644
--- a/auth0/v3/authentication/passwordless.py
+++ b/auth0/v3/authentication/passwordless.py
@@ -37,7 +37,7 @@ class Passwordless(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/passwordless/start' % self.domain,
+            'https://{}/passwordless/start'.format(self.domain),
             data={
                 'client_id': client_id,
                 'connection': 'email',
@@ -53,7 +53,7 @@ class Passwordless(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/passwordless/start' % self.domain,
+            'https://{}/passwordless/start'.format(self.domain),
             data={
                 'client_id': client_id,
                 'connection': 'sms',
@@ -67,7 +67,7 @@ class Passwordless(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/oauth/ro' % self.domain,
+            'https://{}/oauth/ro'.format(self.domain),
             data={
                 'client_id': client_id,
                 'connection': 'sms',
diff --git a/auth0/v3/authentication/social.py b/auth0/v3/authentication/social.py
index 4f61289..a4c044b 100644
--- a/auth0/v3/authentication/social.py
+++ b/auth0/v3/authentication/social.py
@@ -32,7 +32,7 @@ class Social(AuthenticationBase):
         """
 
         return self.post(
-            'https://%s/oauth/access_token' % self.domain,
+            'https://{}/oauth/access_token'.format(self.domain),
             data={
                 'client_id': client_id,
                 'access_token': access_token,
diff --git a/auth0/v3/authentication/users.py b/auth0/v3/authentication/users.py
index 1df6b24..3594779 100644
--- a/auth0/v3/authentication/users.py
+++ b/auth0/v3/authentication/users.py
@@ -26,8 +26,8 @@ class Users(AuthenticationBase):
         """
 
         return self.get(
-            url='https://%s/userinfo' % self.domain,
-            headers={'Authorization': 'Bearer %s' % access_token}
+            url='https://{}/userinfo'.format(self.domain),
+            headers={'Authorization': 'Bearer {}'.format(access_token)}
         )
 
     def tokeninfo(self, jwt):
@@ -46,7 +46,7 @@ class Users(AuthenticationBase):
         """
         warnings.warn("/tokeninfo will be deprecated in future releases", DeprecationWarning)
         return self.post(
-            url='https://%s/tokeninfo' % self.domain,
+            url='https://{}/tokeninfo'.format(self.domain),
             data={'id_token': jwt},
             headers={'Content-Type': 'application/json'}
         )
diff --git a/auth0/v3/management/blacklists.py b/auth0/v3/management/blacklists.py
index b1aa4d3..534f190 100644
--- a/auth0/v3/management/blacklists.py
+++ b/auth0/v3/management/blacklists.py
@@ -2,7 +2,6 @@ from .rest import RestClient
 
 
 class Blacklists(object):
-
     """Auth0 blacklists endpoints
 
     Args:
@@ -15,7 +14,7 @@ class Blacklists(object):
     """
 
     def __init__(self, domain, token, telemetry=True):
-        self.url = 'https://%s/api/v2/blacklists/tokens' % domain
+        self.url = 'https://{}/api/v2/blacklists/tokens'.format(domain)
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def get(self, aud=None):
diff --git a/auth0/v3/management/client_grants.py b/auth0/v3/management/client_grants.py
index 6b37015..4322256 100644
--- a/auth0/v3/management/client_grants.py
+++ b/auth0/v3/management/client_grants.py
@@ -19,12 +19,12 @@ class ClientGrants(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/client-grants' % self.domain
+        url = 'https://{}/api/v2/client-grants'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
-    def all(self, audience=None, page=None, per_page=None, include_totals=False):
+    def all(self, audience=None, page=None, per_page=None, include_totals=False, client_id=None):
         """Retrieves all client grants.
 
         Args:
@@ -37,7 +37,9 @@ class ClientGrants(object):
 
             include_totals (bool, optional): True if the query summary is
                 to be included in the result, False otherwise.
-            
+
+            client_id (string, optional): The id of a client to filter
+
         See: https://auth0.com/docs/api/management/v2#!/Client_Grants/get_client_grants
         """
 
@@ -45,7 +47,8 @@ class ClientGrants(object):
             'audience': audience,
             'page': page,
             'per_page': per_page,
-            'include_totals': str(include_totals).lower()
+            'include_totals': str(include_totals).lower(),
+            'client_id': client_id,
         }
 
         return self.client.get(self._url(), params=params)
diff --git a/auth0/v3/management/clients.py b/auth0/v3/management/clients.py
index 93ae869..60db7a9 100644
--- a/auth0/v3/management/clients.py
+++ b/auth0/v3/management/clients.py
@@ -19,9 +19,9 @@ class Clients(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/clients' % self.domain
+        url = 'https://{}/api/v2/clients'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def all(self, fields=None, include_fields=True, page=None, per_page=None, extra_params=None):
diff --git a/auth0/v3/management/connections.py b/auth0/v3/management/connections.py
index 483fe0e..2e2db1c 100644
--- a/auth0/v3/management/connections.py
+++ b/auth0/v3/management/connections.py
@@ -18,9 +18,9 @@ class Connections(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/connections' % self.domain
+        url = 'https://{}/api/v2/connections'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def all(self, strategy=None, fields=None, include_fields=True, page=None, per_page=None, extra_params=None):
@@ -139,4 +139,3 @@ class Connections(object):
             An empty dict.
         """
         return self.client.delete(self._url(id) + '/users', params={'email': email})
-
diff --git a/auth0/v3/management/device_credentials.py b/auth0/v3/management/device_credentials.py
index d85a672..516a70a 100644
--- a/auth0/v3/management/device_credentials.py
+++ b/auth0/v3/management/device_credentials.py
@@ -19,9 +19,9 @@ class DeviceCredentials(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/device-credentials' % self.domain
+        url = 'https://{}/api/v2/device-credentials'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def get(self, user_id, client_id, type, fields=None, include_fields=True):
diff --git a/auth0/v3/management/email_templates.py b/auth0/v3/management/email_templates.py
index 49e7fc8..365b4d9 100644
--- a/auth0/v3/management/email_templates.py
+++ b/auth0/v3/management/email_templates.py
@@ -19,9 +19,9 @@ class EmailTemplates(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/email-templates' % self.domain
+        url = 'https://{}/api/v2/email-templates'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def create(self, body):
diff --git a/auth0/v3/management/emails.py b/auth0/v3/management/emails.py
index 0b1aae5..e709846 100644
--- a/auth0/v3/management/emails.py
+++ b/auth0/v3/management/emails.py
@@ -19,9 +19,9 @@ class Emails(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/emails/provider' % self.domain
+        url = 'https://{}/api/v2/emails/provider'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def get(self, fields=None, include_fields=True):
diff --git a/auth0/v3/management/guardian.py b/auth0/v3/management/guardian.py
index ab94d5b..e421b4b 100644
--- a/auth0/v3/management/guardian.py
+++ b/auth0/v3/management/guardian.py
@@ -19,9 +19,9 @@ class Guardian(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/guardian' % self.domain
+        url = 'https://{}/api/v2/guardian'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def all_factors(self):
@@ -42,7 +42,7 @@ class Guardian(object):
             body (dict): Attributes to modify.
             See: https://auth0.com/docs/api/management/v2#!/Guardian/put_factors_by_name
         """
-        url = self._url('factors/%s' % (name))
+        url = self._url('factors/{}'.format(name))
         return self.client.put(url, data=body)
 
     def update_templates(self, body):
@@ -79,7 +79,7 @@ class Guardian(object):
 
         See: https://auth0.com/docs/api/management/v2#!/Guardian/get_enrollments_by_id
         """
-        url = self._url('enrollments/%s' % (id))
+        url = self._url('enrollments/{}'.format(id))
         return self.client.get(url)
 
     def delete_enrollment(self, id):
@@ -93,7 +93,7 @@ class Guardian(object):
 
         See: https://auth0.com/docs/api/management/v2#!/Guardian/delete_enrollments_by_id
         """
-        url = self._url('enrollments/%s' % (id))
+        url = self._url('enrollments/{}'.format(id))
         return self.client.delete(url)
 
     def create_enrollment_ticket(self, body):
diff --git a/auth0/v3/management/jobs.py b/auth0/v3/management/jobs.py
index c88cf99..c06f3db 100644
--- a/auth0/v3/management/jobs.py
+++ b/auth0/v3/management/jobs.py
@@ -19,9 +19,9 @@ class Jobs(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, path=None):
-        url = 'https://%s/api/v2/jobs' % self.domain
+        url = 'https://{}/api/v2/jobs'.format(self.domain)
         if path is not None:
-            return url + '/' + path
+            return '{}/{}'.format(url, path)
         return url
 
     def get(self, id):
@@ -42,7 +42,7 @@ class Jobs(object):
 
         See: https://auth0.com/docs/api/management/v2#!/Jobs/get_errors
         """
-        url = self._url('%s/errors' % (id))
+        url = self._url('{}/errors'.format(id))
         return self.client.get(url)
 
     def export_users(self, body):
diff --git a/auth0/v3/management/logs.py b/auth0/v3/management/logs.py
index 46cf572..d7b7028 100644
--- a/auth0/v3/management/logs.py
+++ b/auth0/v3/management/logs.py
@@ -19,9 +19,9 @@ class Logs(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/logs' % self.domain
+        url = 'https://{}/api/v2/logs'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def search(self, page=0, per_page=50, sort=None, q=None,
diff --git a/auth0/v3/management/resource_servers.py b/auth0/v3/management/resource_servers.py
index 33f8705..90a6be7 100644
--- a/auth0/v3/management/resource_servers.py
+++ b/auth0/v3/management/resource_servers.py
@@ -19,9 +19,9 @@ class ResourceServers(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/resource-servers' % self.domain
+        url = 'https://{}/api/v2/resource-servers'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def create(self, body):
diff --git a/auth0/v3/management/rest.py b/auth0/v3/management/rest.py
index 9f87614..cca81c3 100644
--- a/auth0/v3/management/rest.py
+++ b/auth0/v3/management/rest.py
@@ -37,7 +37,7 @@ class RestClient(object):
             }).encode('utf-8')
 
             self.base_headers = {
-                'User-Agent': 'Python/%s' % py_version,
+                'User-Agent': 'Python/{}'.format(py_version),
                 'Auth0-Client': base64.b64encode(auth0_client),
                 'Content-Type': 'application/json'
             }
@@ -47,7 +47,7 @@ class RestClient(object):
     def get(self, url, params=None):
         headers = self.base_headers.copy()
         headers.update({
-            'Authorization': 'Bearer %s' % self.jwt,
+            'Authorization': 'Bearer {}'.format(self.jwt),
         })
 
         response = requests.get(url, params=params, headers=headers)
@@ -56,7 +56,7 @@ class RestClient(object):
     def post(self, url, data=None):
         headers = self.base_headers.copy()
         headers.update({
-            'Authorization': 'Bearer %s' % self.jwt,
+            'Authorization': 'Bearer {}'.format(self.jwt),
             'Content-Type': 'application/json'
         })
 
@@ -67,7 +67,7 @@ class RestClient(object):
         headers = self.base_headers.copy()
         headers.pop('Content-Type', None)
         headers.update({
-            'Authorization': 'Bearer %s' % self.jwt,
+            'Authorization': 'Bearer {}'.format(self.jwt),
         })
 
         response = requests.post(url, data=data, files=files, headers=headers)
@@ -76,7 +76,7 @@ class RestClient(object):
     def patch(self, url, data=None):
         headers = self.base_headers.copy()
         headers.update({
-            'Authorization': 'Bearer %s' % self.jwt,
+            'Authorization': 'Bearer {}'.format(self.jwt),
             'Content-Type': 'application/json'
         })
 
@@ -86,7 +86,7 @@ class RestClient(object):
     def put(self, url, data=None):
         headers = self.base_headers.copy()
         headers.update({
-            'Authorization': 'Bearer %s' % self.jwt,
+            'Authorization': 'Bearer {}'.format(self.jwt),
             'Content-Type': 'application/json'
         })
 
@@ -96,7 +96,7 @@ class RestClient(object):
     def delete(self, url, params=None):
         headers = self.base_headers.copy()
         headers.update({
-            'Authorization': 'Bearer %s' % self.jwt,
+            'Authorization': 'Bearer {}'.format(self.jwt),
         })
 
         response = requests.delete(url, headers=headers, params=params or {})
diff --git a/auth0/v3/management/rules.py b/auth0/v3/management/rules.py
index 1abb208..5838a65 100644
--- a/auth0/v3/management/rules.py
+++ b/auth0/v3/management/rules.py
@@ -19,9 +19,9 @@ class Rules(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/rules' % self.domain
+        url = 'https://{}/api/v2/rules'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def all(self, stage='login_success', enabled=True, fields=None,
diff --git a/auth0/v3/management/tenants.py b/auth0/v3/management/tenants.py
index 210165d..b1faef7 100644
--- a/auth0/v3/management/tenants.py
+++ b/auth0/v3/management/tenants.py
@@ -19,7 +19,7 @@ class Tenants(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self):
-        return 'https://%s/api/v2/tenants/settings' % self.domain
+        return 'https://{}/api/v2/tenants/settings'.format(self.domain)
 
     def get(self, fields=None, include_fields=True):
         """Get tenant settings.
diff --git a/auth0/v3/management/user_blocks.py b/auth0/v3/management/user_blocks.py
index a6b0adb..82af8e5 100644
--- a/auth0/v3/management/user_blocks.py
+++ b/auth0/v3/management/user_blocks.py
@@ -19,9 +19,9 @@ class UserBlocks(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/user-blocks' % self.domain
+        url = 'https://{}/api/v2/user-blocks'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def get_by_identifier(self, identifier):
diff --git a/auth0/v3/management/users.py b/auth0/v3/management/users.py
index 46854b5..6640e5e 100644
--- a/auth0/v3/management/users.py
+++ b/auth0/v3/management/users.py
@@ -19,9 +19,9 @@ class Users(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self, id=None):
-        url = 'https://%s/api/v2/users' % self.domain
+        url = 'https://{}/api/v2/users'.format(self.domain)
         if id is not None:
-            return url + '/' + id
+            return '{}/{}'.format(url, id)
         return url
 
     def list(self, page=0, per_page=25, sort=None, connection=None, q=None,
@@ -166,7 +166,7 @@ class Users(object):
 
             body (dict): Please see: https://auth0.com/docs/api/v2#!/Users/post_identities
         """
-        url = self._url('%s/identities' % user_id)
+        url = self._url('{}/identities'.format(user_id))
         return self.client.post(url, data=body)
 
     def regenerate_recovery_code(self, user_id):
@@ -177,7 +177,7 @@ class Users(object):
 
         See: https://auth0.com/docs/api/management/v2#!/Users/post_recovery_code_regeneration
         """
-        url = self._url('%s/recovery-code-regeneration' % user_id)
+        url = self._url('{}/recovery-code-regeneration'.format(user_id))
         return self.client.post(url)
 
     def get_guardian_enrollments(self, user_id):
@@ -188,7 +188,7 @@ class Users(object):
 
         See: https://auth0.com/docs/api/management/v2#!/Users/get_enrollments
         """
-        url = self._url('%s/enrollments' % user_id)
+        url = self._url('{}/enrollments'.format(user_id))
         return self.client.get(url)
 
     def get_log_events(self, user_id, page=0, per_page=50, sort=None,
@@ -220,5 +220,5 @@ class Users(object):
             'sort': sort
         }
 
-        url = self._url('%s/logs' % user_id)
+        url = self._url('{}/logs'.format(user_id))
         return self.client.get(url, params=params)
diff --git a/auth0/v3/management/users_by_email.py b/auth0/v3/management/users_by_email.py
index 0c92e6a..c9f582e 100644
--- a/auth0/v3/management/users_by_email.py
+++ b/auth0/v3/management/users_by_email.py
@@ -19,7 +19,7 @@ class UsersByEmail(object):
         self.client = RestClient(jwt=token, telemetry=telemetry)
 
     def _url(self):
-        url = 'https://%s/api/v2/users-by-email' % self.domain
+        url = 'https://{}/api/v2/users-by-email'.format(self.domain)
         return url
 
     def search_users_by_email(self, email, fields=None, include_fields=True):
@@ -39,7 +39,7 @@ class UsersByEmail(object):
         See: https://auth0.com/docs/api/management/v2#!/Users_By_Email/get_users_by_email
         """
         params = {
-            'email': email.lower(),
+            'email': email,
             'fields': fields and ','.join(fields) or None,
             'include_fields': str(include_fields).lower()
         }
diff --git a/docs/conf.py b/docs/conf.py
index c1de7cd..98802d8 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -19,14 +19,14 @@ import shlex
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('.'))
+# sys.path.insert(0, os.path.abspath('.'))
 
 sys.path.insert(0, os.path.abspath('../'))
 
 # -- General configuration ------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+# needs_sphinx = '1.0'
 
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@@ -47,7 +47,7 @@ templates_path = ['_templates']
 source_suffix = '.rst'
 
 # The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
 
 # The master toctree document.
 master_doc = 'index'
@@ -75,9 +75,9 @@ language = None
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ''
 # Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
@@ -85,32 +85,31 @@ exclude_patterns = ['_build']
 
 # The reST default role (used for this markup: `text`) to use for all
 # documents.
-#default_role = None
+# default_role = None
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = 'sphinx'
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 # If true, keep warnings as "system message" paragraphs in the built documents.
-#keep_warnings = False
+# keep_warnings = False
 
 # If true, `todo` and `todoList` produce output, else they produce nothing.
 todo_include_todos = False
 
-
 # -- Options for HTML output ----------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
@@ -120,21 +119,21 @@ html_theme = 'classic'
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+# html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-#html_title = None
+# html_title = None
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
@@ -149,62 +148,62 @@ html_static_path = ['_static']
 # Add any extra paths that contain custom files (such as robots.txt or
 # .htaccess) here, relative to this directory. These files are copied
 # directly to the root of the documentation.
-#html_extra_path = []
+# html_extra_path = []
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
 
 # If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
 
 # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
 
 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
 
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
 
 # This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
 
 # Language to be used for generating the HTML full-text search index.
 # Sphinx supports the following languages:
 #   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
 #   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
-#html_search_language = 'en'
+# html_search_language = 'en'
 
 # A dictionary with options for the search language support, empty by default.
 # Now only 'ja' uses this config value
-#html_search_options = {'type': 'default'}
+# html_search_options = {'type': 'default'}
 
 # The name of a javascript file (relative to the configuration directory) that
 # implements a search results scorer. If empty, the default will be used.
-#html_search_scorer = 'scorer.js'
+# html_search_scorer = 'scorer.js'
 
 # Output file base name for HTML help builder.
 htmlhelp_basename = 'Auth0-Pythondoc'
@@ -212,46 +211,46 @@ htmlhelp_basename = 'Auth0-Pythondoc'
 # -- Options for LaTeX output ---------------------------------------------
 
 latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-#'papersize': 'letterpaper',
+    # The paper size ('letterpaper' or 'a4paper').
+    # 'papersize': 'letterpaper',
 
-# The font size ('10pt', '11pt' or '12pt').
-#'pointsize': '10pt',
+    # The font size ('10pt', '11pt' or '12pt').
+    # 'pointsize': '10pt',
 
-# Additional stuff for the LaTeX preamble.
-#'preamble': '',
+    # Additional stuff for the LaTeX preamble.
+    # 'preamble': '',
 
-# Latex figure (float) alignment
-#'figure_align': 'htbp',
+    # Latex figure (float) alignment
+    # 'figure_align': 'htbp',
 }
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
 latex_documents = [
-  (master_doc, 'Auth0-Python.tex', u'Auth0 - Python Documentation',
-   u'Auth0', 'manual'),
+    (master_doc, 'Auth0-Python.tex', u'Auth0 - Python Documentation',
+     u'Auth0', 'manual'),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
 
 # If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
 
 
 # -- Options for manual page output ---------------------------------------
@@ -264,7 +263,7 @@ man_pages = [
 ]
 
 # If true, show URL addresses after external links.
-#man_show_urls = False
+# man_show_urls = False
 
 
 # -- Options for Texinfo output -------------------------------------------
@@ -273,19 +272,19 @@ man_pages = [
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  (master_doc, 'Auth0-Python', u'Auth0 - Python Documentation',
-   author, 'Auth0-Python', 'One line description of project.',
-   'Miscellaneous'),
+    (master_doc, 'Auth0-Python', u'Auth0 - Python Documentation',
+     author, 'Auth0-Python', 'One line description of project.',
+     'Miscellaneous'),
 ]
 
 # Documents to append as an appendix to all manuals.
-#texinfo_appendices = []
+# texinfo_appendices = []
 
 # If false, no module index is generated.
-#texinfo_domain_indices = True
+# texinfo_domain_indices = True
 
 # How to display URL addresses: 'footnote', 'no', or 'inline'.
-#texinfo_show_urls = 'footnote'
+# texinfo_show_urls = 'footnote'
 
 # If true, do not generate a @detailmenu in the "Top" node's menu.
-#texinfo_no_detailmenu = False
+# texinfo_no_detailmenu = False


Test patch:
diff --git a/auth0/v3/test/management/test_client_grants.py b/auth0/v3/test/management/test_client_grants.py
index 3d6b292..1155649 100644
--- a/auth0/v3/test/management/test_client_grants.py
+++ b/auth0/v3/test/management/test_client_grants.py
@@ -21,7 +21,8 @@ class TestClientGrants(unittest.TestCase):
             'audience': None,
             'page': None,
             'per_page': None,
-            'include_totals': 'false'
+            'include_totals': 'false',
+            'client_id': None,
         })
 
         # With audience
@@ -34,7 +35,8 @@ class TestClientGrants(unittest.TestCase):
             'audience': 'http://domain.auth0.com/api/v2/',
             'page': None,
             'per_page': None,
-            'include_totals': 'false'
+            'include_totals': 'false',
+            'client_id': None,
         })
 
         # With pagination params
@@ -47,7 +49,22 @@ class TestClientGrants(unittest.TestCase):
             'audience': None,
             'page': 7,
             'per_page': 23,
-            'include_totals': 'true'
+            'include_totals': 'true',
+            'client_id': None,
+        })
+
+        # With client_id param
+        c.all(client_id='exampleid')
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/client-grants', args[0])
+        self.assertEqual(kwargs['params'], {
+            'audience': None,
+            'page': None,
+            'per_page': None,
+            'include_totals': 'false',
+            'client_id': 'exampleid',
         })
 
     @mock.patch('auth0.v3.management.client_grants.RestClient')
diff --git a/auth0/v3/test/management/test_users_by_email.py b/auth0/v3/test/management/test_users_by_email.py
index 012c597..2c6e9af 100644
--- a/auth0/v3/test/management/test_users_by_email.py
+++ b/auth0/v3/test/management/test_users_by_email.py
@@ -16,7 +16,7 @@ class TestUsersByEmail(unittest.TestCase):
 
         self.assertEqual('https://domain/api/v2/users-by-email', args[0])
         self.assertEqual(kwargs['params'], {
-            'email': 'a@b.com',
+            'email': 'A@B.com',
             'fields': None,
             'include_fields': 'true'
         })