Repository: auth0/auth0-python
Pull request #12 (2015-11-12 11:49:05)

V2 Api
All endpoints are implemented and have unit tests.


Patch:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..66ca411
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,47 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+.hypothesis/
+
+# Translations
+*.mo
+*.pot
+
+# Sphinx documentation
+docs/_build/
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..3ddb4e2
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,7 @@
+Changes
+=======
+
+2.0.0 (unreleased)
+------------------
+
+- ...
diff --git a/README.md b/README.md
deleted file mode 100644
index bcfecca..0000000
--- a/README.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Auth0 - Python
-In this repository, you'll find all the information about integrating Auth0 with Python.
-
-Check out the examples that we have in here in our examples folder. Each of them has a README on how to run them and on how to use them.
-
-## What is Auth0?
-
-Auth0 helps you to:
-
-* Add authentication with [multiple authentication sources](https://docs.auth0.com/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, amont others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**.
-* Add authentication through more traditional **[username/password databases](https://docs.auth0.com/mysql-connection-tutorial)**.
-* Add support for **[linking different user accounts](https://docs.auth0.com/link-accounts)** with the same user.
-* Support for generating signed [Json Web Tokens](https://docs.auth0.com/jwt) to call your APIs and **flow the user identity** securely.
-* Analytics of how, when and where users are logging in.
-* Pull data from other sources and add it to the user profile, through [JavaScript rules](https://docs.auth0.com/rules).
-
-## Create a free Auth0 Account
-
-1. Go to [Auth0](https://auth0.com) and click Sign Up.
-2. Use Google, GitHub or Microsoft Account to login.
-
-## Issue Reporting
-
-If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues.
-
-## Author
-
-[Auth0](auth0.com)
-
-## License
-
-This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..83eea20
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,84 @@
+**************
+Auth0 - Python
+**************
+
+|pypi| |unix_build| |windows_build| |coverage|
+
+In this repository, you'll find all the information about integrating Auth0 with Python.
+
+Check out the examples that we have in here in our examples folder. Each of them has a README on how to run them and on how to use them.
+
+
+==============
+What is Auth0?
+==============
+
+Auth0 helps you to:
+
+* Add authentication with `multiple authentication sources <https://docs.auth0.com/identityproviders>`_,
+  either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, amont others**,
+  or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**.
+* Add authentication through more traditional **[username/password databases](https://docs.auth0.com/mysql-connection-tutorial)**.
+* Add support for **`linking different user accounts <https://docs.auth0.com/link-accounts>`_** with the same user.
+* Support for generating signed `Json Web Tokens <https://docs.auth0.com/jwt>`_ to call your APIs and **flow the user identity** securely.
+* Analytics of how, when and where users are logging in.
+* Pull data from other sources and add it to the user profile, through `JavaScript rules <https://docs.auth0.com/rules>`_.
+
+
+===========================
+Create a free Auth0 Account
+===========================
+
+1. Go to `Auth0`_ and click Sign Up.
+2. Use Google, GitHub or Microsoft Account to login.
+
+
+===============
+Issue Reporting
+===============
+
+If you have found a bug or if you have a feature request, please report them at this repository issues section.
+Please do not report security vulnerabilities on the public GitHub issue tracker.
+The `Responsible Disclosure Program <https://auth0.com/whitehat>`_ details the procedure for disclosing security issues.
+
+
+======
+Author
+======
+
+`Auth0`_
+
+==========
+Contribute
+==========
+
+Please see `CONTRIBUTING.rst <https://github.com/sophilabs/auth0-python/blob/v2/CONTRIBUTING.rst>`_.
+
+
+==========
+Change Log
+==========
+
+Please see `CHANGELOG.rst <https://github.com/sophilabs/auth0-python/blob/v2/CHANGELOG.rst>`_.
+
+
+=======
+License
+=======
+
+This project is licensed under the MIT license. See the `LICENSE.rst <https://github.com/sophilabs/auth0-python/blob/v2/LICENSE.rst>`_
+file for more info.
+
+.. _Auth0: https://auth0.com
+
+.. |pypi| image:: https://img.shields.io/pypi/v/auth0.svg?style=flat-square&label=latest%20version
+    :target: https://pypi.python.org/pypi/auth0
+    :alt: Latest version released on PyPi
+
+.. |coverage| image:: https://img.shields.io/coveralls/sophilabs/auth0-python/v2.svg?style=flat-square
+    :target: https://coveralls.io/r/sophilabs/auth0-python?branch=v2
+    :alt: Test coverage
+
+.. |build| image:: https://img.shields.io/travis/sophilabs/auth0-python/v2.svg?style=flat-square&label=build
+    :target: http://travis-ci.org/sophilabs/auth0-python
+    :alt: Build status of the v2 branch
diff --git a/auth0/__init__.py b/auth0/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/auth0/v2/__init__.py b/auth0/v2/__init__.py
new file mode 100644
index 0000000..dba6fb3
--- /dev/null
+++ b/auth0/v2/__init__.py
@@ -0,0 +1,12 @@
+from .auth0 import Auth0
+from .exceptions import Auth0Error
+from .connection import Connection
+from .client import Client
+from .device_credentials import DeviceCredentials
+from .blacklists import Blacklists
+from .emails import Emails
+from .jobs import Jobs
+from .rules import Rules
+from .stats import Stats
+from .tickets import Tickets
+from .users import Users
diff --git a/auth0/v2/auth0.py b/auth0/v2/auth0.py
new file mode 100644
index 0000000..83eff7e
--- /dev/null
+++ b/auth0/v2/auth0.py
@@ -0,0 +1,26 @@
+from .connection import Connection
+from .client import Client
+from .device_credentials import DeviceCredentials
+from .blacklists import Blacklists
+from .emails import Emails
+from .jobs import Jobs
+from .rules import Rules
+from .stats import Stats
+from .tickets import Tickets
+from .users import Users
+
+
+class Auth0(object):
+    """Docstring for Auth0. """
+
+    def __init__(self, domain, jwt_token):
+        self.client = Client(domain, jwt_token)
+        self.connection = Connection(domain, jwt_token)
+        self.device_credentials = DeviceCredentials(domain, jwt_token)
+        self.blacklists = Blacklists(domain, jwt_token)
+        self.emails = Emails(domain, jwt_token)
+        self.jobs = Jobs(domain, jwt_token)
+        self.rules = Rules(domain, jwt_token)
+        self.stats = Stats(domain, jwt_token)
+        self.tickets = Tickets(domain, jwt_token)
+        self.users = Users(domain, jwt_token)
diff --git a/auth0/v2/blacklists.py b/auth0/v2/blacklists.py
new file mode 100644
index 0000000..f64e701
--- /dev/null
+++ b/auth0/v2/blacklists.py
@@ -0,0 +1,43 @@
+from .rest import RestClient
+
+
+class Blacklists(object):
+
+    """Auth0 blacklists endpoints
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+
+        jwt_token (str): An API token created with your account's global
+            keys. You can create one by using the token generator in the
+            API Explorer: https://auth0.com/docs/api/v2
+    """
+
+    def __init__(self, domain, jwt_token):
+        self.url = 'https://%s/api/v2/blacklists/tokens' % domain
+        self.client = RestClient(jwt=jwt_token)
+
+    def get(self, aud=None):
+        """Retrieves the jti and aud of all tokens in the blacklist.
+
+        Args:
+            aud (str, optional): The JWT's aud claim. The client_id of the
+                client for which it was issued.
+        """
+
+        params = {
+            'aud': aud
+        }
+
+        return self.client.get(self.url, params=params)
+
+    def create(self, jti, aud=''):
+        """Adds a token to the blacklist.
+
+        Args:
+            jti (str): the jti of the JWT to blacklist.
+            aud (str, optional): The JWT's aud claim. The client_id of the
+                client for which it was issued.
+        """
+
+        return self.client.post(self.url, data={'jti': jti, 'aud': aud})
diff --git a/auth0/v2/client.py b/auth0/v2/client.py
new file mode 100644
index 0000000..d223395
--- /dev/null
+++ b/auth0/v2/client.py
@@ -0,0 +1,99 @@
+from .rest import RestClient
+
+
+class Client(object):
+
+    """Auth0 client endpoints
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+
+        jwt_token (str): An API token created with your account's global
+            keys. You can create one by using the token generator in the
+            API Explorer: https://auth0.com/docs/api/v2
+    """
+
+    def __init__(self, domain, jwt_token):
+        self.domain = domain
+        self.client = RestClient(jwt=jwt_token)
+
+    def _url(self, id=None):
+        url = 'https://%s/api/v2/clients' % self.domain
+        if id is not None:
+            return url + '/' + id
+        return url
+
+    def all(self, fields=[], include_fields=True):
+        """Retrieves a list of all client applications.
+
+        Important: The client_secret and encryption_key attributes can only be
+        retrieved with the read:client_keys scope.
+
+        Args:
+           fields (list of str, optional): A list of fields to include or
+              exclude from the result (depending on include_fields). Empty to
+              retrieve all fields.
+
+           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()}
+
+        return self.client.get(self._url(), params=params)
+
+    def create(self, body):
+        """Create a new client application.
+
+        Args:
+           body (dict): Attributes for the new client application.
+              See: https://auth0.com/docs/api/v2#!/Clients/post_clients
+        """
+
+        return self.client.post(self._url(), data=body)
+
+    def get(self, id, fields=[], include_fields=True):
+        """Retrieves a client by its id.
+
+        Important: The client_secret, encryption_key and signing_keys
+        attributes can only be retrieved with the read:client_keys scope.
+
+        Args:
+           id (str): Id of the client to get.
+
+           fields (list of str, optional): A list of fields to include or
+              exclude from the result (depending on include_fields). Empty to
+              retrieve all fields.
+
+           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()}
+
+        return self.client.get(self._url(id), params=params)
+
+    def delete(self, id):
+        """Deletes a client and all its related assets.
+
+        Args:
+           id (str): Id of client to delete.
+        """
+
+        return self.client.delete(self._url(id))
+
+    def update(self, id, body):
+        """Modifies a client.
+
+        Important: The client_secret, encryption_key and signing_keys
+        attributes can only be updated with the update:client_keys scope.
+
+        Args:
+           id (str): Client id.
+
+           body (dict): Attributes to modify.
+        """
+
+        return self.client.patch(self._url(id), data=body)
diff --git a/auth0/v2/connection.py b/auth0/v2/connection.py
new file mode 100644
index 0000000..62662ce
--- /dev/null
+++ b/auth0/v2/connection.py
@@ -0,0 +1,105 @@
+from .rest import RestClient
+
+
+class Connection(object):
+    """Auth0 connection endpoints
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+        jwt_token (str): An API token created with your account's global
+            keys. You can create one by using the token generator in the
+            API Explorer: https://auth0.com/docs/api/v2
+    """
+
+    def __init__(self, domain, jwt_token):
+        self.domain = domain
+        self.client = RestClient(jwt=jwt_token)
+
+    def _url(self, id=None):
+        url = 'https://%s/api/v2/connections' % self.domain
+        if id is not None:
+            return url + '/' + id
+        return url
+
+    def all(self, strategy=None, fields=[], include_fields=True):
+        """Retrieves all connections.
+
+        Args:
+           strategy (str, optional): Only retrieve connections of
+              this strategy type. (e.g: strategy='amazon')
+
+           fields (list of str, optional): A list of fields to include or
+              exclude from the result (depending on include_fields). Empty to
+              retrieve all fields.
+
+           include_fields (bool, optional): True if the fields specified are
+              to be include in the result, False otherwise.
+
+        Returns:
+           A list of connection objects.
+        """
+
+        params = {'strategy': strategy or None,
+                  'fields': ','.join(fields) or None,
+                  'include_fields': str(include_fields).lower()}
+
+        return self.client.get(self._url(), params=params)
+
+    def get(self, id, fields=[], include_fields=True):
+        """Retrieve connection by id.
+
+        Args:
+           id (str): Id of the connection to get.
+
+           fields (list of str, optional): A list of fields to include or
+              exclude from the result (depending on include_fields). Empty to
+              retrieve all fields.
+
+           include_fields (bool, optional): True if the fields specified are
+              to be include in the result, False otherwise.
+
+        Returns:
+            A connection object.
+        """
+
+        params = {'fields': ','.join(fields) or None,
+                  'include_fields': str(include_fields).lower()}
+
+        return self.client.get(self._url(id), params=params)
+
+    def delete(self, id):
+        """Deletes a connection and all its users.
+
+        Args:
+           id: Id of the connection to delete.
+
+        Returns:
+           An empty dict.
+        """
+
+        return self.client.delete(self._url(id))
+
+    def update(self, id, body):
+        """Modifies a connection.
+
+        Args:
+           id: Id of the connection.
+
+           body (dict): Specifies which fields are to be modified, and to what
+              values.
+
+        Returns:
+           The modified connection object.
+        """
+
+        return self.client.patch(self._url(id), data=body)
+
+    def create(self, body):
+        """Creates a new connection.
+
+        Args:
+            body (dict): Attributes used to create the connection. Mandatory
+                attributes are: 'name' and 'strategy'.
+        """
+
+        return self.client.post(self._url(), data=body)
diff --git a/auth0/v2/device_credentials.py b/auth0/v2/device_credentials.py
new file mode 100644
index 0000000..ad1735a
--- /dev/null
+++ b/auth0/v2/device_credentials.py
@@ -0,0 +1,57 @@
+from .rest import RestClient
+
+
+class DeviceCredentials(object):
+
+    """Auth0 connection endpoints
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+
+        jwt_token (str): An API token created with your account's global
+            keys. You can create one by using the token generator in the
+            API Explorer: https://auth0.com/docs/api/v2
+    """
+
+    def __init__(self, domain, jwt_token):
+        self.domain = domain
+        self.client = RestClient(jwt=jwt_token)
+
+    def _url(self, id=None):
+        url = 'https://%s/api/v2/device-credentials' % self.domain
+        if id is not None:
+            return url + '/' + id
+        return url
+
+    def get(self, user_id, client_id, type, fields=[], include_fields=True):
+        """
+        Args:
+            user_id (str): The user_id of the devices to retrieve.
+
+            client_id (str): The client_id of the devices to retrieve.
+
+            type (str): The type of credentials (public_key, refresh_token).
+
+            fields (list, optional): A list of fields to include or exclude
+                (depending on include_fields) from the result, empty to
+                retrieve all fields
+
+            include_fields (bool, optional): True if the fields specified are
+                to be excluded from the result, false otherwise
+                (defaults to true)
+        """
+
+        params = {
+            'fields': ','.join(fields) or None,
+            'include_fields': str(include_fields).lower(),
+            'user_id': user_id,
+            'client_id': client_id,
+            'type': type,
+        }
+        return self.client.get(self._url(), params=params)
+
+    def create(self, body):
+        return self.client.post(self._url(), data=body)
+
+    def delete(self, id):
+        return self.client.delete(self._url(id))
diff --git a/auth0/v2/emails.py b/auth0/v2/emails.py
new file mode 100644
index 0000000..eeca083
--- /dev/null
+++ b/auth0/v2/emails.py
@@ -0,0 +1,29 @@
+from .rest import RestClient
+
+
+class Emails(object):
+    def __init__(self, domain, jwt_token):
+        self.domain = domain
+        self.client = RestClient(jwt=jwt_token)
+
+    def _url(self, id=None):
+        url = 'https://%s/api/v2/emails/provider' % self.domain
+        if id is not None:
+            return url + '/' + id
+        return url
+
+    def get(self, fields=[], include_fields=True):
+        params = {'fields': ','.join(fields) or None,
+                  'include_fields': str(include_fields).lower()}
+
+        return self.client.get(self._url(), params=params)
+
+    def config(self, body):
+        return self.client.post(self._url(), data=body)
+
+    def delete(self):
+        return self.client.delete(self._url())
+
+    def update(self, body):
+        return self.client.patch(self._url(), data=body)
+
diff --git a/auth0/v2/exceptions.py b/auth0/v2/exceptions.py
new file mode 100644
index 0000000..e817a31
--- /dev/null
+++ b/auth0/v2/exceptions.py
@@ -0,0 +1,8 @@
+class Auth0Error(Exception):
+    def __init__(self, status_code, error_code, message):
+        self.status_code = status_code
+        self.error_code = error_code
+        self.message = message
+
+    def __str__(self):
+        return '%s: %s' % (self.status_code, self.message)
diff --git a/auth0/v2/jobs.py b/auth0/v2/jobs.py
new file mode 100644
index 0000000..51357ab
--- /dev/null
+++ b/auth0/v2/jobs.py
@@ -0,0 +1,35 @@
+from .rest import RestClient
+
+
+class Jobs(object):
+
+    """Auth0 jobs endpoints
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+
+        jwt_token (str): An API token created with your account's global
+            keys. You can create one by using the token generator in the
+            API Explorer: https://auth0.com/docs/api/v2
+    """
+
+    def __init__(self, domain, jwt_token):
+        self.domain = domain
+        self.client = RestClient(jwt=jwt_token)
+
+    def _url(self, path=None):
+        url = 'https://%s/api/v2/jobs' % self.domain
+        if path is not None:
+            return url + '/' + path
+        return url
+
+    def get(self, id):
+        return self.client.get(self._url(id))
+
+    def import_users(self, connection_id, file_obj):
+        return self.client.file_post(self._url('users-imports'),
+                                     data={'connection_id': connection_id},
+                                     files={'users': file_obj})
+
+    def send_verification_email(self, body):
+        return self.client.post(self._url('verification-email'), data=body)
diff --git a/auth0/v2/rest.py b/auth0/v2/rest.py
new file mode 100644
index 0000000..771b7d4
--- /dev/null
+++ b/auth0/v2/rest.py
@@ -0,0 +1,62 @@
+import json
+import requests
+from .exceptions import Auth0Error
+
+
+class RestClient(object):
+
+    """Provides simple methods for handling all RESTful api endpoints. """
+
+    def __init__(self, jwt):
+        self.jwt = jwt
+
+    def get(self, url, params={}):
+        headers = {
+            'Authorization': 'Bearer %s' % self.jwt,
+        }
+
+        response = requests.get(url, params=params, headers=headers)
+        return self._process_response(response)
+
+    def post(self, url, data={}):
+        headers = {
+            'Authorization': 'Bearer %s' % self.jwt,
+            'Content-Type': 'application/json'
+        }
+
+        response = requests.post(url, data=json.dumps(data), headers=headers)
+        return self._process_response(response)
+
+    def file_post(self, url, data={}, files={}):
+        headers = {
+            'Authorization': 'Bearer %s' % self.jwt,
+        }
+
+        response = requests.post(url, data=data, files=files, headers=headers)
+        return self._process_response(response)
+
+    def patch(self, url, data={}):
+        headers = {
+            'Authorization': 'Bearer %s' % self.jwt,
+            'Content-Type': 'application/json'
+        }
+
+        response = requests.patch(url, data=json.dumps(data), headers=headers)
+        return self._process_response(response)
+
+    def delete(self, url):
+        headers = {
+            'Authorization': 'Bearer %s' % self.jwt,
+        }
+
+        response = requests.delete(url, headers=headers)
+        return self._process_response(response)
+
+    def _process_response(self, response):
+        text = json.loads(response.text) if response.text else {}
+
+        if isinstance(text, dict) and 'errorCode' in text:
+            raise Auth0Error(status_code=text['statusCode'],
+                             error_code=text['errorCode'],
+                             message=text['message'])
+        return text
diff --git a/auth0/v2/rules.py b/auth0/v2/rules.py
new file mode 100644
index 0000000..701e4f7
--- /dev/null
+++ b/auth0/v2/rules.py
@@ -0,0 +1,50 @@
+from .rest import RestClient
+
+
+class Rules(object):
+
+    """Rules endpoint implementation.
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+
+        jwt_token (str): An API token created with your account's global
+            keys. You can create one by using the token generator in the
+            API Explorer: https://auth0.com/docs/api/v2
+    """
+
+    def __init__(self, domain, jwt_token):
+        self.domain = domain
+        self.client = RestClient(jwt=jwt_token)
+
+    def _url(self, id=None):
+        url = 'https://%s/api/v2/rules' % self.domain
+        if id is not None:
+            return url + '/' + id
+        return url
+
+    def all(self, stage='login_success', enabled=True, fields=[],
+            include_fields=True):
+        """Retrieves a list of all rules.
+        """
+
+        params = {'fields': ','.join(fields) or None,
+                  'include_fields': str(include_fields).lower(),
+                  'enabled': str(enabled).lower(),
+                  'stage': stage}
+
+        return self.client.get(self._url(), params=params)
+
+    def create(self, body):
+        return self.client.post(self._url(), data=body)
+
+    def get(self, id, fields=[], include_fields=True):
+        params = {'fields': ','.join(fields) or None,
+                  'include_fields': str(include_fields).lower()}
+        return self.client.get(self._url(id), params=params)
+
+    def delete(self, id):
+        return self.client.delete(self._url(id))
+
+    def update(self, id, body):
+        return self.client.patch(self._url(id), data=body)
diff --git a/auth0/v2/stats.py b/auth0/v2/stats.py
new file mode 100644
index 0000000..86bb584
--- /dev/null
+++ b/auth0/v2/stats.py
@@ -0,0 +1,42 @@
+from .rest import RestClient
+
+
+class Stats(object):
+    """Auth0 stats endpoints
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+
+        jwt_token (str): An API token created with your account's global
+            keys. You can create one by using the token generator in the
+            API Explorer: https://auth0.com/docs/api/v2
+    """
+
+    def __init__(self, domain, jwt_token):
+        self.domain = domain
+        self.client = RestClient(jwt=jwt_token)
+
+    def _url(self, action):
+        return 'https://%s/api/v2/stats/%s' % (self.domain, action)
+
+    def active_users(self):
+        """Gets the active users count (logged in during the last 30 days).
+
+        Returns: An integer.
+        """
+
+        return self.client.get(self._url('active-users'))
+
+    def daily_stats(self, from_date=None, to_date=None):
+        """Gets the daily stats for a particular period.
+
+        Args:
+           from_date (str): The first day of the period (inclusive) in
+              YYYYMMDD format.
+
+           to_date (str): The last day of the period (inclusive) in
+              YYYYMMDD format.
+        """
+
+        return self.client.get(self._url('daily'), params={'from': from_date,
+                                                           'to': to_date})
diff --git a/auth0/v2/tickets.py b/auth0/v2/tickets.py
new file mode 100644
index 0000000..aaa55fc
--- /dev/null
+++ b/auth0/v2/tickets.py
@@ -0,0 +1,27 @@
+from .rest import RestClient
+
+
+class Tickets(object):
+
+    """Auth0 tickets endpoints
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+
+        jwt_token (str): An API token created with your account's global
+            keys. You can create one by using the token generator in the
+            API Explorer: https://auth0.com/docs/api/v2
+    """
+
+    def __init__(self, domain, jwt_token):
+        self.domain = domain
+        self.client = RestClient(jwt=jwt_token)
+
+    def _url(self, action):
+        return 'https://%s/api/v2/tickets/%s' % (self.domain, action)
+
+    def create_email_verification(self, body):
+        return self.client.post(self._url('email-verification'), data=body)
+
+    def create_pswd_change(self, body):
+        return self.client.post(self._url('password-change'), data=body)
diff --git a/auth0/v2/users.py b/auth0/v2/users.py
new file mode 100644
index 0000000..0d60aa6
--- /dev/null
+++ b/auth0/v2/users.py
@@ -0,0 +1,72 @@
+from .rest import RestClient
+
+
+class Users(object):
+
+    """Auth0 users endpoints
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+
+        jwt_token (str): An API token created with your account's global
+            keys. You can create one by using the token generator in the
+            API Explorer: https://auth0.com/docs/api/v2
+    """
+
+    def __init__(self, domain, jwt_token):
+        self.domain = domain
+        self.client = RestClient(jwt=jwt_token)
+
+    def _url(self, id=None):
+        url = 'https://%s/api/v2/users' % self.domain
+        if id is not None:
+            return url + '/' + id
+        return url
+
+    def list(self, page=0, per_page=25, sort=None, connection=None, q=None,
+             search_engine='v1', include_totals=True, fields=[],
+             include_fields=True):
+        params = {
+            'per_page': per_page,
+            'page': page,
+            'include_totals': str(include_totals).lower(),
+            'sort': sort,
+            'connection': connection,
+            'fields': ','.join(fields) or None,
+            'include_fields': str(include_fields).lower(),
+            'q': q,
+            'search_engine': search_engine
+        }
+        return self.client.get(self._url(), params=params)
+
+    def create(self, body):
+        return self.client.post(self._url(), data=body)
+
+    def delete_all_users(self):
+        return self.client.delete(self._url())
+
+    def get(self, id, fields=[], include_fields=True):
+        params = {
+            'fields': ','.join(fields) or None,
+            'include_fields': str(include_fields).lower()
+        }
+
+        return self.client.get(self._url(id), params=params)
+
+    def delete(self, id):
+        return self.client.delete(self._url(id))
+
+    def update(self, id, body):
+        return self.client.patch(self._url(id), data=body)
+
+    def delete_multifactor(self, id, provider):
+        url = self._url('%s/multifactor/%s' % (id, provider))
+        return self.client.delete(url)
+
+    def unlink_user_account(self, id, provider, user_id):
+        url = self._url('%s/identities/%s/%s' % (id, provider, user_id))
+        return self.client.delete(url)
+
+    def link_user_account(self, user_id, body):
+        url = self._url('%s/identities' % user_id)
+        return self.client.post(url, data=body)
diff --git a/examples/flask-webapp/README.md b/examples/flask-webapp/README.md
index 9581cac..e079b79 100644
--- a/examples/flask-webapp/README.md
+++ b/examples/flask-webapp/README.md
@@ -4,9 +4,9 @@ This is the seed project you need to use if you're going to create a Regular Web
 #Running the example
 In order to run the example you need to have `python` and `pip` installed.
 
-You also need to set the ClientSecret, ClientId, Domain and CallbackURL for your Auth0 app as environment variables with the following names respectively: AUTH0_CLIENT_SECRET, AUTH0_CLIENT_ID, AUTH0_DOMAIN and AUTH0_CALLBACK_URL.
+You also need to set the ClientSecret, ClientId, Domain and CallbackURL for your Auth0 app as enviroment variables with the following names respectively: AUTH0_CLIENT_SECRET, AUTH0_CLIENT_ID, AUTH0_DOMAIN and AUTH0_CALLBACK_URL.
 
-For that, the .env file should be created already; if not, simply create a file named .env in the directory and set the values like the following, the app will just work:
+For that, if you just create a file named .env in the directory and set the values like the following, the app will just work:
 
 ````bash
 # .env file
@@ -15,6 +15,4 @@ AUTH0_CLIENT_ID=myCoolClientId
 AUTH0_DOMAIN=samples.auth0.com
 AUTH0_CALLBACK_URL=http://localhost:3000/auth/auth0/callback
 ````
-Once you've set those 4 environment variables, just run `python server.py` and try calling [http://localhost:3000/](http://localhost:3000/), but you may change the port as needed.
-
-
+Once you've set those 4 enviroment variables, just run `python server.py` and try calling [http://localhost:3000/](http://localhost:3000/)
diff --git a/examples/flask-webapp/public/app.js b/examples/flask-webapp/public/app.js
index 16a837e..7a74d74 100644
--- a/examples/flask-webapp/public/app.js
+++ b/examples/flask-webapp/public/app.js
@@ -1,10 +1,14 @@
 $(document).ready(function() {
-     var lock = new Auth0Lock(AUTH0_CLIENT_ID, AUTH0_DOMAIN  );
+    var widget = new Auth0Widget({
+        // All this properties are set on auth0-variables.js
+        domain: AUTH0_DOMAIN,
+        clientID: AUTH0_CLIENT_ID,
+        callbackURL: AUTH0_CALLBACK_URL,
+        callbackOnLocationHash: false
+    });
 
     $('.btn-login').click(function(e) {
       e.preventDefault();
-      lock.show({
-        callbackURL: AUTH0_CALLBACK_URL
-    });
+      widget.signin();
     });
 });
diff --git a/examples/flask-webapp/server.py b/examples/flask-webapp/server.py
index df54eb4..c5e1ec4 100644
--- a/examples/flask-webapp/server.py
+++ b/examples/flask-webapp/server.py
@@ -23,7 +23,7 @@ app.debug = True
 def requires_auth(f):
   @wraps(f)
   def decorated(*args, **kwargs):
-    if 'profile' not in session:
+    if not session.has_key('profile'):
       return redirect('/')
     return f(*args, **kwargs)
 
diff --git a/examples/flask-webapp/templates/home.html b/examples/flask-webapp/templates/home.html
index 82ca7da..6db3847 100644
--- a/examples/flask-webapp/templates/home.html
+++ b/examples/flask-webapp/templates/home.html
@@ -1,7 +1,7 @@
 <html>
     <head>
         <script src="http://code.jquery.com/jquery-2.1.1.min.js" type="text/javascript"></script>
-        <script src="https://cdn.auth0.com/js/lock-7.9.min.js"></script>
+        <script src="https://cdn.auth0.com/w2/auth0-widget-5.js"></script>
 
         <script type="text/javascript" src="//use.typekit.net/iws6ohy.js"></script>
         <script type="text/javascript">try{Typekit.load();}catch(e){}</script>
@@ -32,4 +32,4 @@
             </div>
         </div>
     </body>
-</html>
\ No newline at end of file
+</html>


Test patch:
diff --git a/auth0/v2/test/__init__.py b/auth0/v2/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/auth0/v2/test/test_blacklists.py b/auth0/v2/test/test_blacklists.py
new file mode 100644
index 0000000..5774fbc
--- /dev/null
+++ b/auth0/v2/test/test_blacklists.py
@@ -0,0 +1,30 @@
+import unittest
+import mock
+from ..blacklists import Blacklists
+
+
+class TestBlacklists(unittest.TestCase):
+
+    @mock.patch('auth0.v2.blacklists.RestClient')
+    def test_get(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        t = Blacklists(domain='domain', jwt_token='jwttoken')
+        t.get(aud='an-id')
+
+        mock_instance.get.assert_called_with(
+            'https://domain/api/v2/blacklists/tokens',
+            params={'aud': 'an-id'}
+        )
+
+    @mock.patch('auth0.v2.blacklists.RestClient')
+    def test_create(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        t = Blacklists(domain='domain', jwt_token='jwttoken')
+        t.create(jti='the-jti', aud='the-aud')
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/blacklists/tokens', args[0])
+        self.assertEqual(kwargs['data'], {'jti': 'the-jti', 'aud': 'the-aud'})
diff --git a/auth0/v2/test/test_client.py b/auth0/v2/test/test_client.py
new file mode 100644
index 0000000..84c0d13
--- /dev/null
+++ b/auth0/v2/test/test_client.py
@@ -0,0 +1,83 @@
+import unittest
+import mock
+from ..client import Client
+
+
+class TestClient(unittest.TestCase):
+
+    @mock.patch('auth0.v2.client.RestClient')
+    def test_all(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = Client(domain='domain', jwt_token='jwttoken')
+        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'})
+
+        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'})
+
+    @mock.patch('auth0.v2.client.RestClient')
+    def test_create(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = Client(domain='domain', jwt_token='jwttoken')
+        c.create({'a': 'b', 'c': 'd'})
+
+        mock_instance.post.assert_called_with(
+            'https://domain/api/v2/clients',
+            data={'a': 'b', 'c': 'd'}
+        )
+
+    @mock.patch('auth0.v2.client.RestClient')
+    def test_get(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = Client(domain='domain', jwt_token='jwttoken')
+        c.get('this-id')
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/clients/this-id', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'include_fields': 'true'})
+
+        c.get('this-id', fields=['a', 'b'], include_fields=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/clients/this-id', args[0])
+        self.assertEqual(kwargs['params'], {'fields': 'a,b',
+                                            'include_fields': 'false'})
+
+    @mock.patch('auth0.v2.client.RestClient')
+    def test_delete(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = Client(domain='domain', jwt_token='jwttoken')
+        c.delete('this-id')
+
+        mock_instance.delete.assert_called_with(
+            'https://domain/api/v2/clients/this-id'
+        )
+
+    @mock.patch('auth0.v2.client.RestClient')
+    def test_update(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = Client(domain='domain', jwt_token='jwttoken')
+        c.update('this-id', {'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.patch.call_args
+
+        self.assertEqual('https://domain/api/v2/clients/this-id', args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
diff --git a/auth0/v2/test/test_connections.py b/auth0/v2/test/test_connections.py
new file mode 100644
index 0000000..905d256
--- /dev/null
+++ b/auth0/v2/test/test_connections.py
@@ -0,0 +1,104 @@
+import unittest
+import mock
+from ..connection import Connection
+
+
+class TestConnection(unittest.TestCase):
+
+    @mock.patch('auth0.v2.connection.RestClient')
+    def test_all(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.get.return_value = {}
+
+        c = Connection(domain='domain', jwt_token='jwttoken')
+        c.all()
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/connections', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'strategy': None,
+                                            'include_fields': 'true'})
+
+        c.all(fields=['a', 'b'], include_fields=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/connections', args[0])
+        self.assertEqual(kwargs['params'], {'fields': 'a,b',
+                                            'strategy': None,
+                                            'include_fields': 'false'})
+
+        c.all(fields=['a', 'b'], strategy='strategy', 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',
+                                            'include_fields': 'true'})
+
+    @mock.patch('auth0.v2.connection.RestClient')
+    def test_get(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.get.return_value = {}
+
+        c = Connection(domain='domain', jwt_token='jwttoken')
+        c.get('an-id')
+
+        args, kwargs = mock_instance.get.call_args
+        self.assertEqual('https://domain/api/v2/connections/an-id', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'include_fields': 'true'})
+
+        c.get('an-id', fields=['a', 'b'])
+
+        args, kwargs = mock_instance.get.call_args
+        self.assertEqual('https://domain/api/v2/connections/an-id', args[0])
+        self.assertEqual(kwargs['params'], {'fields': 'a,b',
+                                            'include_fields': 'true'})
+
+        c.get('an-id', fields=['a', 'b'], include_fields=False)
+
+        args, kwargs = mock_instance.get.call_args
+        self.assertEqual('https://domain/api/v2/connections/an-id', args[0])
+        self.assertEqual(kwargs['params'], {'fields': 'a,b',
+                                            'include_fields': 'false'})
+
+    @mock.patch('auth0.v2.connection.RestClient')
+    def test_delete(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.delete.return_value = {}
+
+        c = Connection(domain='domain', jwt_token='jwttoken')
+        c.delete('this-id')
+
+        mock_instance.delete.assert_called_with(
+            'https://domain/api/v2/connections/this-id'
+        )
+
+    @mock.patch('auth0.v2.connection.RestClient')
+    def test_update(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.patch.return_value = {}
+
+        c = Connection(domain='domain', jwt_token='jwttoken')
+        c.update('that-id', {'a': 'b', 'c': 'd'})
+
+        mock_instance.patch.assert_called_with(
+            'https://domain/api/v2/connections/that-id',
+            data={'a': 'b', 'c': 'd'}
+        )
+
+    @mock.patch('auth0.v2.connection.RestClient')
+    def test_create(self, mock_rc):
+        mock_instance = mock_rc.return_value
+        mock_instance.post.return_value = {}
+
+        c = Connection(domain='domain', jwt_token='jwttoken')
+        c.create({'a': 'b', 'c': 'd'})
+
+        mock_instance.post.assert_called_with(
+            'https://domain/api/v2/connections',
+            data={'a': 'b', 'c': 'd'}
+        )
diff --git a/auth0/v2/test/test_device_credentials.py b/auth0/v2/test/test_device_credentials.py
new file mode 100644
index 0000000..9675a8d
--- /dev/null
+++ b/auth0/v2/test/test_device_credentials.py
@@ -0,0 +1,45 @@
+import unittest
+import mock
+from ..device_credentials import DeviceCredentials
+
+
+class TestDeviceCredentials(unittest.TestCase):
+
+    @mock.patch('auth0.v2.device_credentials.RestClient')
+    def test_get(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = DeviceCredentials(domain='domain', jwt_token='jwttoken')
+        c.get(user_id='uid', client_id='cid', type='type')
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/device-credentials', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'include_fields': 'true',
+                                            'user_id': 'uid',
+                                            'client_id': 'cid',
+                                            'type': 'type'})
+
+    @mock.patch('auth0.v2.device_credentials.RestClient')
+    def test_create(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = DeviceCredentials(domain='domain', jwt_token='jwttoken')
+        c.create({'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/device-credentials', args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
+
+    @mock.patch('auth0.v2.device_credentials.RestClient')
+    def test_delete(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = DeviceCredentials(domain='domain', jwt_token='jwttoken')
+        c.delete('an-id')
+
+        mock_instance.delete.assert_called_with(
+            'https://domain/api/v2/device-credentials/an-id',
+        )
diff --git a/auth0/v2/test/test_emails.py b/auth0/v2/test/test_emails.py
new file mode 100644
index 0000000..9907974
--- /dev/null
+++ b/auth0/v2/test/test_emails.py
@@ -0,0 +1,62 @@
+import unittest
+import mock
+from ..emails import Emails
+
+
+class TestEmails(unittest.TestCase):
+
+    @mock.patch('auth0.v2.emails.RestClient')
+    def test_get(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        e = Emails(domain='domain', jwt_token='jwttoken')
+        e.get()
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/emails/provider', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'include_fields': 'true'})
+
+        e.get(fields=['a', 'b'], include_fields=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/emails/provider', args[0])
+        self.assertEqual(kwargs['params'], {'fields': 'a,b',
+                                            'include_fields': 'false'})
+
+    @mock.patch('auth0.v2.emails.RestClient')
+    def test_config(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        e = Emails(domain='domain', jwt_token='jwttoken')
+        e.config({'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/emails/provider', args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
+
+    @mock.patch('auth0.v2.emails.RestClient')
+    def test_update(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        e = Emails(domain='domain', jwt_token='jwttoken')
+        e.update({'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.patch.call_args
+
+        self.assertEqual('https://domain/api/v2/emails/provider', args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
+
+    @mock.patch('auth0.v2.emails.RestClient')
+    def test_delete(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        e = Emails(domain='domain', jwt_token='jwttoken')
+        e.delete()
+
+        mock_instance.delete.assert_called_with(
+            'https://domain/api/v2/emails/provider'
+        )
diff --git a/auth0/v2/test/test_jobs.py b/auth0/v2/test/test_jobs.py
new file mode 100644
index 0000000..c997a84
--- /dev/null
+++ b/auth0/v2/test/test_jobs.py
@@ -0,0 +1,42 @@
+import unittest
+import mock
+from ..jobs import Jobs
+
+
+class TestJobs(unittest.TestCase):
+
+    @mock.patch('auth0.v2.jobs.RestClient')
+    def test_get(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        j = Jobs(domain='domain', jwt_token='jwttoken')
+        j.get('an-id')
+
+        mock_instance.get.assert_called_with(
+            'https://domain/api/v2/jobs/an-id',
+        )
+
+    @mock.patch('auth0.v2.jobs.RestClient')
+    def test_import_users(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        j = Jobs(domain='domain', jwt_token='jwttoken')
+        j.import_users(connection_id='1234', file_obj={})
+
+        mock_instance.file_post.assert_called_with(
+            'https://domain/api/v2/jobs/users-imports',
+            data={'connection_id': '1234'},
+            files={'users': {}}
+        )
+
+    @mock.patch('auth0.v2.jobs.RestClient')
+    def test_verification_email(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        j = Jobs(domain='domain', jwt_token='jwttoken')
+        j.send_verification_email({'a': 'b', 'c': 'd'})
+
+        mock_instance.post.assert_called_with(
+            'https://domain/api/v2/jobs/verification-email',
+            data={'a': 'b', 'c': 'd'}
+        )
diff --git a/auth0/v2/test/test_rest.py b/auth0/v2/test/test_rest.py
new file mode 100644
index 0000000..548bd5a
--- /dev/null
+++ b/auth0/v2/test/test_rest.py
@@ -0,0 +1,135 @@
+import unittest
+import json
+import mock
+from ..rest import RestClient
+from ..exceptions import Auth0Error
+
+
+class TestRest(unittest.TestCase):
+
+    @mock.patch('requests.get')
+    def test_get(self, mock_get):
+        rc = RestClient(jwt='a-token')
+        headers = {'Authorization': 'Bearer a-token'}
+
+        mock_get.return_value.text = '["a", "b"]'
+
+        response = rc.get('the-url')
+        mock_get.assert_called_with('the-url', params={}, headers=headers)
+
+        self.assertEqual(response, ['a', 'b'])
+
+        response = rc.get(url='the/url', params={'A': 'param', 'B': 'param'})
+        mock_get.assert_called_with('the/url', params={'A': 'param',
+                                                       'B': 'param'},
+                                               headers=headers)
+        self.assertEqual(response, ['a', 'b'])
+
+        mock_get.return_value.text = ''
+        response = rc.get('the/url')
+        self.assertEqual(response, {})
+
+    @mock.patch('requests.get')
+    def test_get_errors(self, mock_get):
+        rc = RestClient(jwt='a-token')
+        headers = {'Authorization': 'Bearer a-token'}
+
+        mock_get.return_value.text = '{"statusCode": 999,' \
+                                     ' "errorCode": "code",' \
+                                     ' "message": "message"}'
+
+        with self.assertRaises(Auth0Error) as context:
+            rc.get('the/url')
+
+        self.assertEqual(context.exception.status_code, 999)
+        self.assertEqual(context.exception.error_code, 'code')
+        self.assertEqual(context.exception.message, 'message')
+
+    @mock.patch('requests.post')
+    def test_post(self, mock_post):
+        rc = RestClient(jwt='a-token')
+        headers = {'Authorization': 'Bearer a-token',
+                   'Content-Type': 'application/json'}
+
+        mock_post.return_value.text = '{"a": "b"}'
+
+        data = {'some': 'data'}
+
+        response = rc.post('the/url', data=data)
+        mock_post.assert_called_with('the/url', data=json.dumps(data),
+                                     headers=headers)
+
+        self.assertEqual(response, {'a': 'b'})
+
+    @mock.patch('requests.post')
+    def test_post_errors(self, mock_post):
+        rc = RestClient(jwt='a-token')
+
+        mock_post.return_value.text = '{"statusCode": 999,' \
+                                      ' "errorCode": "code",' \
+                                      ' "message": "message"}'
+
+        with self.assertRaises(Auth0Error) as context:
+            rc.post('the-url')
+
+        self.assertEqual(context.exception.status_code, 999)
+        self.assertEqual(context.exception.error_code, 'code')
+        self.assertEqual(context.exception.message, 'message')
+
+    @mock.patch('requests.patch')
+    def test_patch(self, mock_patch):
+        rc = RestClient(jwt='a-token')
+        headers = {'Authorization': 'Bearer a-token',
+                   'Content-Type': 'application/json'}
+
+        mock_patch.return_value.text = '["a", "b"]'
+
+        data = {'some': 'data'}
+
+        response = rc.patch(url='the-url', data=data)
+        mock_patch.assert_called_with('the-url', data=json.dumps(data),
+                                      headers=headers)
+
+        self.assertEqual(response, ['a', 'b'])
+
+    @mock.patch('requests.patch')
+    def test_patch_errors(self, mock_patch):
+        rc = RestClient(jwt='a-token')
+
+        mock_patch.return_value.text = '{"statusCode": 999,' \
+                                       ' "errorCode": "code",' \
+                                       ' "message": "message"}'
+
+        with self.assertRaises(Auth0Error) as context:
+            rc.patch(url='the/url')
+
+        self.assertEqual(context.exception.status_code, 999)
+        self.assertEqual(context.exception.error_code, 'code')
+        self.assertEqual(context.exception.message, 'message')
+
+    @mock.patch('requests.delete')
+    def test_delete(self, mock_delete):
+        rc = RestClient(jwt='a-token')
+        headers = {'Authorization': 'Bearer a-token'}
+
+        mock_delete.return_value.text = '["a", "b"]'
+
+        response = rc.delete(url='the-url/ID')
+        mock_delete.assert_called_with('the-url/ID', headers=headers)
+
+        self.assertEqual(response, ['a', 'b'])
+
+    @mock.patch('requests.delete')
+    def test_delete_errors(self, mock_delete):
+        rc = RestClient(jwt='a-token')
+
+        mock_delete.return_value.text = '{"statusCode": 999,' \
+                                        ' "errorCode": "code",' \
+                                        ' "message": "message"}'
+
+        with self.assertRaises(Auth0Error) as context:
+            rc.delete(url='the-url')
+
+        self.assertEqual(context.exception.status_code, 999)
+        self.assertEqual(context.exception.error_code, 'code')
+        self.assertEqual(context.exception.message, 'message')
diff --git a/auth0/v2/test/test_rules.py b/auth0/v2/test/test_rules.py
new file mode 100644
index 0000000..3313f93
--- /dev/null
+++ b/auth0/v2/test/test_rules.py
@@ -0,0 +1,88 @@
+import unittest
+import mock
+from ..rules import Rules
+
+
+class TestRules(unittest.TestCase):
+
+    @mock.patch('auth0.v2.rules.RestClient')
+    def test_all(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = Rules(domain='domain', jwt_token='jwttoken')
+        c.all()
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/rules', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'include_fields': 'true',
+                                            'enabled': 'true',
+                                            'stage': 'login_success'})
+
+        c.all(stage='stage', enabled=False, fields=['a', 'b'],
+              include_fields=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/rules', args[0])
+        self.assertEqual(kwargs['params'], {'fields': 'a,b',
+                                            'include_fields': 'false',
+                                            'enabled': 'false',
+                                            'stage': 'stage'})
+
+    @mock.patch('auth0.v2.rules.RestClient')
+    def test_create(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = Rules(domain='domain', jwt_token='jwttoken')
+        c.create({'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/rules', args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
+
+    @mock.patch('auth0.v2.rules.RestClient')
+    def test_get(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = Rules(domain='domain', jwt_token='jwttoken')
+        c.get('an-id')
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/rules/an-id', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'include_fields': 'true'})
+
+        c.get('an-id', fields=['a', 'b'], include_fields=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/rules/an-id', args[0])
+        self.assertEqual(kwargs['params'], {'fields': 'a,b',
+                                            'include_fields': 'false'})
+
+    @mock.patch('auth0.v2.rules.RestClient')
+    def test_delete(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = Rules(domain='domain', jwt_token='jwttoken')
+        c.delete('an-id')
+
+        mock_instance.delete.assert_called_with(
+            'https://domain/api/v2/rules/an-id'
+        )
+
+    @mock.patch('auth0.v2.rules.RestClient')
+    def test_update(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        c = Rules(domain='domain', jwt_token='jwttoken')
+        c.update('an-id', {'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.patch.call_args
+
+        self.assertEqual('https://domain/api/v2/rules/an-id', args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
diff --git a/auth0/v2/test/test_stats.py b/auth0/v2/test/test_stats.py
new file mode 100644
index 0000000..4ecf113
--- /dev/null
+++ b/auth0/v2/test/test_stats.py
@@ -0,0 +1,36 @@
+import unittest
+import mock
+from ..stats import Stats
+
+
+class TestTickets(unittest.TestCase):
+
+    @mock.patch('auth0.v2.stats.RestClient')
+    def test_active_users(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = Stats(domain='domain', jwt_token='jwttoken')
+        s.active_users()
+
+        mock_instance.get.assert_called_with(
+            'https://domain/api/v2/stats/active-users',
+        )
+
+    @mock.patch('auth0.v2.stats.RestClient')
+    def test_daily_stats(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = Stats(domain='domain', jwt_token='jwttoken')
+        s.daily_stats()
+
+        mock_instance.get.assert_called_with(
+            'https://domain/api/v2/stats/daily',
+            params={'from': None, 'to': None},
+        )
+
+        s.daily_stats(from_date='12341212', to_date='56785656')
+
+        mock_instance.get.assert_called_with(
+            'https://domain/api/v2/stats/daily',
+            params={'from': '12341212', 'to': '56785656'},
+        )
diff --git a/auth0/v2/test/test_tickets.py b/auth0/v2/test/test_tickets.py
new file mode 100644
index 0000000..64e00a3
--- /dev/null
+++ b/auth0/v2/test/test_tickets.py
@@ -0,0 +1,30 @@
+import unittest
+import mock
+from ..tickets import Tickets
+
+
+class TestTickets(unittest.TestCase):
+
+    @mock.patch('auth0.v2.tickets.RestClient')
+    def test_email(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        t = Tickets(domain='domain', jwt_token='jwttoken')
+        t.create_email_verification({'a': 'b', 'c': 'd'})
+
+        mock_instance.post.assert_called_with(
+            'https://domain/api/v2/tickets/email-verification',
+            data={'a': 'b', 'c': 'd'}
+        )
+
+    @mock.patch('auth0.v2.tickets.RestClient')
+    def test_pswd(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        t = Tickets(domain='domain', jwt_token='jwttoken')
+        t.create_pswd_change({'a': 'b', 'c': 'd'})
+
+        mock_instance.post.assert_called_with(
+            'https://domain/api/v2/tickets/password-change',
+            data={'a': 'b', 'c': 'd'}
+        )
diff --git a/auth0/v2/test/test_users.py b/auth0/v2/test/test_users.py
new file mode 100644
index 0000000..76ecf4e
--- /dev/null
+++ b/auth0/v2/test/test_users.py
@@ -0,0 +1,149 @@
+import unittest
+import mock
+from ..users import Users
+
+
+class TestUsers(unittest.TestCase):
+
+    @mock.patch('auth0.v2.users.RestClient')
+    def test_list(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', jwt_token='jwttoken')
+        u.list()
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/users', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 25,
+            'page': 0,
+            'include_totals': 'true',
+            'sort': None,
+            'connection': None,
+            'fields': None,
+            'include_fields': 'true',
+            'q': None,
+            'search_engine': 'v1'
+        })
+
+        u.list(page=1, per_page=50, sort='s', connection='con', q='q',
+               search_engine='se', include_totals=False, fields=['a', 'b'],
+               include_fields=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/users', args[0])
+        self.assertEqual(kwargs['params'], {
+            'per_page': 50,
+            'page': 1,
+            'include_totals': 'false',
+            'sort': 's',
+            'connection': 'con',
+            'fields': 'a,b',
+            'include_fields': 'false',
+            'q': 'q',
+            'search_engine': 'se'
+        })
+
+    @mock.patch('auth0.v2.users.RestClient')
+    def test_create(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', jwt_token='jwttoken')
+        u.create({'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/users', args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
+
+    @mock.patch('auth0.v2.users.RestClient')
+    def test_delete_all_users(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', jwt_token='jwttoken')
+        u.delete_all_users()
+
+        mock_instance.delete.assert_called_with(
+            'https://domain/api/v2/users'
+        )
+
+    @mock.patch('auth0.v2.users.RestClient')
+    def test_get(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', jwt_token='jwttoken')
+        u.get('an-id')
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/users/an-id', args[0])
+        self.assertEqual(kwargs['params'], {'fields': None,
+                                            'include_fields': 'true'})
+
+        u.get('an-id', fields=['a', 'b'], include_fields=False)
+
+        args, kwargs = mock_instance.get.call_args
+
+        self.assertEqual('https://domain/api/v2/users/an-id', args[0])
+        self.assertEqual(kwargs['params'], {'fields': 'a,b',
+                                            'include_fields': 'false'})
+
+    @mock.patch('auth0.v2.users.RestClient')
+    def test_delete(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', jwt_token='jwttoken')
+        u.delete('an-id')
+
+        mock_instance.delete.assert_called_with(
+            'https://domain/api/v2/users/an-id'
+        )
+
+    @mock.patch('auth0.v2.users.RestClient')
+    def test_update(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', jwt_token='jwttoken')
+        u.update('an-id', {'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.patch.call_args
+
+        self.assertEqual('https://domain/api/v2/users/an-id', args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})
+
+    @mock.patch('auth0.v2.users.RestClient')
+    def test_delete_multifactor(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', jwt_token='jwttoken')
+        u.delete_multifactor('an-id', 'provider')
+
+        mock_instance.delete.assert_called_with(
+            'https://domain/api/v2/users/an-id/multifactor/provider'
+        )
+
+    @mock.patch('auth0.v2.users.RestClient')
+    def test_unlink_user_account(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', jwt_token='jwttoken')
+        u.unlink_user_account('an-id', 'provider', 'user-id')
+
+        mock_instance.delete.assert_called_with(
+            'https://domain/api/v2/users/an-id/identities/provider/user-id'
+        )
+
+    @mock.patch('auth0.v2.users.RestClient')
+    def test_link_user_account(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        u = Users(domain='domain', jwt_token='jwttoken')
+        u.link_user_account('user-id', {'a': 'b', 'c': 'd'})
+
+        args, kwargs = mock_instance.post.call_args
+
+        self.assertEqual('https://domain/api/v2/users/user-id/identities',
+                         args[0])
+        self.assertEqual(kwargs['data'], {'a': 'b', 'c': 'd'})