Browse Source

[libs] Adding djangorestframework-simplejwt 3.3

Romain Rigaux 4 years ago
parent
commit
70a77e4915
33 changed files with 2470 additions and 0 deletions
  1. 19 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/LICENSE.txt
  2. 4 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/MANIFEST.in
  3. 519 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/PKG-INFO
  4. 499 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/README.rst
  5. 1 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/__init__.py
  6. 131 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/authentication.py
  7. 50 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/backends.py
  8. 55 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/compat.py
  9. 41 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/exceptions.py
  10. 106 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/models.py
  11. 136 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/serializers.py
  12. 72 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/settings.py
  13. 10 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/state.py
  14. 1 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/__init__.py
  15. 91 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/admin.py
  16. 9 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/apps.py
  17. 0 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/management/__init__.py
  18. 0 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/management/commands/__init__.py
  19. 11 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/management/commands/flushexpiredtokens.py
  20. 45 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0001_initial.py
  21. 20 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0002_outstandingtoken_jti_hex.py
  22. 34 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0003_auto_20171017_2007.py
  23. 20 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0004_auto_20171017_2013.py
  24. 19 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0005_remove_outstandingtoken_jti.py
  25. 20 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0006_auto_20171017_2113.py
  26. 27 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0007_auto_20171017_2214.py
  27. 0 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/__init__.py
  28. 49 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/models.py
  29. 298 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/tokens.py
  30. 34 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/utils.py
  31. 83 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/views.py
  32. 10 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/setup.cfg
  33. 56 0
      desktop/core/ext-py/djangorestframework_simplejwt-3.3/setup.py

+ 19 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/LICENSE.txt

@@ -0,0 +1,19 @@
+Copyright 2017 David Sanders
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 4 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/MANIFEST.in

@@ -0,0 +1,4 @@
+include README.rst
+include LICENSE.txt
+recursive-exclude * __pycache__
+recursive-exclude * *.py[co]

+ 519 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/PKG-INFO

@@ -0,0 +1,519 @@
+Metadata-Version: 1.1
+Name: djangorestframework_simplejwt
+Version: 3.3
+Summary: A minimal JSON Web Token authentication plugin for Django REST Framework
+Home-page: https://github.com/davesque/django-rest-framework-simplejwt
+Author: David Sanders
+Author-email: davesque@gmail.com
+License: MIT
+Description: Simple JWT
+        ==========
+        
+        A JSON Web Token authentication plugin for the `Django REST Framework
+        <http://www.django-rest-framework.org/>`__.
+        
+        .. image:: https://travis-ci.org/davesque/django-rest-framework-simplejwt.svg?branch=master
+          :target: https://travis-ci.org/davesque/django-rest-framework-simplejwt
+        .. image:: https://codecov.io/gh/davesque/django-rest-framework-simplejwt/branch/master/graph/badge.svg
+          :target: https://codecov.io/gh/davesque/django-rest-framework-simplejwt
+        
+        -------------------------------------------------------------------------------
+        
+        Simple JWT provides a JSON Web Token authentication backend for the Django REST
+        Framework.  It aims to provide an out-of-the-box solution for JWT
+        authentication which avoids some of the common pitfalls of the JWT
+        specification.  Assuming users of the library don't extensively and invasively
+        subclass everything, Simple JWT's behavior shouldn't be surprising.  Settings
+        variable defaults should be safe.
+        
+        Requirements
+        ------------
+        
+        * Python (2.7, 3.5, 3.6, 3.7)
+        * Django (1.11, 2.0, 2.1)
+        * Django REST Framework (3.5, 3.6, 3.7, 3.8, 3.9)
+        
+        These are the officially supported python and package versions.  Other versions
+        will probably work.  You're free to modify the tox config and see what is
+        possible.
+        
+        Installation
+        ------------
+        
+        Simple JWT can be installed with pip::
+        
+          pip install djangorestframework_simplejwt
+        
+        Then, your django project must be configured to use the library.  In
+        ``settings.py``, add
+        ``rest_framework_simplejwt.authentication.JWTAuthentication`` to the list of
+        authentication classes:
+        
+        .. code-block:: python
+        
+          REST_FRAMEWORK = {
+              ...
+              'DEFAULT_AUTHENTICATION_CLASSES': (
+                  ...
+                  'rest_framework_simplejwt.authentication.JWTAuthentication',
+              )
+              ...
+          }
+        
+        Also, in your root ``urls.py`` file (or any other url config), include routes
+        for Simple JWT's ``TokenObtainPairView`` and ``TokenRefreshView`` views:
+        
+        .. code-block:: python
+        
+          from rest_framework_simplejwt.views import (
+              TokenObtainPairView,
+              TokenRefreshView,
+          )
+        
+          urlpatterns = [
+              ...
+              url(r'^api/token/$', TokenObtainPairView.as_view(), name='token_obtain_pair'),
+              url(r'^api/token/refresh/$', TokenRefreshView.as_view(), name='token_refresh'),
+              ...
+          ]
+        
+        You can also include a route for Simple JWT's ``TokenVerifyView`` if you wish to
+        allow API users to verify HMAC-signed tokens without having access to your
+        signing key:
+        
+        .. code-block:: python
+        
+          urlpatterns = [
+              ...
+              url(r'^api/token/verify/$', TokenVerifyView.as_view(), name='token_verify'),
+              ...
+          ]
+        
+        Usage
+        -----
+        
+        To verify that Simple JWT is working, you can use curl to issue a couple of
+        test requests:
+        
+        .. code-block:: bash
+        
+          curl \
+            -X POST \
+            -H "Content-Type: application/json" \
+            -d '{"username": "davidattenborough", "password": "boatymcboatface"}' \
+            http://localhost:8000/api/token/
+        
+          ...
+          {
+            "access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU",
+            "refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"
+          }
+        
+        You can use the returned access token to prove authentication for a protected
+        view:
+        
+        .. code-block:: bash
+        
+          curl \
+            -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU" \
+            http://localhost:8000/api/some-protected-view/
+        
+        When this short-lived access token expires, you can use the longer-lived
+        refresh token to obtain another access token:
+        
+        .. code-block:: bash
+        
+          curl \
+            -X POST \
+            -H "Content-Type: application/json" \
+            -d '{"refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"}' \
+            http://localhost:8000/api/token/refresh/
+        
+          ...
+          {"access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNTY3LCJqdGkiOiJjNzE4ZTVkNjgzZWQ0NTQyYTU0NWJkM2VmMGI0ZGQ0ZSJ9.ekxRxgb9OKmHkfy-zs1Ro_xs1eMLXiR17dIDBVxeT-w"}
+        
+        Settings
+        --------
+        
+        Some of Simple JWT's behavior can be customized through settings variables in
+        ``settings.py``:
+        
+        .. code-block:: python
+        
+          # Django project settings.py
+        
+          from datetime import timedelta
+        
+          ...
+        
+          SIMPLE_JWT = {
+              'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
+              'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
+              'ROTATE_REFRESH_TOKENS': False,
+              'BLACKLIST_AFTER_ROTATION': True,
+        
+              'ALGORITHM': 'HS256',
+              'SIGNING_KEY': settings.SECRET_KEY,
+              'VERIFYING_KEY': None,
+        
+              'AUTH_HEADER_TYPES': ('Bearer',),
+              'USER_ID_FIELD': 'id',
+              'USER_ID_CLAIM': 'user_id',
+        
+              'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
+              'TOKEN_TYPE_CLAIM': 'token_type',
+        
+              'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
+              'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
+              'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
+          }
+        
+        Above, the default values for these settings are shown.
+        
+        -------------------------------------------------------------------------------
+        
+        ACCESS_TOKEN_LIFETIME
+          A ``datetime.timedelta`` object which specifies how long access tokens are
+          valid.  This ``timedelta`` value is added to the current UTC time during
+          token generation to obtain the token's default "exp" claim value.
+        
+        REFRESH_TOKEN_LIFETIME
+          A ``datetime.timedelta`` object which specifies how long refresh tokens are
+          valid.  This ``timedelta`` value is added to the current UTC time during
+          token generation to obtain the token's default "exp" claim value.
+        
+        ROTATE_REFRESH_TOKENS
+          When set to ``True``, if a refresh token is submitted to the
+          ``TokenRefreshView``, a new refresh token will be returned along with the new
+          access token.  This new refresh token will be supplied via a "refresh" key in
+          the JSON response.  New refresh tokens will have a renewed expiration time
+          which is determined by adding the timedelta in the ``REFRESH_TOKEN_LIFETIME``
+          setting to the current time when the request is made.  If the blacklist app
+          is in use and the ``BLACKLIST_AFTER_ROTATION`` setting is set to ``True``,
+          refresh tokens submitted to the refresh view will be added to the blacklist.
+        
+        BLACKLIST_AFTER_ROTATION
+          When set to ``True``, causes refresh tokens submitted to the
+          ``TokenRefreshView`` to be added to the blacklist if the blacklist app is in
+          use and the ``ROTATE_REFRESH_TOKENS`` setting is set to ``True``.
+        
+        ALGORITHM
+          The algorithm from the PyJWT library which will be used to perform
+          signing/verification operations on tokens.  To use symmetric HMAC signing and
+          verification, the following algorithms may be used: ``'HS256'``, ``'HS384'``,
+          ``'HS512'``.  When an HMAC algorithm is chosen, the ``SIGNING_KEY`` setting
+          will be used as both the signing key and the verifying key.  In that case,
+          the ``VERIFYING_KEY`` setting will be ignored.  To use asymmetric RSA signing
+          and verification, the following algorithms may be used: ``'RS256'``,
+          ``'RS384'``, ``'RS512'``.  When an RSA algorithm is chosen, the
+          ``SIGNING_KEY`` setting must be set to a string which contains an RSA private
+          key.  Likewise, the ``VERIFYING_KEY`` setting must be set to a string which
+          contains an RSA public key.
+        
+        SIGNING_KEY
+          The signing key which is used to sign the content of generated tokens.  For
+          HMAC signing, this should be a random string with at least as many bits of
+          data as is required by the signing protocol.  For RSA signing, this
+          should be a string which contains an RSA private key which is 2048 bits or
+          longer.  Since Simple JWT defaults to using 256-bit HMAC signing, the
+          ``SIGNING_KEY`` setting defaults to the value of the ``SECRET_KEY`` setting
+          for your django project.  Although this is the most reasonable default that
+          Simple JWT can provide, it is recommended that developers change this setting
+          to a value which is independent from the django project secret key.  This
+          will make changing the signing key used for tokens easier in the event that
+          it is compromised.
+        
+        VERIFYING_KEY
+          The verifying key which is used to verify the content of generated tokens.
+          If an HMAC algorithm has been specified by the ``ALGORITHM`` setting, the
+          ``VERIFYING_KEY`` setting will be ignored and the value of the
+          ``SIGNING_KEY`` setting will be used.  If an RSA algorithm has been specified
+          by the ``ALGORITHM`` setting, the ``VERIFYING_KEY`` setting must be set to a
+          string which contains an RSA public key.
+        
+        AUTH_HEADER_TYPES
+          The authorization header type(s) that will be accepted for views that require
+          authentication.  For example, a value of ``'Bearer'`` means that views
+          requiring authentication would look for a header with the following format:
+          ``Authorization: Bearer <token>``.  This setting may also contain a list or
+          tuple of possible header types (e.g. ``('Bearer', 'JWT')``).  If a list or
+          tuple is used in this way, and authentication fails, the first item in the
+          collection will be used to build the "WWW-Authenticate" header in the
+          response.
+        
+        USER_ID_FIELD
+          The database field from the user model that will be included in generated
+          tokens to identify users.  It is recommended that the value of this setting
+          specifies a field which does not normally change once its initial value is
+          chosen.  For example, specifying a "username" or "email" field would be a
+          poor choice since an account's username or email might change depending on
+          how account management in a given service is designed.  This could allow a
+          new account to be created with an old username while an existing token is
+          still valid which uses that username as a user identifier.
+        
+        USER_ID_CLAIM
+          The claim in generated tokens which will be used to store user identifiers.
+          For example, a setting value of ``'user_id'`` would mean generated tokens
+          include a "user_id" claim that contains the user's identifier.
+        
+        AUTH_TOKEN_CLASSES
+          A list of dot paths to classes which specify the types of token that are
+          allowed to prove authentication.  More about this in the "Token types"
+          section below.
+        
+        TOKEN_TYPE_CLAIM
+          The claim name that is used to store a token's type.  More about this in the
+          "Token types" section below.
+        
+        SLIDING_TOKEN_LIFETIME
+          A ``datetime.timedelta`` object which specifies how long sliding tokens are
+          valid to prove authentication.  This ``timedelta`` value is added to the
+          current UTC time during token generation to obtain the token's default "exp"
+          claim value.  More about this in the "Sliding tokens" section below.
+        
+        SLIDING_TOKEN_REFRESH_LIFETIME
+          A ``datetime.timedelta`` object which specifies how long sliding tokens are
+          valid to be refreshed.  This ``timedelta`` value is added to the current UTC
+          time during token generation to obtain the token's default "exp" claim value.
+          More about this in the "Sliding tokens" section below.
+        
+        SLIDING_TOKEN_REFRESH_EXP_CLAIM
+          The claim name that is used to store the exipration time of a sliding token's
+          refresh period.  More about this in the "Sliding tokens" section below.
+        
+        Customizing token claims
+        ------------------------
+        
+        If you wish to customize the claims contained in web tokens which are generated
+        by the ``TokenObtainPairView`` and ``TokenObtainSlidingView`` views, create a
+        subclass for the desired view as well as a subclass for its corresponding
+        serializer.  Here's an example of how to customize the claims in tokens
+        generated by the ``TokenObtainPairView``:
+        
+        .. code-block:: python
+        
+          from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
+          from rest_framework_simplejwt.views import TokenObtainPairView
+        
+          class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
+              @classmethod
+              def get_token(cls, user):
+                  token = super(MyTokenObtainPairSerializer, cls).get_token(user)
+        
+                  # Add custom claims
+                  token['name'] = user.name
+                  # ...
+        
+                  return token
+        
+          class MyTokenObtainPairView(TokenObtainPairView):
+              serializer_class = MyTokenObtainPairSerializer
+        
+        Note that the example above will cause the customized claims to be present in
+        both refresh *and* access tokens which are generated by the view.  This follows
+        from the fact that the ``get_token`` method above produces the *refresh* token
+        for the view, which is in turn used to generate the view's access token.
+        
+        As with the standard token views, you'll also need to include a url route to
+        your subclassed view.
+        
+        Token types
+        -----------
+        
+        Simple JWT provides two different token types which can be used to prove
+        authentication.  In a token's payload, its type can be identified by the value
+        of its token type claim, which is "token_type" by default.  This may have a
+        value of "access", "sliding", or "refresh" however refresh tokens are not
+        considered valid for authentication at this time.  The claim name used to store
+        the type can be customized by changing the ``TOKEN_TYPE_CLAIM`` setting.
+        
+        By default, Simple JWT expects an "access" token to prove authentication.  The
+        allowed auth token types are determined by the value of the
+        ``AUTH_TOKEN_CLASSES`` setting.  This setting contains a list of dot paths to
+        token classes.  It includes the
+        ``'rest_framework_simplejwt.tokens.AccessToken'`` dot path by default but may
+        also include the ``'rest_framework_simplejwt.tokens.SlidingToken'`` dot path.
+        Either or both of those dot paths may be present in the list of auth token
+        classes.  If they are both present, then both of those token types may be used
+        to prove authentication.
+        
+        Sliding tokens
+        --------------
+        
+        Sliding tokens offer a more convenient experience to users of tokens with the
+        trade-offs of being less secure and, in the case that the blacklist app is
+        being used, less performant.  A sliding token is one which contains both an
+        expiration claim and a refresh expiration claim.  As long as the timestamp in a
+        sliding token's expiration claim has not passed, it can be used to prove
+        authentication.  Additionally, as long as the timestamp in its refresh
+        expiration claim has not passed, it may also be submitted to a refresh view to
+        get another copy of itself with a renewed expiration claim.
+        
+        If you want to use sliding tokens, change the ``AUTH_TOKEN_CLASSES`` setting to
+        ``('rest_framework_simplejwt.tokens.SlidingToken',)``.  (Alternatively, the
+        ``AUTH_TOKEN_CLASSES`` setting may include dot paths to both the
+        ``AccessToken`` and ``SlidingToken`` token classes in the
+        ``rest_framework_simplejwt.tokens`` module if you want to allow both token
+        types to be used for authentication.)
+        
+        Also, include urls for the sliding token specific ``TokenObtainSlidingView``
+        and ``TokenRefreshSlidingView`` views along side or in place of urls for the
+        access token specific ``TokenObtainPairView`` and ``TokenRefreshView`` views:
+        
+        .. code-block:: python
+        
+          from rest_framework_simplejwt.views import (
+              TokenObtainSlidingView,
+              TokenRefreshSlidingView,
+          )
+        
+          urlpatterns = [
+              ...
+              url(r'^api/token/$', TokenObtainSlidingView.as_view(), name='token_obtain'),
+              url(r'^api/token/refresh/$', TokenRefreshSlidingView.as_view(), name='token_refresh'),
+              ...
+          ]
+        
+        Be aware that, if you are using the blacklist app, Simple JWT will validate all
+        sliding tokens against the blacklist for each authenticated request.  This will
+        reduce the performance of authenticated API views.
+        
+        Blacklist app
+        -------------
+        
+        Simple JWT includes an app that provides token blacklist functionality.  To use
+        this app, include it in your list of installed apps in ``settings.py``:
+        
+        .. code-block:: python
+        
+          # Django project settings.py
+        
+          ...
+        
+          INSTALLED_APPS = (
+              ...
+              'rest_framework_simplejwt.token_blacklist',
+              ...
+          }
+        
+        Also, make sure to run ``python manage.py migrate`` to run the app's
+        migrations.
+        
+        If the blacklist app is detected in ``INSTALLED_APPS``, Simple JWT will add any
+        generated refresh or sliding tokens to a list of outstanding tokens.  It will
+        also check that any refresh or sliding token does not appear in a blacklist of
+        tokens before it considers it as valid.
+        
+        The Simple JWT blacklist app implements its outstanding and blacklisted token
+        lists using two model: ``OutstandingToken`` and ``BlacklistedToken``.  Model
+        admins are defined for both of these models.  To add a token to the blacklist,
+        find its corresponding ``OutstandingToken`` record in the admin and use the
+        admin again to create a ``BlacklistedToken`` record that points to the
+        ``OutstandingToken`` record.
+        
+        Alternatively, you can blacklist a token by creating a ``BlacklistMixin``
+        subclass instance and calling the instance's ``blacklist`` method:
+        
+        .. code-block:: python
+        
+          from rest_framework_simplejwt.tokens import RefreshToken
+        
+          token = RefreshToken(base64_encoded_token_string)
+          token.blacklist()
+        
+        This will create unique outstanding token and blacklist records for the token's
+        "jti" claim.
+        
+        The blacklist app also provides a management command, ``flushexpiredtokens``,
+        which will delete any tokens from the outstanding list and blacklist that have
+        expired.  You should set up a cron job on your server or hosting platform which
+        runs this command daily.
+        
+        Experimental features
+        ---------------------
+        
+        JWTTokenUserAuthentication backend
+          The ``JWTTokenUserAuthentication`` backend's ``authenticate`` method does not
+          perform a database lookup to obtain a user instance.  Instead, it returns a
+          ``rest_framework_simplejwt.models.TokenUser`` instance which acts as a
+          stateless user object backed only by a validated token instead of a record in
+          a database.  This can facilitate developing single sign-on functionality
+          between separately hosted Django apps which all share the same token secret
+          key.  To use this feature, add the
+          ``rest_framework_simplejwt.authentication.JWTTokenUserAuthentication``
+          backend (instead of the default ``JWTAuthentication`` backend) to the Django
+          REST Framework's ``DEFAULT_AUTHENTICATION_CLASSES`` config setting:
+        
+          .. code-block:: python
+        
+            REST_FRAMEWORK = {
+                ...
+                'DEFAULT_AUTHENTICATION_CLASSES': (
+                    ...
+                    'rest_framework_simplejwt.authentication.JWTTokenUserAuthentication',
+                )
+                ...
+            }
+        
+        Running the Tests
+        -----------------
+        
+        To run the tests, first `install pyenv
+        <https://github.com/pyenv/pyenv#installation>`__.  Next, install the relevant
+        Python minor versions and create a ``.python-version`` file in your cloned
+        repo's directory:
+        
+        .. code-block:: bash
+        
+          pyenv install 3.6.x
+          pyenv install 3.5.x
+          pyenv install 3.4.x
+          pyenv install 2.7.x
+          cd <repo directory>
+          cat > .python-version <<EOF
+          3.6.x
+          3.5.x
+          3.4.x
+          2.7.x
+          EOF
+        
+        Above, the ``x`` in each case should be replaced with the latest corresponding
+        patch version.  The ``.python-version`` file will tell pyenv and tox that
+        you're testing against multiple versions of Python.  Next, install and run tox:
+        
+        .. code-block:: bash
+        
+          pip install tox
+          tox
+        
+        Alternatively, if you'd like to avoid using tox, you can set up your
+        environment and run the tests manually:
+        
+        .. code-block:: bash
+        
+          pip install -r requirements.txt
+          ./setup.py develop
+          ./runtests.py
+        
+        Acknowledgements
+        ----------------
+        
+        This project borrows code from the `Django REST Framework
+        <https://github.com/encode/django-rest-framework/>`__ as well as concepts from
+        the implementation of another JSON web token library for the Django REST
+        Framework, `django-rest-framework-jwt
+        <https://github.com/GetBlimp/django-rest-framework-jwt>`__.  The licenses from
+        both of those projects have been included in this repository in the "licenses"
+        directory.
+        
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Internet :: WWW/HTTP

+ 499 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/README.rst

@@ -0,0 +1,499 @@
+Simple JWT
+==========
+
+A JSON Web Token authentication plugin for the `Django REST Framework
+<http://www.django-rest-framework.org/>`__.
+
+.. image:: https://travis-ci.org/davesque/django-rest-framework-simplejwt.svg?branch=master
+  :target: https://travis-ci.org/davesque/django-rest-framework-simplejwt
+.. image:: https://codecov.io/gh/davesque/django-rest-framework-simplejwt/branch/master/graph/badge.svg
+  :target: https://codecov.io/gh/davesque/django-rest-framework-simplejwt
+
+-------------------------------------------------------------------------------
+
+Simple JWT provides a JSON Web Token authentication backend for the Django REST
+Framework.  It aims to provide an out-of-the-box solution for JWT
+authentication which avoids some of the common pitfalls of the JWT
+specification.  Assuming users of the library don't extensively and invasively
+subclass everything, Simple JWT's behavior shouldn't be surprising.  Settings
+variable defaults should be safe.
+
+Requirements
+------------
+
+* Python (2.7, 3.5, 3.6, 3.7)
+* Django (1.11, 2.0, 2.1)
+* Django REST Framework (3.5, 3.6, 3.7, 3.8, 3.9)
+
+These are the officially supported python and package versions.  Other versions
+will probably work.  You're free to modify the tox config and see what is
+possible.
+
+Installation
+------------
+
+Simple JWT can be installed with pip::
+
+  pip install djangorestframework_simplejwt
+
+Then, your django project must be configured to use the library.  In
+``settings.py``, add
+``rest_framework_simplejwt.authentication.JWTAuthentication`` to the list of
+authentication classes:
+
+.. code-block:: python
+
+  REST_FRAMEWORK = {
+      ...
+      'DEFAULT_AUTHENTICATION_CLASSES': (
+          ...
+          'rest_framework_simplejwt.authentication.JWTAuthentication',
+      )
+      ...
+  }
+
+Also, in your root ``urls.py`` file (or any other url config), include routes
+for Simple JWT's ``TokenObtainPairView`` and ``TokenRefreshView`` views:
+
+.. code-block:: python
+
+  from rest_framework_simplejwt.views import (
+      TokenObtainPairView,
+      TokenRefreshView,
+  )
+
+  urlpatterns = [
+      ...
+      url(r'^api/token/$', TokenObtainPairView.as_view(), name='token_obtain_pair'),
+      url(r'^api/token/refresh/$', TokenRefreshView.as_view(), name='token_refresh'),
+      ...
+  ]
+
+You can also include a route for Simple JWT's ``TokenVerifyView`` if you wish to
+allow API users to verify HMAC-signed tokens without having access to your
+signing key:
+
+.. code-block:: python
+
+  urlpatterns = [
+      ...
+      url(r'^api/token/verify/$', TokenVerifyView.as_view(), name='token_verify'),
+      ...
+  ]
+
+Usage
+-----
+
+To verify that Simple JWT is working, you can use curl to issue a couple of
+test requests:
+
+.. code-block:: bash
+
+  curl \
+    -X POST \
+    -H "Content-Type: application/json" \
+    -d '{"username": "davidattenborough", "password": "boatymcboatface"}' \
+    http://localhost:8000/api/token/
+
+  ...
+  {
+    "access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU",
+    "refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"
+  }
+
+You can use the returned access token to prove authentication for a protected
+view:
+
+.. code-block:: bash
+
+  curl \
+    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU" \
+    http://localhost:8000/api/some-protected-view/
+
+When this short-lived access token expires, you can use the longer-lived
+refresh token to obtain another access token:
+
+.. code-block:: bash
+
+  curl \
+    -X POST \
+    -H "Content-Type: application/json" \
+    -d '{"refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"}' \
+    http://localhost:8000/api/token/refresh/
+
+  ...
+  {"access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNTY3LCJqdGkiOiJjNzE4ZTVkNjgzZWQ0NTQyYTU0NWJkM2VmMGI0ZGQ0ZSJ9.ekxRxgb9OKmHkfy-zs1Ro_xs1eMLXiR17dIDBVxeT-w"}
+
+Settings
+--------
+
+Some of Simple JWT's behavior can be customized through settings variables in
+``settings.py``:
+
+.. code-block:: python
+
+  # Django project settings.py
+
+  from datetime import timedelta
+
+  ...
+
+  SIMPLE_JWT = {
+      'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
+      'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
+      'ROTATE_REFRESH_TOKENS': False,
+      'BLACKLIST_AFTER_ROTATION': True,
+
+      'ALGORITHM': 'HS256',
+      'SIGNING_KEY': settings.SECRET_KEY,
+      'VERIFYING_KEY': None,
+
+      'AUTH_HEADER_TYPES': ('Bearer',),
+      'USER_ID_FIELD': 'id',
+      'USER_ID_CLAIM': 'user_id',
+
+      'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
+      'TOKEN_TYPE_CLAIM': 'token_type',
+
+      'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
+      'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
+      'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
+  }
+
+Above, the default values for these settings are shown.
+
+-------------------------------------------------------------------------------
+
+ACCESS_TOKEN_LIFETIME
+  A ``datetime.timedelta`` object which specifies how long access tokens are
+  valid.  This ``timedelta`` value is added to the current UTC time during
+  token generation to obtain the token's default "exp" claim value.
+
+REFRESH_TOKEN_LIFETIME
+  A ``datetime.timedelta`` object which specifies how long refresh tokens are
+  valid.  This ``timedelta`` value is added to the current UTC time during
+  token generation to obtain the token's default "exp" claim value.
+
+ROTATE_REFRESH_TOKENS
+  When set to ``True``, if a refresh token is submitted to the
+  ``TokenRefreshView``, a new refresh token will be returned along with the new
+  access token.  This new refresh token will be supplied via a "refresh" key in
+  the JSON response.  New refresh tokens will have a renewed expiration time
+  which is determined by adding the timedelta in the ``REFRESH_TOKEN_LIFETIME``
+  setting to the current time when the request is made.  If the blacklist app
+  is in use and the ``BLACKLIST_AFTER_ROTATION`` setting is set to ``True``,
+  refresh tokens submitted to the refresh view will be added to the blacklist.
+
+BLACKLIST_AFTER_ROTATION
+  When set to ``True``, causes refresh tokens submitted to the
+  ``TokenRefreshView`` to be added to the blacklist if the blacklist app is in
+  use and the ``ROTATE_REFRESH_TOKENS`` setting is set to ``True``.
+
+ALGORITHM
+  The algorithm from the PyJWT library which will be used to perform
+  signing/verification operations on tokens.  To use symmetric HMAC signing and
+  verification, the following algorithms may be used: ``'HS256'``, ``'HS384'``,
+  ``'HS512'``.  When an HMAC algorithm is chosen, the ``SIGNING_KEY`` setting
+  will be used as both the signing key and the verifying key.  In that case,
+  the ``VERIFYING_KEY`` setting will be ignored.  To use asymmetric RSA signing
+  and verification, the following algorithms may be used: ``'RS256'``,
+  ``'RS384'``, ``'RS512'``.  When an RSA algorithm is chosen, the
+  ``SIGNING_KEY`` setting must be set to a string which contains an RSA private
+  key.  Likewise, the ``VERIFYING_KEY`` setting must be set to a string which
+  contains an RSA public key.
+
+SIGNING_KEY
+  The signing key which is used to sign the content of generated tokens.  For
+  HMAC signing, this should be a random string with at least as many bits of
+  data as is required by the signing protocol.  For RSA signing, this
+  should be a string which contains an RSA private key which is 2048 bits or
+  longer.  Since Simple JWT defaults to using 256-bit HMAC signing, the
+  ``SIGNING_KEY`` setting defaults to the value of the ``SECRET_KEY`` setting
+  for your django project.  Although this is the most reasonable default that
+  Simple JWT can provide, it is recommended that developers change this setting
+  to a value which is independent from the django project secret key.  This
+  will make changing the signing key used for tokens easier in the event that
+  it is compromised.
+
+VERIFYING_KEY
+  The verifying key which is used to verify the content of generated tokens.
+  If an HMAC algorithm has been specified by the ``ALGORITHM`` setting, the
+  ``VERIFYING_KEY`` setting will be ignored and the value of the
+  ``SIGNING_KEY`` setting will be used.  If an RSA algorithm has been specified
+  by the ``ALGORITHM`` setting, the ``VERIFYING_KEY`` setting must be set to a
+  string which contains an RSA public key.
+
+AUTH_HEADER_TYPES
+  The authorization header type(s) that will be accepted for views that require
+  authentication.  For example, a value of ``'Bearer'`` means that views
+  requiring authentication would look for a header with the following format:
+  ``Authorization: Bearer <token>``.  This setting may also contain a list or
+  tuple of possible header types (e.g. ``('Bearer', 'JWT')``).  If a list or
+  tuple is used in this way, and authentication fails, the first item in the
+  collection will be used to build the "WWW-Authenticate" header in the
+  response.
+
+USER_ID_FIELD
+  The database field from the user model that will be included in generated
+  tokens to identify users.  It is recommended that the value of this setting
+  specifies a field which does not normally change once its initial value is
+  chosen.  For example, specifying a "username" or "email" field would be a
+  poor choice since an account's username or email might change depending on
+  how account management in a given service is designed.  This could allow a
+  new account to be created with an old username while an existing token is
+  still valid which uses that username as a user identifier.
+
+USER_ID_CLAIM
+  The claim in generated tokens which will be used to store user identifiers.
+  For example, a setting value of ``'user_id'`` would mean generated tokens
+  include a "user_id" claim that contains the user's identifier.
+
+AUTH_TOKEN_CLASSES
+  A list of dot paths to classes which specify the types of token that are
+  allowed to prove authentication.  More about this in the "Token types"
+  section below.
+
+TOKEN_TYPE_CLAIM
+  The claim name that is used to store a token's type.  More about this in the
+  "Token types" section below.
+
+SLIDING_TOKEN_LIFETIME
+  A ``datetime.timedelta`` object which specifies how long sliding tokens are
+  valid to prove authentication.  This ``timedelta`` value is added to the
+  current UTC time during token generation to obtain the token's default "exp"
+  claim value.  More about this in the "Sliding tokens" section below.
+
+SLIDING_TOKEN_REFRESH_LIFETIME
+  A ``datetime.timedelta`` object which specifies how long sliding tokens are
+  valid to be refreshed.  This ``timedelta`` value is added to the current UTC
+  time during token generation to obtain the token's default "exp" claim value.
+  More about this in the "Sliding tokens" section below.
+
+SLIDING_TOKEN_REFRESH_EXP_CLAIM
+  The claim name that is used to store the exipration time of a sliding token's
+  refresh period.  More about this in the "Sliding tokens" section below.
+
+Customizing token claims
+------------------------
+
+If you wish to customize the claims contained in web tokens which are generated
+by the ``TokenObtainPairView`` and ``TokenObtainSlidingView`` views, create a
+subclass for the desired view as well as a subclass for its corresponding
+serializer.  Here's an example of how to customize the claims in tokens
+generated by the ``TokenObtainPairView``:
+
+.. code-block:: python
+
+  from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
+  from rest_framework_simplejwt.views import TokenObtainPairView
+
+  class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
+      @classmethod
+      def get_token(cls, user):
+          token = super(MyTokenObtainPairSerializer, cls).get_token(user)
+
+          # Add custom claims
+          token['name'] = user.name
+          # ...
+
+          return token
+
+  class MyTokenObtainPairView(TokenObtainPairView):
+      serializer_class = MyTokenObtainPairSerializer
+
+Note that the example above will cause the customized claims to be present in
+both refresh *and* access tokens which are generated by the view.  This follows
+from the fact that the ``get_token`` method above produces the *refresh* token
+for the view, which is in turn used to generate the view's access token.
+
+As with the standard token views, you'll also need to include a url route to
+your subclassed view.
+
+Token types
+-----------
+
+Simple JWT provides two different token types which can be used to prove
+authentication.  In a token's payload, its type can be identified by the value
+of its token type claim, which is "token_type" by default.  This may have a
+value of "access", "sliding", or "refresh" however refresh tokens are not
+considered valid for authentication at this time.  The claim name used to store
+the type can be customized by changing the ``TOKEN_TYPE_CLAIM`` setting.
+
+By default, Simple JWT expects an "access" token to prove authentication.  The
+allowed auth token types are determined by the value of the
+``AUTH_TOKEN_CLASSES`` setting.  This setting contains a list of dot paths to
+token classes.  It includes the
+``'rest_framework_simplejwt.tokens.AccessToken'`` dot path by default but may
+also include the ``'rest_framework_simplejwt.tokens.SlidingToken'`` dot path.
+Either or both of those dot paths may be present in the list of auth token
+classes.  If they are both present, then both of those token types may be used
+to prove authentication.
+
+Sliding tokens
+--------------
+
+Sliding tokens offer a more convenient experience to users of tokens with the
+trade-offs of being less secure and, in the case that the blacklist app is
+being used, less performant.  A sliding token is one which contains both an
+expiration claim and a refresh expiration claim.  As long as the timestamp in a
+sliding token's expiration claim has not passed, it can be used to prove
+authentication.  Additionally, as long as the timestamp in its refresh
+expiration claim has not passed, it may also be submitted to a refresh view to
+get another copy of itself with a renewed expiration claim.
+
+If you want to use sliding tokens, change the ``AUTH_TOKEN_CLASSES`` setting to
+``('rest_framework_simplejwt.tokens.SlidingToken',)``.  (Alternatively, the
+``AUTH_TOKEN_CLASSES`` setting may include dot paths to both the
+``AccessToken`` and ``SlidingToken`` token classes in the
+``rest_framework_simplejwt.tokens`` module if you want to allow both token
+types to be used for authentication.)
+
+Also, include urls for the sliding token specific ``TokenObtainSlidingView``
+and ``TokenRefreshSlidingView`` views along side or in place of urls for the
+access token specific ``TokenObtainPairView`` and ``TokenRefreshView`` views:
+
+.. code-block:: python
+
+  from rest_framework_simplejwt.views import (
+      TokenObtainSlidingView,
+      TokenRefreshSlidingView,
+  )
+
+  urlpatterns = [
+      ...
+      url(r'^api/token/$', TokenObtainSlidingView.as_view(), name='token_obtain'),
+      url(r'^api/token/refresh/$', TokenRefreshSlidingView.as_view(), name='token_refresh'),
+      ...
+  ]
+
+Be aware that, if you are using the blacklist app, Simple JWT will validate all
+sliding tokens against the blacklist for each authenticated request.  This will
+reduce the performance of authenticated API views.
+
+Blacklist app
+-------------
+
+Simple JWT includes an app that provides token blacklist functionality.  To use
+this app, include it in your list of installed apps in ``settings.py``:
+
+.. code-block:: python
+
+  # Django project settings.py
+
+  ...
+
+  INSTALLED_APPS = (
+      ...
+      'rest_framework_simplejwt.token_blacklist',
+      ...
+  }
+
+Also, make sure to run ``python manage.py migrate`` to run the app's
+migrations.
+
+If the blacklist app is detected in ``INSTALLED_APPS``, Simple JWT will add any
+generated refresh or sliding tokens to a list of outstanding tokens.  It will
+also check that any refresh or sliding token does not appear in a blacklist of
+tokens before it considers it as valid.
+
+The Simple JWT blacklist app implements its outstanding and blacklisted token
+lists using two model: ``OutstandingToken`` and ``BlacklistedToken``.  Model
+admins are defined for both of these models.  To add a token to the blacklist,
+find its corresponding ``OutstandingToken`` record in the admin and use the
+admin again to create a ``BlacklistedToken`` record that points to the
+``OutstandingToken`` record.
+
+Alternatively, you can blacklist a token by creating a ``BlacklistMixin``
+subclass instance and calling the instance's ``blacklist`` method:
+
+.. code-block:: python
+
+  from rest_framework_simplejwt.tokens import RefreshToken
+
+  token = RefreshToken(base64_encoded_token_string)
+  token.blacklist()
+
+This will create unique outstanding token and blacklist records for the token's
+"jti" claim.
+
+The blacklist app also provides a management command, ``flushexpiredtokens``,
+which will delete any tokens from the outstanding list and blacklist that have
+expired.  You should set up a cron job on your server or hosting platform which
+runs this command daily.
+
+Experimental features
+---------------------
+
+JWTTokenUserAuthentication backend
+  The ``JWTTokenUserAuthentication`` backend's ``authenticate`` method does not
+  perform a database lookup to obtain a user instance.  Instead, it returns a
+  ``rest_framework_simplejwt.models.TokenUser`` instance which acts as a
+  stateless user object backed only by a validated token instead of a record in
+  a database.  This can facilitate developing single sign-on functionality
+  between separately hosted Django apps which all share the same token secret
+  key.  To use this feature, add the
+  ``rest_framework_simplejwt.authentication.JWTTokenUserAuthentication``
+  backend (instead of the default ``JWTAuthentication`` backend) to the Django
+  REST Framework's ``DEFAULT_AUTHENTICATION_CLASSES`` config setting:
+
+  .. code-block:: python
+
+    REST_FRAMEWORK = {
+        ...
+        'DEFAULT_AUTHENTICATION_CLASSES': (
+            ...
+            'rest_framework_simplejwt.authentication.JWTTokenUserAuthentication',
+        )
+        ...
+    }
+
+Running the Tests
+-----------------
+
+To run the tests, first `install pyenv
+<https://github.com/pyenv/pyenv#installation>`__.  Next, install the relevant
+Python minor versions and create a ``.python-version`` file in your cloned
+repo's directory:
+
+.. code-block:: bash
+
+  pyenv install 3.6.x
+  pyenv install 3.5.x
+  pyenv install 3.4.x
+  pyenv install 2.7.x
+  cd <repo directory>
+  cat > .python-version <<EOF
+  3.6.x
+  3.5.x
+  3.4.x
+  2.7.x
+  EOF
+
+Above, the ``x`` in each case should be replaced with the latest corresponding
+patch version.  The ``.python-version`` file will tell pyenv and tox that
+you're testing against multiple versions of Python.  Next, install and run tox:
+
+.. code-block:: bash
+
+  pip install tox
+  tox
+
+Alternatively, if you'd like to avoid using tox, you can set up your
+environment and run the tests manually:
+
+.. code-block:: bash
+
+  pip install -r requirements.txt
+  ./setup.py develop
+  ./runtests.py
+
+Acknowledgements
+----------------
+
+This project borrows code from the `Django REST Framework
+<https://github.com/encode/django-rest-framework/>`__ as well as concepts from
+the implementation of another JSON web token library for the Django REST
+Framework, `django-rest-framework-jwt
+<https://github.com/GetBlimp/django-rest-framework-jwt>`__.  The licenses from
+both of those projects have been included in this repository in the "licenses"
+directory.

+ 1 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/__init__.py

@@ -0,0 +1 @@
+__version__ = '3.3'

+ 131 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/authentication.py

@@ -0,0 +1,131 @@
+from __future__ import unicode_literals
+
+from django.utils.six import text_type
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import HTTP_HEADER_ENCODING, authentication
+
+from .exceptions import AuthenticationFailed, InvalidToken, TokenError
+from .models import TokenUser
+from .settings import api_settings
+from .state import User
+
+AUTH_HEADER_TYPES = api_settings.AUTH_HEADER_TYPES
+
+if not isinstance(api_settings.AUTH_HEADER_TYPES, (list, tuple)):
+    AUTH_HEADER_TYPES = (AUTH_HEADER_TYPES,)
+
+AUTH_HEADER_TYPE_BYTES = set(
+    h.encode(HTTP_HEADER_ENCODING)
+    for h in AUTH_HEADER_TYPES
+)
+
+
+class JWTAuthentication(authentication.BaseAuthentication):
+    """
+    An authentication plugin that authenticates requests through a JSON web
+    token provided in a request header.
+    """
+    www_authenticate_realm = 'api'
+
+    def authenticate(self, request):
+        header = self.get_header(request)
+        if header is None:
+            return None
+
+        raw_token = self.get_raw_token(header)
+        if raw_token is None:
+            return None
+
+        validated_token = self.get_validated_token(raw_token)
+
+        return self.get_user(validated_token), None
+
+    def authenticate_header(self, request):
+        return '{0} realm="{1}"'.format(
+            AUTH_HEADER_TYPES[0],
+            self.www_authenticate_realm,
+        )
+
+    def get_header(self, request):
+        """
+        Extracts the header containing the JSON web token from the given
+        request.
+        """
+        header = request.META.get('HTTP_AUTHORIZATION')
+
+        if isinstance(header, text_type):
+            # Work around django test client oddness
+            header = header.encode(HTTP_HEADER_ENCODING)
+
+        return header
+
+    def get_raw_token(self, header):
+        """
+        Extracts an unvalidated JSON web token from the given "Authorization"
+        header value.
+        """
+        parts = header.split()
+
+        if parts[0] not in AUTH_HEADER_TYPE_BYTES:
+            # Assume the header does not contain a JSON web token
+            return None
+
+        if len(parts) != 2:
+            raise AuthenticationFailed(
+                _('Authorization header must contain two space-delimited values'),
+                code='bad_authorization_header',
+            )
+
+        return parts[1]
+
+    def get_validated_token(self, raw_token):
+        """
+        Validates an encoded JSON web token and returns a validated token
+        wrapper object.
+        """
+        messages = []
+        for AuthToken in api_settings.AUTH_TOKEN_CLASSES:
+            try:
+                return AuthToken(raw_token)
+            except TokenError as e:
+                messages.append({'token_class': AuthToken.__name__,
+                                 'token_type': AuthToken.token_type,
+                                 'message': e.args[0]})
+
+        raise InvalidToken({
+            'detail': _('Given token not valid for any token type'),
+            'messages': messages,
+        })
+
+    def get_user(self, validated_token):
+        """
+        Attempts to find and return a user using the given validated token.
+        """
+        try:
+            user_id = validated_token[api_settings.USER_ID_CLAIM]
+        except KeyError:
+            raise InvalidToken(_('Token contained no recognizable user identification'))
+
+        try:
+            user = User.objects.get(**{api_settings.USER_ID_FIELD: user_id})
+        except User.DoesNotExist:
+            raise AuthenticationFailed(_('User not found'), code='user_not_found')
+
+        if not user.is_active:
+            raise AuthenticationFailed(_('User is inactive'), code='user_inactive')
+
+        return user
+
+
+class JWTTokenUserAuthentication(JWTAuthentication):
+    def get_user(self, validated_token):
+        """
+        Returns a stateless user object which is backed by the given validated
+        token.
+        """
+        if api_settings.USER_ID_CLAIM not in validated_token:
+            # The TokenUser class assumes tokens will have a recognizable user
+            # identifier claim.
+            raise InvalidToken(_('Token contained no recognizable user identification'))
+
+        return TokenUser(validated_token)

+ 50 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/backends.py

@@ -0,0 +1,50 @@
+from __future__ import unicode_literals
+
+from django.utils.translation import ugettext_lazy as _
+from jwt import InvalidTokenError
+import jwt
+
+from .exceptions import TokenBackendError
+from .utils import format_lazy
+
+ALLOWED_ALGORITHMS = (
+    'HS256',
+    'HS384',
+    'HS512',
+    'RS256',
+    'RS384',
+    'RS512',
+)
+
+
+class TokenBackend(object):
+    def __init__(self, algorithm, signing_key=None, verifying_key=None):
+        if algorithm not in ALLOWED_ALGORITHMS:
+            raise TokenBackendError(format_lazy(_("Unrecognized algorithm type '{}'"), algorithm))
+
+        self.algorithm = algorithm
+        self.signing_key = signing_key
+        if algorithm.startswith('HS'):
+            self.verifying_key = signing_key
+        else:
+            self.verifying_key = verifying_key
+
+    def encode(self, payload):
+        """
+        Returns an encoded token for the given payload dictionary.
+        """
+        token = jwt.encode(payload, self.signing_key, algorithm=self.algorithm)
+        return token.decode('utf-8')
+
+    def decode(self, token, verify=True):
+        """
+        Performs a validation of the given token and returns its payload
+        dictionary.
+
+        Raises a `TokenBackendError` if the token is malformed, if its
+        signature check fails, or if its 'exp' claim indicates it has expired.
+        """
+        try:
+            return jwt.decode(token, self.verifying_key, algorithms=[self.algorithm], verify=verify)
+        except InvalidTokenError:
+            raise TokenBackendError(_('Token is invalid or expired'))

+ 55 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/compat.py

@@ -0,0 +1,55 @@
+from __future__ import unicode_literals
+
+import warnings
+
+try:
+    from django.urls import reverse, reverse_lazy
+except ImportError:
+    from django.core.urlresolvers import reverse, reverse_lazy  # NOQA
+
+
+class RemovedInDjango20Warning(DeprecationWarning):
+    pass
+
+
+class CallableBool:  # pragma: no cover
+    """
+    An boolean-like object that is also callable for backwards compatibility.
+    """
+    do_not_call_in_templates = True
+
+    def __init__(self, value):
+        self.value = value
+
+    def __bool__(self):
+        return self.value
+
+    def __call__(self):
+        warnings.warn(
+            "Using user.is_authenticated() and user.is_anonymous() as a method "
+            "is deprecated. Remove the parentheses to use it as an attribute.",
+            RemovedInDjango20Warning, stacklevel=2
+        )
+        return self.value
+
+    def __nonzero__(self):  # Python 2 compatibility
+        return self.value
+
+    def __repr__(self):
+        return 'CallableBool(%r)' % self.value
+
+    def __eq__(self, other):
+        return self.value == other
+
+    def __ne__(self, other):
+        return self.value != other
+
+    def __or__(self, other):
+        return bool(self.value or other)
+
+    def __hash__(self):
+        return hash(self.value)
+
+
+CallableFalse = CallableBool(False)
+CallableTrue = CallableBool(True)

+ 41 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/exceptions.py

@@ -0,0 +1,41 @@
+from __future__ import unicode_literals
+
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import exceptions, status
+
+
+class TokenError(Exception):
+    pass
+
+
+class TokenBackendError(Exception):
+    pass
+
+
+class DetailDictMixin(object):
+    def __init__(self, detail=None, code=None):
+        """
+        Builds a detail dictionary for the error to give more information to API
+        users.
+        """
+        detail_dict = {'detail': self.default_detail, 'code': self.default_code}
+
+        if isinstance(detail, dict):
+            detail_dict.update(detail)
+        elif detail is not None:
+            detail_dict['detail'] = detail
+
+        if code is not None:
+            detail_dict['code'] = code
+
+        super(DetailDictMixin, self).__init__(detail_dict)
+
+
+class AuthenticationFailed(DetailDictMixin, exceptions.AuthenticationFailed):
+    pass
+
+
+class InvalidToken(AuthenticationFailed):
+    status_code = status.HTTP_401_UNAUTHORIZED
+    default_detail = _('Token is invalid or expired')
+    default_code = 'token_not_valid'

+ 106 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/models.py

@@ -0,0 +1,106 @@
+from __future__ import unicode_literals
+
+from django.contrib.auth import models as auth_models
+from django.db.models.manager import EmptyManager
+from django.utils.encoding import python_2_unicode_compatible
+from django.utils.functional import cached_property
+
+from .compat import CallableFalse, CallableTrue
+from .settings import api_settings
+
+
+@python_2_unicode_compatible
+class TokenUser(object):
+    """
+    A dummy user class modeled after django.contrib.auth.models.AnonymousUser.
+    Used in conjunction with the `JWTTokenUserAuthentication` backend to
+    implement single sign-on functionality across services which share the same
+    secret key.  `JWTTokenUserAuthentication` will return an instance of this
+    class instead of a `User` model instance.  Instances of this class act as
+    stateless user objects which are backed by validated tokens.
+    """
+    username = ''
+
+    # User is always active since Simple JWT will never issue a token for an
+    # inactive user
+    is_active = True
+
+    _groups = EmptyManager(auth_models.Group)
+    _user_permissions = EmptyManager(auth_models.Permission)
+
+    def __init__(self, token):
+        self.token = token
+
+    def __str__(self):
+        return 'TokenUser {}'.format(self.id)
+
+    @cached_property
+    def id(self):
+        return self.token[api_settings.USER_ID_CLAIM]
+
+    @cached_property
+    def pk(self):
+        return self.id
+
+    @cached_property
+    def is_staff(self):
+        return self.token.get('is_staff', False)
+
+    @cached_property
+    def is_superuser(self):
+        return self.token.get('is_superuser', False)
+
+    def __eq__(self, other):
+        return self.id == other.id
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __hash__(self):
+        return hash(self.id)
+
+    def save(self):
+        raise NotImplementedError('Token users have no DB representation')
+
+    def delete(self):
+        raise NotImplementedError('Token users have no DB representation')
+
+    def set_password(self, raw_password):
+        raise NotImplementedError('Token users have no DB representation')
+
+    def check_password(self, raw_password):
+        raise NotImplementedError('Token users have no DB representation')
+
+    @property
+    def groups(self):
+        return self._groups
+
+    @property
+    def user_permissions(self):
+        return self._user_permissions
+
+    def get_group_permissions(self, obj=None):
+        return set()
+
+    def get_all_permissions(self, obj=None):
+        return set()
+
+    def has_perm(self, perm, obj=None):
+        return False
+
+    def has_perms(self, perm_list, obj=None):
+        return False
+
+    def has_module_perms(self, module):
+        return False
+
+    @property
+    def is_anonymous(self):
+        return CallableFalse
+
+    @property
+    def is_authenticated(self):
+        return CallableTrue
+
+    def get_username(self):
+        return self.username

+ 136 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/serializers.py

@@ -0,0 +1,136 @@
+from __future__ import unicode_literals
+
+from django.contrib.auth import authenticate
+from django.utils.six import text_type
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+from .settings import api_settings
+from .state import User
+from .tokens import RefreshToken, SlidingToken, UntypedToken
+
+
+class PasswordField(serializers.CharField):
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('style', {})
+
+        kwargs['style']['input_type'] = 'password'
+        kwargs['write_only'] = True
+
+        super(PasswordField, self).__init__(*args, **kwargs)
+
+
+class TokenObtainSerializer(serializers.Serializer):
+    username_field = User.USERNAME_FIELD
+
+    def __init__(self, *args, **kwargs):
+        super(TokenObtainSerializer, self).__init__(*args, **kwargs)
+
+        self.fields[self.username_field] = serializers.CharField()
+        self.fields['password'] = PasswordField()
+
+    def validate(self, attrs):
+        self.user = authenticate(**{
+            self.username_field: attrs[self.username_field],
+            'password': attrs['password'],
+        })
+
+        # Prior to Django 1.10, inactive users could be authenticated with the
+        # default `ModelBackend`.  As of Django 1.10, the `ModelBackend`
+        # prevents inactive users from authenticating.  App designers can still
+        # allow inactive users to authenticate by opting for the new
+        # `AllowAllUsersModelBackend`.  However, we explicitly prevent inactive
+        # users from authenticating to enforce a reasonable policy and provide
+        # sensible backwards compatibility with older Django versions.
+        if self.user is None or not self.user.is_active:
+            raise serializers.ValidationError(
+                _('No active account found with the given credentials'),
+            )
+
+        return {}
+
+    @classmethod
+    def get_token(cls, user):
+        raise NotImplemented('Must implement `get_token` method for `TokenObtainSerializer` subclasses')
+
+
+class TokenObtainPairSerializer(TokenObtainSerializer):
+    @classmethod
+    def get_token(cls, user):
+        return RefreshToken.for_user(user)
+
+    def validate(self, attrs):
+        data = super(TokenObtainPairSerializer, self).validate(attrs)
+
+        refresh = self.get_token(self.user)
+
+        data['refresh'] = text_type(refresh)
+        data['access'] = text_type(refresh.access_token)
+
+        return data
+
+
+class TokenObtainSlidingSerializer(TokenObtainSerializer):
+    @classmethod
+    def get_token(cls, user):
+        return SlidingToken.for_user(user)
+
+    def validate(self, attrs):
+        data = super(TokenObtainSlidingSerializer, self).validate(attrs)
+
+        token = self.get_token(self.user)
+
+        data['token'] = text_type(token)
+
+        return data
+
+
+class TokenRefreshSerializer(serializers.Serializer):
+    refresh = serializers.CharField()
+
+    def validate(self, attrs):
+        refresh = RefreshToken(attrs['refresh'])
+
+        data = {'access': text_type(refresh.access_token)}
+
+        if api_settings.ROTATE_REFRESH_TOKENS:
+            if api_settings.BLACKLIST_AFTER_ROTATION:
+                try:
+                    # Attempt to blacklist the given refresh token
+                    refresh.blacklist()
+                except AttributeError:
+                    # If blacklist app not installed, `blacklist` method will
+                    # not be present
+                    pass
+
+            refresh.set_jti()
+            refresh.set_exp()
+
+            data['refresh'] = text_type(refresh)
+
+        return data
+
+
+class TokenRefreshSlidingSerializer(serializers.Serializer):
+    token = serializers.CharField()
+
+    def validate(self, attrs):
+        token = SlidingToken(attrs['token'])
+
+        # Check that the timestamp in the "refresh_exp" claim has not
+        # passed
+        token.check_exp(api_settings.SLIDING_TOKEN_REFRESH_EXP_CLAIM)
+
+        # Update the "exp" claim
+        token.set_exp()
+
+        return {'token': text_type(token)}
+
+
+class TokenVerifySerializer(serializers.Serializer):
+    token = serializers.CharField()
+
+    def validate(self, attrs):
+        UntypedToken(attrs['token'])
+
+        return {}

+ 72 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/settings.py

@@ -0,0 +1,72 @@
+from __future__ import unicode_literals
+
+from datetime import timedelta
+
+from django.conf import settings
+from django.test.signals import setting_changed
+from django.utils.translation import ugettext_lazy as _
+from rest_framework.settings import APISettings as _APISettings
+
+from .utils import format_lazy
+
+USER_SETTINGS = getattr(settings, 'SIMPLE_JWT', None)
+
+DEFAULTS = {
+    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
+    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
+    'ROTATE_REFRESH_TOKENS': False,
+    'BLACKLIST_AFTER_ROTATION': True,
+
+    'ALGORITHM': 'HS256',
+    'SIGNING_KEY': settings.SECRET_KEY,
+    'VERIFYING_KEY': None,
+
+    'AUTH_HEADER_TYPES': ('Bearer',),
+    'USER_ID_FIELD': 'id',
+    'USER_ID_CLAIM': 'user_id',
+
+    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
+    'TOKEN_TYPE_CLAIM': 'token_type',
+
+    'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
+    'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
+    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
+}
+
+IMPORT_STRINGS = (
+    'AUTH_TOKEN_CLASSES',
+)
+
+REMOVED_SETTINGS = (
+    'AUTH_HEADER_TYPE',
+    'AUTH_TOKEN_CLASS',
+    'SECRET_KEY',
+    'TOKEN_BACKEND_CLASS',
+)
+
+
+class APISettings(_APISettings):  # pragma: no cover
+    def __check_user_settings(self, user_settings):
+        SETTINGS_DOC = 'https://github.com/davesque/django-rest-framework-simplejwt#settings'
+
+        for setting in REMOVED_SETTINGS:
+            if setting in user_settings:
+                raise RuntimeError(format_lazy(
+                    _("The '{}' setting has been removed. Please refer to '{}' for available settings."),
+                    setting, SETTINGS_DOC,
+                ))
+
+        return user_settings
+
+api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
+
+
+def reload_api_settings(*args, **kwargs):  # pragma: no cover
+    global api_settings
+
+    setting, value = kwargs['setting'], kwargs['value']
+
+    if setting == 'SIMPLE_JWT':
+        api_settings = APISettings(value, DEFAULTS, IMPORT_STRINGS)
+
+setting_changed.connect(reload_api_settings)

+ 10 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/state.py

@@ -0,0 +1,10 @@
+from __future__ import unicode_literals
+
+from django.contrib.auth import get_user_model
+
+from .backends import TokenBackend
+from .settings import api_settings
+
+User = get_user_model()
+token_backend = TokenBackend(api_settings.ALGORITHM, api_settings.SIGNING_KEY,
+                             api_settings.VERIFYING_KEY)

+ 1 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/__init__.py

@@ -0,0 +1 @@
+default_app_config = 'rest_framework_simplejwt.token_blacklist.apps.TokenBlacklistConfig'

+ 91 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/admin.py

@@ -0,0 +1,91 @@
+from __future__ import unicode_literals
+
+from django.contrib import admin
+from django.utils.translation import ugettext_lazy as _
+
+from .models import BlacklistedToken, OutstandingToken
+
+
+class OutstandingTokenAdmin(admin.ModelAdmin):
+    list_display = (
+        'jti',
+        'user',
+        'created_at',
+        'expires_at',
+    )
+    search_fields = (
+        'user__id',
+        'jti',
+    )
+    ordering = (
+        'user',
+    )
+
+    def get_queryset(self, *args, **kwargs):
+        qs = super(OutstandingTokenAdmin, self).get_queryset(*args, **kwargs)
+
+        return qs.select_related('user')
+
+    # Read-only behavior defined below
+    actions = None
+
+    def get_readonly_fields(self, *args, **kwargs):
+        return [f.name for f in self.model._meta.fields]
+
+    def has_add_permission(self, *args, **kwargs):
+        return False
+
+    def has_delete_permission(self, *args, **kwargs):
+        return False
+
+    def has_change_permission(self, request, obj=None):
+        return (
+            request.method in ['GET', 'HEAD'] and
+            super(OutstandingTokenAdmin, self).has_change_permission(request, obj)
+        )
+
+admin.site.register(OutstandingToken, OutstandingTokenAdmin)
+
+
+class BlacklistedTokenAdmin(admin.ModelAdmin):
+    list_display = (
+        'token_jti',
+        'token_user',
+        'token_created_at',
+        'token_expires_at',
+        'blacklisted_at',
+    )
+    search_fields = (
+        'token__user__id',
+        'token__jti',
+    )
+    ordering = (
+        'token__user',
+    )
+
+    def get_queryset(self, *args, **kwargs):
+        qs = super(BlacklistedTokenAdmin, self).get_queryset(*args, **kwargs)
+
+        return qs.select_related('token__user')
+
+    def token_jti(self, obj):
+        return obj.token.jti
+    token_jti.short_description = _('jti')
+    token_jti.admin_order_field = 'token__jti'
+
+    def token_user(self, obj):
+        return obj.token.user
+    token_user.short_description = _('user')
+    token_user.admin_order_field = 'token__user'
+
+    def token_created_at(self, obj):
+        return obj.token.created_at
+    token_created_at.short_description = _('created at')
+    token_created_at.admin_order_field = 'token__created_at'
+
+    def token_expires_at(self, obj):
+        return obj.token.expires_at
+    token_expires_at.short_description = _('expires at')
+    token_expires_at.admin_order_field = 'token__expires_at'
+
+admin.site.register(BlacklistedToken, BlacklistedTokenAdmin)

+ 9 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/apps.py

@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+
+from django.apps import AppConfig
+from django.utils.translation import ugettext_lazy as _
+
+
+class TokenBlacklistConfig(AppConfig):
+    name = 'rest_framework_simplejwt.token_blacklist'
+    verbose_name = _('Token Blacklist')

+ 0 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/management/__init__.py


+ 0 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/management/commands/__init__.py


+ 11 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/management/commands/flushexpiredtokens.py

@@ -0,0 +1,11 @@
+from django.core.management.base import BaseCommand
+from rest_framework_simplejwt.utils import aware_utcnow
+
+from ...models import OutstandingToken
+
+
+class Command(BaseCommand):
+    help = 'Flushes any expired tokens in the outstanding token list'
+
+    def handle(self, *args, **kwargs):
+        OutstandingToken.objects.filter(expires_at__lte=aware_utcnow()).delete()

+ 45 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0001_initial.py

@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-06-26 01:12
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='BlacklistedToken',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('blacklisted_at', models.DateTimeField(auto_now_add=True)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='OutstandingToken',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('jti', models.UUIDField(unique=True)),
+                ('token', models.TextField()),
+                ('created_at', models.DateTimeField()),
+                ('expires_at', models.DateTimeField()),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ('user',),
+            },
+        ),
+        migrations.AddField(
+            model_name='blacklistedtoken',
+            name='token',
+            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='token_blacklist.OutstandingToken'),
+        ),
+    ]

+ 20 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0002_outstandingtoken_jti_hex.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-10-17 20:06
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('token_blacklist', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='outstandingtoken',
+            name='jti_hex',
+            field=models.CharField(blank=True, null=True, max_length=255),
+        ),
+    ]

+ 34 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0003_auto_20171017_2007.py

@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-10-17 20:07
+from __future__ import unicode_literals
+
+from uuid import UUID
+
+from django.db import migrations
+
+
+def populate_jti_hex(apps, schema_editor):
+    OutstandingToken = apps.get_model('token_blacklist', 'OutstandingToken')
+
+    for token in OutstandingToken.objects.all():
+        token.jti_hex = token.jti.hex
+        token.save()
+
+
+def reverse_populate_jti_hex(apps, schema_editor):  # pragma: no cover
+    OutstandingToken = apps.get_model('token_blacklist', 'OutstandingToken')
+
+    for token in OutstandingToken.objects.all():
+        token.jti = UUID(hex=token.jti_hex)
+        token.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('token_blacklist', '0002_outstandingtoken_jti_hex'),
+    ]
+
+    operations = [
+        migrations.RunPython(populate_jti_hex, reverse_populate_jti_hex),
+    ]

+ 20 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0004_auto_20171017_2013.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-10-17 20:13
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('token_blacklist', '0003_auto_20171017_2007'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='outstandingtoken',
+            name='jti_hex',
+            field=models.CharField(unique=True, max_length=255),
+        ),
+    ]

+ 19 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0005_remove_outstandingtoken_jti.py

@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-10-17 21:13
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('token_blacklist', '0004_auto_20171017_2013'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='outstandingtoken',
+            name='jti',
+        ),
+    ]

+ 20 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0006_auto_20171017_2113.py

@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-10-17 21:13
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('token_blacklist', '0005_remove_outstandingtoken_jti'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='outstandingtoken',
+            old_name='jti_hex',
+            new_name='jti',
+        ),
+    ]

+ 27 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/0007_auto_20171017_2214.py

@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.2 on 2017-10-17 22:14
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('token_blacklist', '0006_auto_20171017_2113'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='outstandingtoken',
+            name='created_at',
+            field=models.DateTimeField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name='outstandingtoken',
+            name='user',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+        ),
+    ]

+ 0 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/migrations/__init__.py


+ 49 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/token_blacklist/models.py

@@ -0,0 +1,49 @@
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import models
+from django.utils.six import python_2_unicode_compatible
+
+
+@python_2_unicode_compatible
+class OutstandingToken(models.Model):
+    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True)
+
+    jti = models.CharField(unique=True, max_length=255)
+    token = models.TextField()
+
+    created_at = models.DateTimeField(null=True, blank=True)
+    expires_at = models.DateTimeField()
+
+    class Meta:
+        # Work around for a bug in Django:
+        # https://code.djangoproject.com/ticket/19422
+        #
+        # Also see corresponding ticket:
+        # https://github.com/encode/django-rest-framework/issues/705
+        abstract = 'rest_framework_simplejwt.token_blacklist' not in settings.INSTALLED_APPS
+        ordering = ('user',)
+
+    def __str__(self):
+        return 'Token for {} ({})'.format(
+            self.user,
+            self.jti,
+        )
+
+
+@python_2_unicode_compatible
+class BlacklistedToken(models.Model):
+    token = models.OneToOneField(OutstandingToken, on_delete=models.CASCADE)
+
+    blacklisted_at = models.DateTimeField(auto_now_add=True)
+
+    class Meta:
+        # Work around for a bug in Django:
+        # https://code.djangoproject.com/ticket/19422
+        #
+        # Also see corresponding ticket:
+        # https://github.com/encode/django-rest-framework/issues/705
+        abstract = 'rest_framework_simplejwt.token_blacklist' not in settings.INSTALLED_APPS
+
+    def __str__(self):
+        return 'Blacklisted token for {}'.format(self.token.user)

+ 298 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/tokens.py

@@ -0,0 +1,298 @@
+from __future__ import unicode_literals
+
+from datetime import timedelta
+from uuid import uuid4
+
+from django.conf import settings
+from django.utils.six import python_2_unicode_compatible, text_type
+from django.utils.translation import ugettext_lazy as _
+
+from .exceptions import TokenBackendError, TokenError
+from .settings import api_settings
+from .token_blacklist.models import BlacklistedToken, OutstandingToken
+from .utils import (
+    aware_utcnow, datetime_from_epoch, datetime_to_epoch, format_lazy
+)
+
+
+@python_2_unicode_compatible
+class Token(object):
+    """
+    A class which validates and wraps an existing JWT or can be used to build a
+    new JWT.
+    """
+    token_type = None
+    lifetime = None
+
+    def __init__(self, token=None, verify=True):
+        """
+        !!!! IMPORTANT !!!! MUST raise a TokenError with a user-facing error
+        message if the given token is invalid, expired, or otherwise not safe
+        to use.
+        """
+        if self.token_type is None or self.lifetime is None:
+            raise TokenError(_('Cannot create token with no type or lifetime'))
+
+        self.token = token
+        self.current_time = aware_utcnow()
+
+        # Set up token
+        if token is not None:
+            # An encoded token was provided
+            from .state import token_backend
+
+            # Decode token
+            try:
+                self.payload = token_backend.decode(token, verify=verify)
+            except TokenBackendError:
+                raise TokenError(_('Token is invalid or expired'))
+
+            if verify:
+                self.verify()
+        else:
+            # New token.  Skip all the verification steps.
+            self.payload = {api_settings.TOKEN_TYPE_CLAIM: self.token_type}
+
+            # Set "exp" claim with default value
+            self.set_exp(from_time=self.current_time, lifetime=self.lifetime)
+
+            # Set "jti" claim
+            self.set_jti()
+
+    def __repr__(self):
+        return repr(self.payload)
+
+    def __getitem__(self, key):
+        return self.payload[key]
+
+    def __setitem__(self, key, value):
+        self.payload[key] = value
+
+    def __delitem__(self, key):
+        del self.payload[key]
+
+    def __contains__(self, key):
+        return key in self.payload
+
+    def get(self, key, default=None):
+        return self.payload.get(key, default)
+
+    def __str__(self):
+        """
+        Signs and returns a token as a base64 encoded string.
+        """
+        from .state import token_backend
+
+        return token_backend.encode(self.payload)
+
+    def verify(self):
+        """
+        Performs additional validation steps which were not performed when this
+        token was decoded.  This method is part of the "public" API to indicate
+        the intention that it may be overridden in subclasses.
+        """
+        # According to RFC 7519, the "exp" claim is OPTIONAL
+        # (https://tools.ietf.org/html/rfc7519#section-4.1.4).  As a more
+        # correct behavior for authorization tokens, we require an "exp"
+        # claim.  We don't want any zombie tokens walking around.
+        self.check_exp()
+
+        # Ensure token id is present
+        if 'jti' not in self.payload:
+            raise TokenError(_('Token has no id'))
+
+        self.verify_token_type()
+
+    def verify_token_type(self):
+        """
+        Ensures that the token type claim is present and has the correct value.
+        """
+        try:
+            token_type = self.payload[api_settings.TOKEN_TYPE_CLAIM]
+        except KeyError:
+            raise TokenError(_('Token has no type'))
+
+        if self.token_type != token_type:
+            raise TokenError(_('Token has wrong type'))
+
+    def set_jti(self):
+        """
+        Populates the "jti" claim of a token with a string where there is a
+        negligible probability that the same string will be chosen at a
+        later time.
+
+        See here:
+        https://tools.ietf.org/html/rfc7519#section-4.1.7
+        """
+        self.payload['jti'] = uuid4().hex
+
+    def set_exp(self, claim='exp', from_time=None, lifetime=None):
+        """
+        Updates the expiration time of a token.
+        """
+        if from_time is None:
+            from_time = self.current_time
+
+        if lifetime is None:
+            lifetime = self.lifetime
+
+        self.payload[claim] = datetime_to_epoch(from_time + lifetime)
+
+    def check_exp(self, claim='exp', current_time=None):
+        """
+        Checks whether a timestamp value in the given claim has passed (since
+        the given datetime value in `current_time`).  Raises a TokenError with
+        a user-facing error message if so.
+        """
+        if current_time is None:
+            current_time = self.current_time
+
+        try:
+            claim_value = self.payload[claim]
+        except KeyError:
+            raise TokenError(format_lazy(_("Token has no '{}' claim"), claim))
+
+        claim_time = datetime_from_epoch(claim_value)
+        if claim_time <= current_time:
+            raise TokenError(format_lazy(_("Token '{}' claim has expired"), claim))
+
+    @classmethod
+    def for_user(cls, user):
+        """
+        Returns an authorization token for the given user that will be provided
+        after authenticating the user's credentials.
+        """
+        user_id = getattr(user, api_settings.USER_ID_FIELD)
+        if not isinstance(user_id, int):
+            user_id = text_type(user_id)
+
+        token = cls()
+        token[api_settings.USER_ID_CLAIM] = user_id
+
+        return token
+
+
+class BlacklistMixin(object):
+    """
+    If the `rest_framework_simplejwt.token_blacklist` app was configured to be
+    used, tokens created from `BlacklistMixin` subclasses will insert
+    themselves into an outstanding token list and also check for their
+    membership in a token blacklist.
+    """
+    if 'rest_framework_simplejwt.token_blacklist' in settings.INSTALLED_APPS:
+        def verify(self, *args, **kwargs):
+            self.check_blacklist()
+
+            super(BlacklistMixin, self).verify(*args, **kwargs)
+
+        def check_blacklist(self):
+            """
+            Checks if this token is present in the token blacklist.  Raises
+            `TokenError` if so.
+            """
+            jti = self.payload['jti']
+
+            if BlacklistedToken.objects.filter(token__jti=jti).exists():
+                raise TokenError(_('Token is blacklisted'))
+
+        def blacklist(self):
+            """
+            Ensures this token is included in the outstanding token list and
+            adds it to the blacklist.
+            """
+            jti = self.payload['jti']
+            exp = self.payload['exp']
+
+            # Ensure outstanding token exists with given jti
+            token, _ = OutstandingToken.objects.get_or_create(
+                jti=jti,
+                defaults={
+                    'token': str(self),
+                    'expires_at': datetime_from_epoch(exp),
+                },
+            )
+
+            return BlacklistedToken.objects.get_or_create(token=token)
+
+        @classmethod
+        def for_user(cls, user):
+            """
+            Adds this token to the outstanding token list.
+            """
+            token = super(BlacklistMixin, cls).for_user(user)
+
+            jti = token['jti']
+            exp = token['exp']
+
+            OutstandingToken.objects.create(
+                user=user,
+                jti=jti,
+                token=str(token),
+                created_at=token.current_time,
+                expires_at=datetime_from_epoch(exp),
+            )
+
+            return token
+
+
+class SlidingToken(BlacklistMixin, Token):
+    token_type = 'sliding'
+    lifetime = api_settings.SLIDING_TOKEN_LIFETIME
+
+    def __init__(self, *args, **kwargs):
+        super(SlidingToken, self).__init__(*args, **kwargs)
+
+        if self.token is None:
+            # Set sliding refresh expiration claim if new token
+            self.set_exp(
+                api_settings.SLIDING_TOKEN_REFRESH_EXP_CLAIM,
+                from_time=self.current_time,
+                lifetime=api_settings.SLIDING_TOKEN_REFRESH_LIFETIME,
+            )
+
+
+class RefreshToken(BlacklistMixin, Token):
+    token_type = 'refresh'
+    lifetime = api_settings.REFRESH_TOKEN_LIFETIME
+    no_copy_claims = (api_settings.TOKEN_TYPE_CLAIM, 'exp', 'jti')
+
+    @property
+    def access_token(self):
+        """
+        Returns an access token created from this refresh token.  Copies all
+        claims present in this refresh token to the new access token except
+        those claims listed in the `no_copy_claims` attribute.
+        """
+        access = AccessToken()
+
+        # Use instantiation time of refresh token as relative timestamp for
+        # access token "exp" claim.  This ensures that both a refresh and
+        # access token expire relative to the same time if they are created as
+        # a pair.
+        access.set_exp(from_time=self.current_time)
+
+        no_copy = self.no_copy_claims
+        for claim, value in self.payload.items():
+            if claim in no_copy:
+                continue
+            access[claim] = value
+
+        return access
+
+
+class AccessToken(Token):
+    token_type = 'access'
+    lifetime = api_settings.ACCESS_TOKEN_LIFETIME
+
+
+class UntypedToken(Token):
+    token_type = 'untyped'
+    lifetime = timedelta(seconds=0)
+
+    def verify_token_type(self):
+        """
+        Untyped tokens do not verify the "token_type" claim.  This is useful
+        when performing general validation of a token's signature and other
+        properties which do not relate to the token's intended use.
+        """
+        pass

+ 34 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/utils.py

@@ -0,0 +1,34 @@
+from __future__ import unicode_literals
+
+from calendar import timegm
+from datetime import datetime
+
+from django.conf import settings
+from django.utils import six
+from django.utils.functional import lazy
+from django.utils.timezone import is_naive, make_aware, utc
+
+
+def make_utc(dt):
+    if settings.USE_TZ and is_naive(dt):
+        return make_aware(dt, timezone=utc)
+
+    return dt
+
+
+def aware_utcnow():
+    return make_utc(datetime.utcnow())
+
+
+def datetime_to_epoch(dt):
+    return timegm(dt.utctimetuple())
+
+
+def datetime_from_epoch(ts):
+    return make_utc(datetime.utcfromtimestamp(ts))
+
+
+def format_lazy(s, *args, **kwargs):
+    return s.format(*args, **kwargs)
+
+format_lazy = lazy(format_lazy, six.text_type)

+ 83 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/rest_framework_simplejwt/views.py

@@ -0,0 +1,83 @@
+from __future__ import unicode_literals
+
+from rest_framework import generics, status
+from rest_framework.response import Response
+
+from . import serializers
+from .authentication import AUTH_HEADER_TYPES
+from .exceptions import InvalidToken, TokenError
+
+
+class TokenViewBase(generics.GenericAPIView):
+    permission_classes = ()
+    authentication_classes = ()
+
+    serializer_class = None
+
+    www_authenticate_realm = 'api'
+
+    def get_authenticate_header(self, request):
+        return '{0} realm="{1}"'.format(
+            AUTH_HEADER_TYPES[0],
+            self.www_authenticate_realm,
+        )
+
+    def post(self, request, *args, **kwargs):
+        serializer = self.get_serializer(data=request.data)
+
+        try:
+            serializer.is_valid(raise_exception=True)
+        except TokenError as e:
+            raise InvalidToken(e.args[0])
+
+        return Response(serializer.validated_data, status=status.HTTP_200_OK)
+
+
+class TokenObtainPairView(TokenViewBase):
+    """
+    Takes a set of user credentials and returns an access and refresh JSON web
+    token pair to prove the authentication of those credentials.
+    """
+    serializer_class = serializers.TokenObtainPairSerializer
+
+token_obtain_pair = TokenObtainPairView.as_view()
+
+
+class TokenRefreshView(TokenViewBase):
+    """
+    Takes a refresh type JSON web token and returns an access type JSON web
+    token if the refresh token is valid.
+    """
+    serializer_class = serializers.TokenRefreshSerializer
+
+token_refresh = TokenRefreshView.as_view()
+
+
+class TokenObtainSlidingView(TokenViewBase):
+    """
+    Takes a set of user credentials and returns a sliding JSON web token to
+    prove the authentication of those credentials.
+    """
+    serializer_class = serializers.TokenObtainSlidingSerializer
+
+token_obtain_sliding = TokenObtainSlidingView.as_view()
+
+
+class TokenRefreshSlidingView(TokenViewBase):
+    """
+    Takes a sliding JSON web token and returns a new, refreshed version if the
+    token's refresh period has not expired.
+    """
+    serializer_class = serializers.TokenRefreshSlidingSerializer
+
+token_refresh_sliding = TokenRefreshSlidingView.as_view()
+
+
+class TokenVerifyView(TokenViewBase):
+    """
+    Takes a token and indicates if it is valid.  This view provides no
+    information about a token's fitness for a particular use.
+    """
+    serializer_class = serializers.TokenVerifySerializer
+
+token_verify = TokenVerifyView.as_view()

+ 10 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/setup.cfg

@@ -0,0 +1,10 @@
+[wheel]
+universal = 1
+
+[metadata]
+license_file = LICENSE.txt
+
+[egg_info]
+tag_build = 
+tag_date = 0
+

+ 56 - 0
desktop/core/ext-py/djangorestframework_simplejwt-3.3/setup.py

@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from io import open
+import os
+import re
+import shutil
+import sys
+
+from setuptools import setup, find_packages
+
+
+init_py = open(os.path.join('rest_framework_simplejwt', '__init__.py')).read()
+version = re.search(r'''^__version__ = ['"]([^'"]+)['"]$''', init_py).group(1)
+
+
+if sys.argv[-1] == 'publish':
+    if os.system('pip freeze | grep twine'):
+        print('twine not installed.\nUse `pip install twine`.\nExiting.')
+        sys.exit()
+    os.system('python setup.py sdist bdist_wheel')
+    os.system('twine upload dist/*')
+    print('You probably want to also tag the version now:')
+    print("  git tag -a %s -m 'version %s'" % (version, version))
+    print('  git push --tags')
+    shutil.rmtree('dist')
+    shutil.rmtree('build')
+    shutil.rmtree('djangorestframework_simplejwt.egg-info')
+    sys.exit()
+
+
+setup(
+    name='djangorestframework_simplejwt',
+    version=version,
+    url='https://github.com/davesque/django-rest-framework-simplejwt',
+    license='MIT',
+    description='A minimal JSON Web Token authentication plugin for Django REST Framework',
+    long_description=open('README.rst', 'r', encoding='utf-8').read(),
+    author='David Sanders',
+    author_email='davesque@gmail.com',
+    packages=find_packages(exclude=['tests', 'licenses', 'requirements']),
+    install_requires=['django', 'djangorestframework', 'pyjwt'],
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 3',
+        'Topic :: Internet :: WWW/HTTP',
+    ]
+)