From 74baf11e0b9ea28fd31bf78aa9219ee58e482fb6 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Sun, 11 Mar 2018 20:12:53 +0100 Subject: [PATCH 1/2] Swagger UI: add support for embedded Swagger UI API explorer --- MANIFEST.in | 1 + docs/configuration.rst | 20 ++++- pyramid_swagger/__init__.py | 7 +- pyramid_swagger/api.py | 67 ++++++++++++++++ pyramid_swagger/static/favicon-16x16.png | Bin 0 -> 445 bytes pyramid_swagger/static/favicon-32x32.png | Bin 0 -> 1141 bytes pyramid_swagger/static/index.html | 75 ++++++++++++++++++ .../static/index_script_template.html | 21 +++++ pyramid_swagger/static/oauth2-redirect.html | 67 ++++++++++++++++ pyramid_swagger/tween.py | 2 + tests/acceptance/swagger_ui_test.py | 51 ++++++++++++ 11 files changed, 307 insertions(+), 4 deletions(-) create mode 100644 pyramid_swagger/static/favicon-16x16.png create mode 100644 pyramid_swagger/static/favicon-32x32.png create mode 100644 pyramid_swagger/static/index.html create mode 100644 pyramid_swagger/static/index_script_template.html create mode 100644 pyramid_swagger/static/oauth2-redirect.html create mode 100644 tests/acceptance/swagger_ui_test.py diff --git a/MANIFEST.in b/MANIFEST.in index 421f7d2..c94c6cf 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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/* diff --git a/docs/configuration.rst b/docs/configuration.rst index e32aa92..3e8c904 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -62,7 +62,7 @@ A few relevant settings for your `Pyramid .ini file 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 `_ by adding a ``bravado-core.`` prefix to them. diff --git a/pyramid_swagger/__init__.py b/pyramid_swagger/__init__.py index 8ea4c67..59f7177 100644 --- a/pyramid_swagger/__init__.py +++ b/pyramid_swagger/__init__.py @@ -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 @@ -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) diff --git a/pyramid_swagger/api.py b/pyramid_swagger/api.py index 4ab8904..06ff4ae 100644 --- a/pyramid_swagger/api.py +++ b/pyramid_swagger/api.py @@ -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 @@ -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 + +${swagger_ui_script} + + + diff --git a/pyramid_swagger/static/index_script_template.html b/pyramid_swagger/static/index_script_template.html new file mode 100644 index 0000000..1bf6f4f --- /dev/null +++ b/pyramid_swagger/static/index_script_template.html @@ -0,0 +1,21 @@ + diff --git a/pyramid_swagger/static/oauth2-redirect.html b/pyramid_swagger/static/oauth2-redirect.html new file mode 100644 index 0000000..fb68399 --- /dev/null +++ b/pyramid_swagger/static/oauth2-redirect.html @@ -0,0 +1,67 @@ + + + + + + diff --git a/pyramid_swagger/tween.py b/pyramid_swagger/tween.py index b4d81d4..9d4a450 100644 --- a/pyramid_swagger/tween.py +++ b/pyramid_swagger/tween.py @@ -40,6 +40,8 @@ DEFAULT_EXCLUDED_PATHS = [ r'^/static/?', r'^/api-docs/?', + r'^/pyramid_swagger/static/?', + r'^/api-explorer?', r'^/swagger.(json|yaml)', ] diff --git a/tests/acceptance/swagger_ui_test.py b/tests/acceptance/swagger_ui_test.py new file mode 100644 index 0000000..13fe999 --- /dev/null +++ b/tests/acceptance/swagger_ui_test.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +import pytest +from webtest import TestApp as App + +from .app import main + + +@pytest.fixture +def settings(): + return { + 'pyramid_swagger.schema_directory': 'tests/sample_schemas/good_app/', + 'pyramid_swagger.enable_swagger_spec_validation': False, + } + + +def custom_script_function(request, swagger_json_route): + return '' + + +def test_api_explorer(settings): + app = App(main({}, **settings)) + response = app.get('/api-explorer', status=200) + assert response.text + + +def test_api_explorer_statics(settings): + app = App(main({}, **settings)) + response = app.get('/pyramid_swagger/static/index.html', + status=200) + assert response.text + + +def test_api_explorer_statics_location(settings): + settings['pyramid_swagger.swagger_ui_static'] = 'foo' + settings['pyramid_swagger.exclude_paths'] = '^/foo/?' + app = App(main({}, **settings)) + response = app.get('/foo/index.html', status=200) + assert response.text + + +def test_api_explorer_disabled(settings): + settings['pyramid_swagger.swagger_ui_disable'] = True + app = App(main({}, **settings)) + response = app.get('/api-explorer', status=404) + assert response.text + + +def test_api_ui_default_bootstrap(settings): + app = App(main({}, **settings)) + response = app.get('/api-explorer', status=200) + assert 'url: "http://localhost/swagger.json"' in response.text From bbbad392bfaf14d5152cd7b97e074ff2e187fdbf Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Wed, 11 Apr 2018 12:41:11 +0200 Subject: [PATCH 2/2] docs: illustrate how to server JSON error responses --- docs/quickstart.rst | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 26e5455..33929fd 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -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')