Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

swagger UI: add support for embedded Swagger UI API explorer #226

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
include README.rst
# Required by python 2.6 even though we include these in our setup.py
include pyramid_swagger/swagger_spec_schemas/v1.2/*
include pyramid_swagger/static/*
20 changes: 18 additions & 2 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ A few relevant settings for your `Pyramid .ini file <http://docs.pylonsproject.o

# Exclude certain endpoints from validation. Takes a list of regular
# expressions.
# Default: ^/static/? ^/api-docs/? ^/swagger.json
# Default: ^/static/? ^/pyramid_swagger/static/? ^/api-docs/? ^/api-explorer? /swagger.(json|yaml)
pyramid_swagger.exclude_paths = ^/static/? ^/api-docs/? ^/swagger.json

# Exclude pyramid routes from validation. Accepts a list of strings
Expand Down Expand Up @@ -96,10 +96,26 @@ A few relevant settings for your `Pyramid .ini file <http://docs.pylonsproject.o
# Default: False
pyramid_swagger.dereference_served_schema = false

# Path for Swagger UI static resource serving:
# Default: pyramid_swagger/static
pyramid_swagger.swagger_ui_static = pyramid_swagger/static

# Path for Swagger UI index view serving
# Default: /api-explorer
pyramid_swagger.swagger_ui_path= /api-explorer

# Disable Swagger UI serving
# Default: False
pyramid_swagger.swagger_ui_disable = true

# Swagger UI <script> generator function that allows you to manipulate
# the default bootstrap process
# Default: pyramid_swagger.api:swagger_ui_script_template
pyramid_swagger.swagger_ui_script_generator = your_package.foo:callable_name

.. note::

``pyramid_swawgger`` uses a ``bravado_core.spec.Spec`` instance for handling swagger related details.
``pyramid_swagger`` uses a ``bravado_core.spec.Spec`` instance for handling swagger related details.
You can set `bravado-core config values <http://bravado-core.readthedocs.io/en/stable/config.html>`_ by adding a ``bravado-core.`` prefix to them.


Expand Down
34 changes: 34 additions & 0 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,37 @@ Once you have defined your own renderer you have to wrap the new renderer in ``P
.. code-block:: python

config.add_renderer(name='custom_renderer', factory=PyramidSwaggerRendererFactory(MyPersonalRendererFactory))


---------------------------------------
How to handle swagger responses as JSON
---------------------------------------

You might often want to serve validation errors as JSON responses in your
application, here is an example implementation.

.. code-block:: python

def exc_view(exc, request):
"""
Handle swagger errors and respond with JSON
:param exc:
:param request:
:return:
"""
error_info = exc.child
for_json = {
'message': getattr(error_info, 'message', u'{}'.format(error_info)),
'validator': getattr(error_info, 'validator', None),
'relative_schema_path': list(
getattr(error_info, 'relative_schema_path', []))[:-1],
'schema': getattr(error_info, 'schema', None),
'relative_path': list(getattr(error_info, 'relative_path', [])),
'instance': getattr(error_info, 'instance', None)
}

return for_json

config.add_exception_view(
context='pyramid_swagger.exceptions.RequestValidationError',
view=exc_view, renderer='json')
7 changes: 5 additions & 2 deletions pyramid_swagger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pyramid

from .api import build_swagger_20_swagger_schema_views
from .api import build_swagger_ui_view
from .api import register_api_doc_endpoints
from .ingest import get_swagger_schema
from .ingest import get_swagger_spec
Expand Down Expand Up @@ -50,9 +51,11 @@ def includeme(config):
register_api_doc_endpoints(
config,
settings['pyramid_swagger.schema12'].get_api_doc_endpoints())

if SWAGGER_20 in swagger_versions:
endpoints_20 = list(build_swagger_20_swagger_schema_views(config))
register_api_doc_endpoints(
config,
build_swagger_20_swagger_schema_views(config),
endpoints_20,
base_path=settings.get('pyramid_swagger.base_path_api_docs', ''))
if not settings.get('pyramid_swagger.swagger_ui_disable', False):
build_swagger_ui_view(settings, config, endpoints_20)
67 changes: 67 additions & 0 deletions pyramid_swagger/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
Module for automatically serving /api-docs* via Pyramid.
"""
import copy
import importlib
import os.path
from string import Template

import pkg_resources
import simplejson
import yaml
from bravado_core.spec import strip_xscope
from pyramid.response import Response
from six.moves.urllib.parse import urlparse
from six.moves.urllib.parse import urlunparse

Expand Down Expand Up @@ -102,6 +106,69 @@ def view_for_api_declaration(request):
return view_for_api_declaration


def build_swagger_ui_view(settings, config, endpoints, **kwargs):
"""
Create view that will serve template for swagger UI with proper
urls substituted

:param settings:
:param config:
:param swagger_json_route:
:return:
"""
# sniff out json route from endpoint list
swagger_json_route = None
for endpoint in endpoints:
if endpoint.route_name.endswith('.json'):
swagger_json_route = endpoint.route_name
if not swagger_json_route:
return

static_name = settings.get('pyramid_swagger.swagger_ui_static',
'pyramid_swagger/static')

swagger_ui_path = settings.get('pyramid_swagger.swagger_ui_path',
'/api-explorer').rstrip('/')
config.add_route('pyramid_swagger.swagger_ui_path', swagger_ui_path)
template = pkg_resources.resource_string(
'pyramid_swagger', 'static/index.html').decode('utf8')
script_generator = settings.get(
'pyramid_swagger.swagger_ui_script_generator',
'pyramid_swagger.api:swagger_ui_script_template')
package, callable = script_generator.split(':')
imported_package = importlib.import_module(package)

def swagger_ui_template_view(request):
script_callable = getattr(imported_package, callable)
html = Template(template).safe_substitute(
ui_css_url=request.static_url('pyramid_swagger:static/swagger-ui.css'),
ui_js_bundle_url=request.static_url('pyramid_swagger:static/swagger-ui-bundle.js'),
ui_js_standalone_url=request.static_url('pyramid_swagger:static/swagger-ui-standalone-preset.js'),
swagger_ui_script=script_callable(request, swagger_json_route),
)
return Response(html)

config.add_view(swagger_ui_template_view,
route_name='pyramid_swagger.swagger_ui_path')
config.add_static_view(name=static_name,
path='pyramid_swagger:static')


def swagger_ui_script_template(request, swagger_json_route, **kwargs):
"""
Generates the <script> code that bootstraps Swagger UI, it will be injected
into index template
:param request:
:param swagger_json_route:
:return:
"""
template = pkg_resources.resource_string(
'pyramid_swagger', 'static/index_script_template.html').decode('utf8')
return Template(template).safe_substitute(
swagger_spec_url=request.route_url(swagger_json_route),
)


class NodeWalker(object):
def __init__(self):
pass
Expand Down
Binary file added pyramid_swagger/static/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pyramid_swagger/static/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions pyramid_swagger/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.12.9/swagger-ui.css" >
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}

body {
margin:0;
background: #fafafa;
}
</style>
</head>

<body>

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>

<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
</symbol>

<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
</symbol>

<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
</symbol>

<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
</symbol>


<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
</symbol>

<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
</symbol>

</defs>
</svg>

<div id="swagger-ui"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.12.9/swagger-ui-bundle.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.12.9/swagger-ui-standalone-preset.js"> </script>
${swagger_ui_script}
</body>

</html>
21 changes: 21 additions & 0 deletions pyramid_swagger/static/index_script_template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script>
window.onload = function() {

// Build a system
const ui = SwaggerUIBundle({
url: "${swagger_spec_url}",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})

window.ui = ui
}
</script>
67 changes: 67 additions & 0 deletions pyramid_swagger/static/oauth2-redirect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<!doctype html>
<html lang="en-US">
<body onload="run()">
</body>
</html>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;

if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}

arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value)
}
) : {}

isValid = qp.state === sentState

if ((
oauth2.auth.schema.get("flow") === "accessCode"||
oauth2.auth.schema.get("flow") === "authorizationCode"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}

if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}

oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
</script>
2 changes: 2 additions & 0 deletions pyramid_swagger/tween.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
DEFAULT_EXCLUDED_PATHS = [
r'^/static/?',
r'^/api-docs/?',
r'^/pyramid_swagger/static/?',
r'^/api-explorer?',
r'^/swagger.(json|yaml)',
]

Expand Down
Loading