Run testproj in a Heroku demo app (#38)

* Add Heroku configuration
* Add links in API description
* Read database connection string from DATABASE_URL environment variable
* Restructure settings files for production
* Run server using gunicorn and servce static files with whitenoise
* Install drf-yasg from source instead of pypi in testproj
* Add readme links to demo app
openapi3
Cristi Vîjdea 2018-01-10 10:18:22 +01:00 committed by GitHub
parent 6b38a3b6c1
commit c4379dc6a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 188 additions and 50 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
node_modules/ node_modules/
testproj/db.sqlite3 testproj/db.sqlite3
testproj/staticfiles
.vscode/ .vscode/
# Created by .ignore support plugin (hsz.mobi) # Created by .ignore support plugin (hsz.mobi)

View File

@ -4,7 +4,7 @@
<facet type="django" name="Django"> <facet type="django" name="Django">
<configuration> <configuration>
<option name="rootFolder" value="$MODULE_DIR$/testproj" /> <option name="rootFolder" value="$MODULE_DIR$/testproj" />
<option name="settingsModule" value="testproj/settings.py" /> <option name="settingsModule" value="testproj/settings/local.py" />
<option name="manageScript" value="manage.py" /> <option name="manageScript" value="manage.py" />
<option name="environment" value="&lt;map/&gt;" /> <option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" /> <option name="doNotUseTestRunner" value="false" />

View File

@ -38,7 +38,7 @@ jobs:
distributions: "sdist bdist_wheel" distributions: "sdist bdist_wheel"
allow_failures: allow_failures:
- env: TOXENV=flake8 - env: TOXENV=lint
- env: DRF=master - env: DRF=master
fast_finish: true fast_finish: true

2
Procfile 100644
View File

@ -0,0 +1,2 @@
release: python testproj/manage.py migrate && python testproj/manage.py shell -c "import createsuperuser"
web: gunicorn --chdir testproj testproj.wsgi --log-file -

View File

@ -20,6 +20,11 @@ Resources:
* **Source**: https://github.com/axnsan12/drf-yasg/ * **Source**: https://github.com/axnsan12/drf-yasg/
* **Documentation**: https://drf-yasg.readthedocs.io/ * **Documentation**: https://drf-yasg.readthedocs.io/
* **Changelog**: https://drf-yasg.readthedocs.io/en/stable/changelog.html * **Changelog**: https://drf-yasg.readthedocs.io/en/stable/changelog.html
* **Live demo**: https://drf-yasg-demo.herokuapp.com/
.. image:: https://www.herokucdn.com/deploy/button.svg
:target: https://heroku.com/deploy?template=https://github.com/axnsan12/drf-yasg
:alt: heroku deploy button
******** ********
Features Features

17
app.json 100644
View File

@ -0,0 +1,17 @@
{
"name": "drf-yasg Demo app",
"description": "A demonstrative app using https://github.com/axnsan12/drf-yasg",
"repository": "https://github.com/axnsan12/drf-yasg",
"logo": "https://swaggerhub.com/wp-content/uploads/2017/10/Swagger-Icon.svg",
"keywords": [
"django",
"django-rest-framework",
"swagger",
"openapi"
],
"env": {
"DJANGO_SETTINGS_MODULE": "testproj.settings.heroku",
"DJANGO_SECRET_KEY": "m76=^#=z7xv5^(o%4dv9w7+1_c)y2m6)1ogjx%s@9$1^nupry="
},
"success_url": "/"
}

View File

@ -205,7 +205,7 @@ sys.path.insert(0, os.path.abspath('../src'))
# activate the Django testproj to be able to succesfully import drf_yasg # activate the Django testproj to be able to succesfully import drf_yasg
sys.path.insert(0, os.path.abspath('../testproj')) sys.path.insert(0, os.path.abspath('../testproj'))
os.putenv('DJANGO_SETTINGS_MODULE', 'testproj.settings') os.putenv('DJANGO_SETTINGS_MODULE', 'testproj.settings.local')
from django.conf import settings # noqa: E402 from django.conf import settings # noqa: E402

1
requirements.txt 100644
View File

@ -0,0 +1 @@
-r requirements/heroku.txt

View File

@ -2,3 +2,6 @@
-r tox.txt -r tox.txt
-r test.txt -r test.txt
-r lint.txt -r lint.txt
tox-battery>=0.5
detox>=0.11

View File

@ -0,0 +1,9 @@
# requirements necessary when deploying the test project to heroku
.[validation]
Django>=1.11.7,<2.0; python_version <= "2.7"
Django>=1.11.7; python_version >= "3.4"
-r testproj.txt
psycopg2>=2.7.3
gunicorn>=19.7.1
whitenoise>=3.3.1

View File

@ -5,3 +5,5 @@ django-cors-headers>=2.1.0
django-filter>=1.1.0,<2.0; python_version == "2.7" django-filter>=1.1.0,<2.0; python_version == "2.7"
django-filter>=1.1.0; python_version >= "3.4" django-filter>=1.1.0; python_version >= "3.4"
djangorestframework-camel-case>=0.2.0 djangorestframework-camel-case>=0.2.0
dj-database-url>=0.4.2
user_agents>=1.1.0

View File

@ -1,6 +1,4 @@
# requirements for building and running tox # requirements for building and running tox
tox>=2.9.1 tox>=2.9.1
tox-battery>=0.5
detox>=0.11
-r setup.txt -r setup.txt

1
runtime.txt 100644
View File

@ -0,0 +1 @@
python-3.6.4

View File

@ -3,6 +3,8 @@
import distutils.core import distutils.core
import io import io
import os import os
import random
import string
import sys import sys
from setuptools import find_packages, setup from setuptools import find_packages, setup
@ -32,14 +34,14 @@ def _install_setup_requires(attrs):
dist.fetch_build_eggs(dist.setup_requires) dist.fetch_build_eggs(dist.setup_requires)
if 'sdist' in sys.argv: try:
try:
# try to install setuptools_scm before setuptools does it, otherwise our monkey patch below will come too early # try to install setuptools_scm before setuptools does it, otherwise our monkey patch below will come too early
# (setuptools_scm adds find_files hooks into setuptools on install) # (setuptools_scm adds find_files hooks into setuptools on install)
_install_setup_requires({'setup_requires': requirements_setup}) _install_setup_requires({'setup_requires': requirements_setup})
except Exception: except Exception:
pass pass
if 'sdist' in sys.argv:
try: try:
# see https://github.com/pypa/setuptools_scm/issues/190, setuptools_scm includes ALL versioned files from # see https://github.com/pypa/setuptools_scm/issues/190, setuptools_scm includes ALL versioned files from
# the git repo into the sdist by default, and there is no easy way to provide an opt-out; # the git repo into the sdist by default, and there is no easy way to provide an opt-out;
@ -51,9 +53,22 @@ if 'sdist' in sys.argv:
except ImportError: except ImportError:
pass pass
try:
# this is a workaround for being able to install the package from source without working from a git checkout
# it is needed for building succesfully on Heroku
from setuptools_scm import get_version
version = get_version()
version_kwargs = {'use_scm_version': True}
except LookupError:
if 'sdist' in sys.argv or 'bdist_wheel' in sys.argv:
raise
rnd = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
version_kwargs = {'version': '0.0.0.dummy+' + rnd}
setup( setup(
name='drf-yasg', name='drf-yasg',
use_scm_version=True,
packages=find_packages('src'), packages=find_packages('src'),
package_dir={'': 'src'}, package_dir={'': 'src'},
include_package_data=True, include_package_data=True,
@ -89,4 +104,5 @@ setup(
'Topic :: Documentation', 'Topic :: Documentation',
'Topic :: Software Development :: Code Generators', 'Topic :: Software Development :: Code Generators',
], ],
**version_kwargs
) )

View File

@ -462,8 +462,10 @@ else:
"""Converts property names to camelCase if ``CamelCaseJSONParser`` or ``CamelCaseJSONRenderer`` are used.""" """Converts property names to camelCase if ``CamelCaseJSONParser`` or ``CamelCaseJSONRenderer`` are used."""
def is_camel_case(self): def is_camel_case(self):
return any(issubclass(parser, CamelCaseJSONParser) for parser in self.view.parser_classes) \ return (
or any(issubclass(renderer, CamelCaseJSONRenderer) for renderer in self.view.renderer_classes) any(issubclass(parser, CamelCaseJSONParser) for parser in self.view.parser_classes) or
any(issubclass(renderer, CamelCaseJSONRenderer) for renderer in self.view.renderer_classes)
)
def process_result(self, result, method_name, obj, **kwargs): def process_result(self, result, method_name, obj, **kwargs):
if isinstance(result, openapi.Schema.OR_REF) and self.is_camel_case(): if isinstance(result, openapi.Schema.OR_REF) and self.is_camel_case():

View File

@ -1,11 +1,15 @@
from __future__ import print_function from __future__ import print_function
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.utils import IntegrityError
username = 'admin' username = 'admin'
email = 'admin@admin.admin' email = 'admin@admin.admin'
password = 'passwordadmin' password = 'passwordadmin'
User.objects.filter(username=username).delete()
User.objects.create_superuser(username, email, password)
print("Created superuser '%s <%s>' with password '%s'" % (username, email, password)) try:
User.objects.create_superuser(username, email, password)
except IntegrityError:
print("User '%s <%s>' already exists" % (username, email))
else:
print("Created superuser '%s <%s>' with password '%s'" % (username, email, password))

View File

@ -3,7 +3,7 @@ import os
import sys import sys
if __name__ == "__main__": if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings.local")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError: except ImportError:

View File

@ -1,3 +1,4 @@
drf-yasg[validation] ..[validation]
Django>=1.11.7 Django>=1.11.7,<2.0; python_version <= "2.7"
Django>=1.11.7; python_version >= "3.4"
-r ../requirements/testproj.txt -r ../requirements/testproj.txt

View File

@ -1,16 +1,7 @@
import os import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '!z1yj(9uz)zk0gg@5--j)bc4h^i!8))r^dezco8glf190e0&#p'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
'127.0.0.1', '127.0.0.1',
@ -69,16 +60,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'testproj.wsgi.application' WSGI_APPLICATION = 'testproj.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
@ -97,16 +78,19 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# Django Rest Framework
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ( 'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated', 'rest_framework.permissions.IsAuthenticated',
) )
} }
# drf-yasg
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'LOGIN_URL': '/admin/login', 'LOGIN_URL': '/admin/login',
'LOGOUT_URL': '/admin/logout', 'LOGOUT_URL': '/admin/logout',
'VALIDATOR_URL': 'http://localhost:8189',
'DEFAULT_INFO': 'testproj.urls.swagger_info' 'DEFAULT_INFO': 'testproj.urls.swagger_info'
} }
@ -128,9 +112,14 @@ USE_TZ = True
# https://docs.djangoproject.com/en/1.11/howto/static-files/ # https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Testing
TEST_RUNNER = 'testproj.runner.PytestTestRunner' TEST_RUNNER = 'testproj.runner.PytestTestRunner'
# Logging configuration
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': True, 'disable_existing_loggers': True,

View File

@ -0,0 +1,35 @@
import dj_database_url
from .base import * # noqa: F403
DEBUG = True
ALLOWED_HOSTS.append('.herokuapp.com')
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
assert SECRET_KEY, 'DJANGO_SECRET_KEY environment variable must be set'
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# Simplified static file serving.
# https://warehouse.python.org/project/whitenoise/
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
MIDDLEWARE.insert(0, 'whitenoise.middleware.WhiteNoiseMiddleware')
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': dj_database_url.config(conn_max_age=600)
}
SILENCED_SYSTEM_CHECKS = [
'security.W004', # SECURE_HSTS_SECONDS
'security.W008', # SECURE_SSL_REDIRECT
]

View File

@ -0,0 +1,24 @@
import os
import dj_database_url
from .base import * # noqa: F403
SWAGGER_SETTINGS.update({'VALIDATOR_URL': 'http://localhost:8189'})
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
db_path = os.path.join(BASE_DIR, 'db.sqlite3')
DATABASES = {
'default': dj_database_url.parse('sqlite:///' + db_path)
}
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '!z1yj(9uz)zk0gg@5--j)bc4h^i!8))r^dezco8glf190e0&#p'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

View File

@ -1,5 +1,7 @@
import user_agents
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.shortcuts import redirect
from rest_framework import permissions from rest_framework import permissions
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
@ -9,7 +11,13 @@ from drf_yasg.views import get_schema_view
swagger_info = openapi.Info( swagger_info = openapi.Info(
title="Snippets API", title="Snippets API",
default_version='v1', default_version='v1',
description="Test description", description="""This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg) Django Rest Framework library.
The `swagger-ui` view can be found [here](/cached/swagger).
The `ReDoc` view can be found [here](/cached/redoc).
The swagger YAML document can be found [here](/cached/swagger.yaml).
You can log in using the pre-existing `admin` user with password `passwordadmin`.""", # noqa
terms_of_service="https://www.google.com/policies/terms/", terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="contact@snippets.local"), contact=openapi.Contact(email="contact@snippets.local"),
license=openapi.License(name="BSD License"), license=openapi.License(name="BSD License"),
@ -27,6 +35,18 @@ def plain_view(request):
pass pass
def root_redirect(request):
user_agent_string = request.META.get('HTTP_USER_AGENT', '')
user_agent = user_agents.parse(user_agent_string)
if user_agent.is_mobile:
schema_view = 'cschema-redoc'
else:
schema_view = 'cschema-swagger-ui'
return redirect(schema_view, permanent=True)
urlpatterns = [ urlpatterns = [
url(r'^swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=0), name='schema-json'), url(r'^swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=0), name='schema-json'),
url(r'^swagger/$', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), url(r'^swagger/$', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
@ -35,6 +55,8 @@ urlpatterns = [
url(r'^cached/swagger/$', SchemaView.with_ui('swagger', cache_timeout=None), name='cschema-swagger-ui'), url(r'^cached/swagger/$', SchemaView.with_ui('swagger', cache_timeout=None), name='cschema-swagger-ui'),
url(r'^cached/redoc/$', SchemaView.with_ui('redoc', cache_timeout=None), name='cschema-redoc'), url(r'^cached/redoc/$', SchemaView.with_ui('redoc', cache_timeout=None), name='cschema-redoc'),
url(r'^$', root_redirect),
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^snippets/', include('snippets.urls')), url(r'^snippets/', include('snippets.urls')),
url(r'^articles/', include('articles.urls')), url(r'^articles/', include('articles.urls')),

View File

@ -2,6 +2,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings.local")
application = get_wsgi_application() application = get_wsgi_application()

View File

@ -1,7 +1,11 @@
swagger: '2.0' swagger: '2.0'
info: info:
title: Snippets API title: Snippets API
description: Test description description: "This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg)\
\ Django Rest Framework library.\n\nThe `swagger-ui` view can be found [here](/cached/swagger).\
\ \nThe `ReDoc` view can be found [here](/cached/redoc). \nThe swagger YAML\
\ document can be found [here](/cached/swagger.yaml). \n\nYou can log in using\
\ the pre-existing `admin` user with password `passwordadmin`."
termsOfService: https://www.google.com/policies/terms/ termsOfService: https://www.google.com/policies/terms/
contact: contact:
email: contact@snippets.local email: contact@snippets.local

10
tox.ini
View File

@ -46,15 +46,16 @@ deps =
-rrequirements/docs.txt -rrequirements/docs.txt
commands = commands =
python setup.py check --restructuredtext --metadata --strict python setup.py check --restructuredtext --metadata --strict
sphinx-build -WnEa -b html docs docs\_build\html sphinx-build -WnEa -b html docs docs/_build/html
[pytest] [pytest]
DJANGO_SETTINGS_MODULE = testproj.settings DJANGO_SETTINGS_MODULE = testproj.settings.local
python_paths = testproj python_paths = testproj
[flake8] [flake8]
max-line-length = 120 max-line-length = 120
exclude = **/migrations/* exclude = **/migrations/*
ignore = F405
[isort] [isort]
skip = .eggs,.tox,docs,env,venv skip = .eggs,.tox,docs,env,venv
@ -68,6 +69,7 @@ known_standard_library =
collections,copy,distutils,functools,inspect,io,json,logging,operator,os,pkg_resources,re,setuptools,sys, collections,copy,distutils,functools,inspect,io,json,logging,operator,os,pkg_resources,re,setuptools,sys,
types,warnings types,warnings
known_third_party = known_third_party =
coreapi,coreschema,datadiff,django,django_filters,djangorestframework_camel_case,flex,inflection,pygments, coreapi,coreschema,datadiff,dj_database_url,django,django_filters,djangorestframework_camel_case,flex,gunicorn,
pytest,rest_framework,ruamel,setuptools_scm,swagger_spec_validator,uritemplate inflection,pygments,pytest,rest_framework,ruamel,setuptools_scm,swagger_spec_validator,uritemplate,user_agents,
whitenoise
known_first_party = drf_yasg,testproj,articles,snippets,users,urlconfs known_first_party = drf_yasg,testproj,articles,snippets,users,urlconfs