├── .gitignore ├── HISTORY.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs └── api.md ├── graph_auth ├── __init__.py ├── apps.py ├── schema.py └── settings.py ├── setup.cfg ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | build/ 4 | dist/ 5 | .eggs/ 6 | .idea/ 7 | .tox/ 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | .coverage 13 | dist 14 | build 15 | eggs 16 | parts 17 | bin 18 | var 19 | sdist 20 | develop-eggs 21 | .installed.cfg 22 | lib 23 | lib64 24 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | Pending 5 | ------- 6 | * New release notes go here. 7 | 8 | 0.3.3 (2017-8-22) 9 | ----------------- 10 | * Handled case with empty context and empty user in context. 11 | 12 | 0.3.2 (2017-2-8) 13 | ----------------- 14 | * Added admin only registration option 15 | * Added welcome email options 16 | * Improved dynamic username fields. 17 | 18 | 0.3.1 (2016-11-13) 19 | ----------------- 20 | 21 | * Add installation dependencies. (https://github.com/morgante/django-graph-auth/pull/2) 22 | 23 | 0.3.0 (2016-10-16) 24 | ----------------- 25 | 26 | * Add user changing functionality. 27 | 28 | 0.0.1 (2016-10-10) 29 | ----------------- 30 | 31 | * Initial release 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Morgante Pell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include HISTORY.rst README.rst 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-graph-auth 2 | ======================= 3 | 4 | django-graph-auth is a Django application which provides simple mutations and queries for managing users with GraphQL. It can register users, log in, reset users, and expose `JSON web tokens`_. 5 | 6 | Documentation can be found on `GitHub`_. 7 | 8 | .. _Django Rest Framework: http://www.django-rest-framework.org/ 9 | 10 | .. _JSON web tokens: http://getblimp.github.io/django-rest-framework-jwt/ 11 | 12 | .. _GitHub: https://github.com/morgante/django-graph-auth/blob/master/docs/api.md 13 | 14 | Requirements 15 | ------------ 16 | 17 | This has only been tested with: 18 | 19 | * Python: 3.5 20 | * Django: 1.10 21 | 22 | Setup 23 | ----- 24 | 25 | Install from **pip**: 26 | 27 | .. code-block:: sh 28 | 29 | pip install django-graph-auth 30 | 31 | and then add it to your installed apps: 32 | 33 | .. code-block:: python 34 | 35 | INSTALLED_APPS = ( 36 | ... 37 | 'graph_auth', 38 | ... 39 | ) 40 | 41 | You will also need to edit your base schema to import the mutations and queries, like this: 42 | 43 | .. code-block:: python 44 | 45 | import graphene 46 | from graphene import relay, ObjectType 47 | 48 | import graph_auth.schema 49 | 50 | class Query(graph_auth.schema.Query, ObjectType): 51 | node = relay.Node.Field() 52 | 53 | class Mutation(graph_auth.schema.Mutation, ObjectType): 54 | pass 55 | 56 | schema = graphene.Schema(query=Query, mutation=Mutation) 57 | 58 | Optional Settings 59 | ------- 60 | 61 | .. code-block:: python 62 | 63 | GRAPH_AUTH = { 64 | 'USER_FIELDS': ('email', 'first_name', 'last_name', ), # Which user fields are available 65 | 'ONLY_ADMIN_REGISTRATION': False, # Only alow admins to register new users 66 | 'WELCOME_EMAIL_TEMPLATE': None, # Email template for optional welcome email, user object fields is in scope 67 | 'EMAIL_FROM': None # Email from for welcome email 68 | } 69 | 70 | Credits 71 | ------- 72 | 73 | ``django-graph-auth`` was created by Morgante Pell (`@morgante 74 | `_) and Anthony Loko (`@amelius15 `_). It is based on `django-rest-auth`_. 75 | 76 | .. _django-rest-auth: https://github.com/Tivix/django-rest-auth 77 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | API Endpoints 2 | ======== 3 | 4 | ## Queries 5 | 6 | ### Get Users 7 | ``` 8 | query { 9 | users { 10 | edges { 11 | node { 12 | id, 13 | firstName 14 | } 15 | } 16 | } 17 | } 18 | ``` 19 | 20 | ### Get Current User 21 | ``` 22 | query { 23 | me { 24 | id, 25 | firstName, 26 | lastName, 27 | email 28 | } 29 | } 30 | ``` 31 | 32 | ## Mutations 33 | 34 | ### Registration 35 | ``` 36 | mutation { 37 | registerUser(input: { 38 | email: "morgante@mastermade.co", 39 | password: "test_password", 40 | firstName: "Morgante", 41 | lastName: "Pell" 42 | }) { 43 | ok, 44 | user { 45 | id, 46 | firstName, 47 | email, 48 | token 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ### Log In 55 | Log in to get a JWT for future requests. 56 | ``` 57 | mutation { 58 | loginUser(input: { 59 | email: "morgante@mastermade.co", 60 | password: "test_password" 61 | }) { 62 | ok, 63 | user { 64 | id, 65 | firstName, 66 | email, 67 | token 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | ### Reset Password 74 | First send a password reset request with this mutation: 75 | ``` 76 | mutation { 77 | resetPasswordRequest(input: { 78 | email: "morgante@mastermade.co" 79 | }) { 80 | ok 81 | } 82 | } 83 | ``` 84 | 85 | This will then send an email to the user including a link. This link includes an `id` and a `token`. You then make another query with those components and the new password: 86 | ``` 87 | mutation { 88 | resetPassword(input: { 89 | id: "uid", 90 | token: "token", 91 | password: "new_password" 92 | }) { 93 | ok, 94 | user { 95 | id 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | 102 | ### Update User 103 | Logged in users can update themselves. 104 | 105 | Please note that the user needs to provide their current password to change their password. 106 | Changing the password may reset a user's session. 107 | ``` 108 | mutation { 109 | updateUser(input: { 110 | email: "new@mastermade.co" 111 | firstName: "newFirst", 112 | lastName: "newLast", 113 | username: "newname" 114 | password: "excellent_password" 115 | }) { 116 | ok 117 | result { 118 | email 119 | firstName 120 | lastName 121 | } 122 | } 123 | } 124 | ``` 125 | -------------------------------------------------------------------------------- /graph_auth/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3.3' 2 | -------------------------------------------------------------------------------- /graph_auth/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class GraphauthConfig(AppConfig): 4 | name = 'graph_auth' 5 | -------------------------------------------------------------------------------- /graph_auth/schema.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from graphene import relay, AbstractType, Mutation, Node 3 | from graphql_relay.node.node import from_global_id 4 | import graphene 5 | from graphene_django.filter import DjangoFilterConnectionField 6 | from graphene_django import DjangoObjectType 7 | import django_filters 8 | import logging 9 | from django.db import models 10 | from django.utils.http import urlsafe_base64_decode as uid_decoder 11 | from django.utils.encoding import force_text 12 | 13 | from rest_framework_jwt.settings import api_settings 14 | from graph_auth.settings import graph_auth_settings 15 | 16 | import django.contrib.auth 17 | from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm 18 | from django.contrib.auth.tokens import default_token_generator as token_generator 19 | from django.utils.encoding import force_bytes 20 | from django.utils.http import urlsafe_base64_encode 21 | 22 | UserModel = django.contrib.auth.get_user_model() 23 | 24 | class DynamicUsernameMeta(type): 25 | def __new__(mcs, classname, bases, dictionary): 26 | dictionary[UserModel.USERNAME_FIELD] = graphene.String(required=True) 27 | return type.__new__(mcs, classname, bases, dictionary) 28 | 29 | class UserNode(DjangoObjectType): 30 | class Meta: 31 | model = UserModel 32 | interfaces = (relay.Node, ) 33 | only_fields = graph_auth_settings.USER_FIELDS 34 | filter_fields = graph_auth_settings.USER_FIELDS 35 | 36 | @classmethod 37 | def get_node(cls, id, context, info): 38 | user = super(UserNode, cls).get_node(id, context, info) 39 | if context.user.id and (user.id == context.user.id or context.user.is_staff): 40 | return user 41 | else: 42 | return None 43 | 44 | token = graphene.String() 45 | def resolve_token(self, args, context, info): 46 | if (not context or not context.user or self.id != context.user.id) and not getattr(self, 'is_current_user', False): 47 | return None 48 | 49 | jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER 50 | jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER 51 | 52 | payload = jwt_payload_handler(self) 53 | token = jwt_encode_handler(payload) 54 | 55 | return token 56 | 57 | class RegisterUser(relay.ClientIDMutation): 58 | class Input(metaclass=DynamicUsernameMeta): 59 | email = graphene.String(required=True) 60 | password = graphene.String() 61 | first_name = graphene.String() 62 | last_name = graphene.String() 63 | 64 | ok = graphene.Boolean() 65 | user = graphene.Field(UserNode) 66 | 67 | @classmethod 68 | def mutate_and_get_payload(cls, input, context, info): 69 | model = UserModel 70 | if graph_auth_settings.ONLY_ADMIN_REGISTRATION and not (context.user.id and context.user.is_staff): 71 | return RegisterUser(ok=False, user=None) 72 | if 'clientMutationId' in input: 73 | input.pop('clientMutationId') 74 | email = input.pop('email') 75 | username = input.pop(UserModel.USERNAME_FIELD, email) 76 | password = input.pop('password') if 'password' in input else model.objects.make_random_password() 77 | 78 | user = model.objects.create_user(username, email, password, **input) 79 | user.is_current_user = True 80 | 81 | if graph_auth_settings.WELCOME_EMAIL_TEMPLATE is not None and graph_auth_settings.EMAIL_FROM is not None: 82 | from mail_templated import EmailMessage 83 | input_data = user.__dict__ 84 | input_data['password'] = password 85 | message = EmailMessage(graph_auth_settings.WELCOME_EMAIL_TEMPLATE, input_data, graph_auth_settings.EMAIL_FROM, [user.email]) 86 | message.send() 87 | 88 | return RegisterUser(ok=True, user=user) 89 | 90 | class LoginUser(relay.ClientIDMutation): 91 | class Input(metaclass=DynamicUsernameMeta): 92 | password = graphene.String(required=True) 93 | 94 | ok = graphene.Boolean() 95 | user = graphene.Field(UserNode) 96 | 97 | @classmethod 98 | def mutate_and_get_payload(cls, input, context, info): 99 | model = UserModel 100 | 101 | params = { 102 | model.USERNAME_FIELD: input.get(model.USERNAME_FIELD, ''), 103 | 'password': input.get('password') 104 | } 105 | 106 | user = django.contrib.auth.authenticate(**params) 107 | 108 | if user: 109 | user.is_current_user = True 110 | return cls(ok=True, user=user) 111 | else: 112 | return cls(ok=False, user=None) 113 | 114 | class ResetPasswordRequest(relay.ClientIDMutation): 115 | class Input: 116 | email = graphene.String(required=True) 117 | 118 | ok = graphene.Boolean() 119 | 120 | @classmethod 121 | def mutate_and_get_payload(cls, input, context, info): 122 | if graph_auth_settings.CUSTOM_PASSWORD_RESET_TEMPLATE is not None and graph_auth_settings.EMAIL_FROM is not None and graph_auth_settings.PASSWORD_RESET_URL_TEMPLATE is not None: 123 | 124 | from mail_templated import EmailMessage 125 | 126 | for user in UserModel.objects.filter(email=input.get('email')): 127 | uid = urlsafe_base64_encode(force_bytes(user.pk)).decode() 128 | token = token_generator.make_token(user) 129 | link = graph_auth_settings.PASSWORD_RESET_URL_TEMPLATE.format(token=token, uid=uid) 130 | input_data = { 131 | "email": user.email, 132 | "first_name": user.first_name, 133 | "last_name": user.last_name, 134 | "link": link 135 | } 136 | message = EmailMessage(graph_auth_settings.CUSTOM_PASSWORD_RESET_TEMPLATE, input_data, graph_auth_settings.EMAIL_FROM, [user.email]) 137 | message.send() 138 | 139 | else: 140 | data = { 141 | 'email': input.get('email'), 142 | } 143 | 144 | reset_form = PasswordResetForm(data=data) 145 | 146 | if not reset_form.is_valid(): 147 | raise Exception("The email is not valid") 148 | 149 | options = { 150 | 'use_https': context.is_secure(), 151 | 'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'), 152 | 'request': context 153 | } 154 | 155 | reset_form.save(**options) 156 | 157 | return ResetPasswordRequest(ok=True) 158 | 159 | class ResetPassword(relay.ClientIDMutation): 160 | class Input: 161 | password = graphene.String(required=True) 162 | id = graphene.String(required=True) 163 | token = graphene.String(required=True) 164 | 165 | ok = graphene.Boolean() 166 | user = graphene.Field(UserNode) 167 | 168 | @classmethod 169 | def mutate_and_get_payload(cls, input, context, info): 170 | Model = UserModel 171 | 172 | try: 173 | uid = force_text(uid_decoder(input.get('id'))) 174 | user = Model.objects.get(pk=uid) 175 | except (TypeError, ValueError, OverflowError, Model.DoesNotExist): 176 | raise Exception('uid has an invalid value') 177 | 178 | data = { 179 | 'uid': input.get('id'), 180 | 'token': input.get('token'), 181 | 'new_password1': input.get('password'), 182 | 'new_password2': input.get('password') 183 | } 184 | 185 | reset_form = SetPasswordForm(user=user, data=data) 186 | 187 | if not reset_form.is_valid(): 188 | raise Exception("The token is not valid") 189 | 190 | reset_form.save() 191 | 192 | return ResetPassword(ok=True, user=user) 193 | 194 | class UpdateUsernameMeta(type): 195 | def __new__(mcs, classname, bases, dictionary): 196 | for field in graph_auth_settings.USER_FIELDS: 197 | dictionary[field] = graphene.String() 198 | return type.__new__(mcs, classname, bases, dictionary) 199 | 200 | class UpdateUser(relay.ClientIDMutation): 201 | class Input(metaclass=UpdateUsernameMeta): 202 | password = graphene.String() 203 | current_password = graphene.String() 204 | 205 | ok = graphene.Boolean() 206 | result = graphene.Field(UserNode) 207 | 208 | @classmethod 209 | def mutate_and_get_payload(cls, input, context, info): 210 | Model = UserModel 211 | user = context.user 212 | user.is_current_user = True 213 | 214 | if not user.is_authenticated: 215 | raise Exception("You must be logged in to update renter profiles.") 216 | 217 | if 'password' in input: 218 | try: 219 | current_password = input.pop('current_password') 220 | except KeyError: 221 | raise Exception("Please provide your current password to change your password.") 222 | 223 | if user.check_password(current_password): 224 | user.set_password(input.pop('password')) 225 | else: 226 | raise Exception("Current password is incorrect.") 227 | 228 | for key, value in input.items(): 229 | if not key is 'current_password': 230 | setattr(user, key, value) 231 | 232 | user.save() 233 | 234 | updated_user = Model.objects.get(pk=user.pk) 235 | 236 | return UpdateUser(ok=True, result=updated_user) 237 | 238 | class Query(AbstractType): 239 | user = relay.Node.Field(UserNode) 240 | users = DjangoFilterConnectionField(UserNode) 241 | 242 | me = graphene.Field(UserNode) 243 | def resolve_me(self, args, context, info): 244 | return UserNode.get_node(context.user.id, context, info) 245 | 246 | class Mutation(AbstractType): 247 | register_user = RegisterUser.Field() 248 | login_user = LoginUser.Field() 249 | reset_password_request = ResetPasswordRequest.Field() 250 | reset_password = ResetPassword.Field() 251 | update_user = UpdateUser.Field() 252 | -------------------------------------------------------------------------------- /graph_auth/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Settings for Graph Auth framework are all namespaced in the GRAPH_AUTH setting. 3 | For example your project's `settings.py` file might look like this: 4 | REST_FRAMEWORK = { 5 | 'USER_FIELDS': ('first_name', 'last_name', ) 6 | } 7 | This module provides the `graph_auth_settings` object, that is used to access 8 | graph auth settings, checking for user settings first, then falling 9 | back to the defaults. 10 | 11 | This implementation is taken from Graphene's Django library: 12 | https://github.com/graphql-python/graphene-django/blob/master/graphene_django/settings.py 13 | """ 14 | from __future__ import unicode_literals 15 | 16 | from django.conf import settings 17 | from django.test.signals import setting_changed 18 | from django.utils import six 19 | 20 | try: 21 | import importlib # Available in Python 3.1+ 22 | except ImportError: 23 | from django.utils import importlib # Will be removed in Django 1.9 24 | 25 | DEFAULTS = { 26 | 'USER_FIELDS': ('email', 'first_name', 'last_name', ), 27 | 'ONLY_ADMIN_REGISTRATION': False, 28 | 'WELCOME_EMAIL_TEMPLATE': None, 29 | 'EMAIL_FROM': None, 30 | 'CUSTOM_PASSWORD_RESET_TEMPLATE': None, 31 | 'PASSWORD_RESET_URL_TEMPLATE': None 32 | } 33 | 34 | # List of settings that may be in string import notation. 35 | IMPORT_STRINGS = () 36 | 37 | def perform_import(val, setting_name): 38 | """ 39 | If the given setting is a string import notation, 40 | then perform the necessary import or imports. 41 | """ 42 | if val is None: 43 | return None 44 | elif isinstance(val, six.string_types): 45 | return import_from_string(val, setting_name) 46 | elif isinstance(val, (list, tuple)): 47 | return [import_from_string(item, setting_name) for item in val] 48 | return val 49 | 50 | 51 | def import_from_string(val, setting_name): 52 | """ 53 | Attempt to import a class from a string representation. 54 | """ 55 | try: 56 | # Nod to tastypie's use of importlib. 57 | parts = val.split('.') 58 | module_path, class_name = '.'.join(parts[:-1]), parts[-1] 59 | module = importlib.import_module(module_path) 60 | return getattr(module, class_name) 61 | except (ImportError, AttributeError) as e: 62 | msg = "Could not import '%s' for Graph Auth setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e) 63 | raise ImportError(msg) 64 | 65 | 66 | class GraphAuthSettings(object): 67 | """ 68 | A settings object, that allows API settings to be accessed as properties. 69 | For example: 70 | from graph_auth.settings import graph_auth_settings 71 | print(graph_auth_settings.USER_FIELDS) 72 | Any setting with string import paths will be automatically resolved 73 | and return the class, rather than the string literal. 74 | """ 75 | def __init__(self, user_settings=None, defaults=None, import_strings=None): 76 | if user_settings: 77 | self._user_settings = self.__check_user_settings(user_settings) 78 | self.defaults = defaults or DEFAULTS 79 | self.import_strings = import_strings or IMPORT_STRINGS 80 | 81 | @property 82 | def user_settings(self): 83 | if not hasattr(self, '_user_settings'): 84 | self._user_settings = getattr(settings, 'GRAPH_AUTH', {}) 85 | return self._user_settings 86 | 87 | def __getattr__(self, attr): 88 | if attr not in self.defaults: 89 | raise AttributeError("Invalid Graph Auth setting: '%s'" % attr) 90 | 91 | try: 92 | # Check if present in user settings 93 | val = self.user_settings[attr] 94 | except KeyError: 95 | # Fall back to defaults 96 | val = self.defaults[attr] 97 | 98 | # Coerce import strings into classes 99 | if attr in self.import_strings: 100 | val = perform_import(val, attr) 101 | 102 | # Cache the result 103 | setattr(self, attr, val) 104 | return val 105 | 106 | graph_auth_settings = GraphAuthSettings(None, DEFAULTS, IMPORT_STRINGS) 107 | 108 | def reload_graph_auth_settings(*args, **kwargs): 109 | global graph_auth_settings 110 | setting, value = kwargs['setting'], kwargs['value'] 111 | if setting == 'GRAPH_AUTH': 112 | graph_auth_settings = GraphAuthSettings(value, DEFAULTS, IMPORT_STRINGS) 113 | 114 | setting_changed.connect(reload_graph_auth_settings) 115 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from graph_auth import __version__ 2 | from setuptools import setup 3 | 4 | with open('README.rst') as readme_file: 5 | readme = readme_file.read() 6 | 7 | with open('HISTORY.rst') as history_file: 8 | history = history_file.read() 9 | 10 | setup( 11 | name='django-graph-auth', 12 | version=__version__, 13 | description='django-graph-auth is a Django application which provides simple mutations and queries for managing users with GraphQL.', 14 | long_description=readme + '\n\n' + history, 15 | author='Morgante Pell', 16 | author_email='morgante.pell@morgante.net', 17 | url='https://github.com/morgante/django-graph-auth', 18 | packages=['graph_auth'], 19 | license='MIT License', 20 | keywords='django graphql api authentication jwt', 21 | platforms=['any'], 22 | classifiers=[ 23 | 'Development Status :: 5 - Production/Stable', 24 | 'Environment :: Web Environment', 25 | 'Framework :: Django', 26 | 'Framework :: Django :: 1.8', 27 | 'Framework :: Django :: 1.9', 28 | 'Framework :: Django :: 1.10', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 2', 34 | 'Programming Language :: Python :: 2.7', 35 | 'Programming Language :: Python :: 3', 36 | 'Programming Language :: Python :: 3.5', 37 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 38 | 'Topic :: Software Development :: Libraries :: Python Modules', 39 | ], 40 | install_requires=['djangorestframework','djangorestframework-jwt', 'django-mail-templated'], 41 | ) 42 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # this file is *not* meant to cover or endorse the use of tox or pytest or 2 | # testing in general, 3 | # 4 | # It's meant to show the use of: 5 | # 6 | # - check-manifest 7 | # confirm items checked into vcs are in your sdist 8 | # - python setup.py check (using the readme_renderer extension) 9 | # confirms your long_description will render correctly on pypi 10 | # 11 | # and also to help confirm pull requests to this project. 12 | 13 | [tox] 14 | envlist = py{26,27,33,34} 15 | 16 | [testenv] 17 | basepython = 18 | py26: python2.6 19 | py27: python2.7 20 | py33: python3.3 21 | py34: python3.4 22 | deps = 23 | check-manifest 24 | {py27,py33,py34}: readme_renderer 25 | flake8 26 | pytest 27 | commands = 28 | check-manifest --ignore tox.ini,tests* 29 | # py26 doesn't have "setup.py check" 30 | {py27,py33,py34}: python setup.py check -m -r -s 31 | flake8 . 32 | py.test tests 33 | [flake8] 34 | exclude = .tox,*.egg,build,data 35 | select = E,W,F 36 | --------------------------------------------------------------------------------