Skip to content

Commit

Permalink
introducing the spectacular sidecar
Browse files Browse the repository at this point in the history
  • Loading branch information
tfranzel committed Sep 24, 2021
1 parent 0437440 commit aeda969
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 6 deletions.
29 changes: 29 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,33 @@ specify any settings, but we recommend to specify at least some metadata.
# OTHER SETTINGS
}
Self-contained UI installation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Certain environments have no direct access to the internet and as such are unable
to retrieve Swagger UI or Redoc from CDNs. `drf-spectacular-sidecar`_ provides
the these static files as a separate optional package. Usage is as follows:

.. code:: bash
$ pip install drf-spectacular[sidecar]
.. code:: python
INSTALLED_APPS = [
# ALL YOUR APPS
'drf_spectacular',
'drf_spectacular_sidecar, # required for Django collectstatic discovery
]
SPECTACULAR_SETTINGS = {
'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
'REDOC_DIST': 'SIDECAR',
# OTHER SETTINGS
}
Release management
^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -255,6 +282,8 @@ globally, and then simply run:
.. _tox: http://tox.readthedocs.org/en/latest/

.. _drf-spectacular-sidecar: https://github.com/tfranzel/drf-spectacular-sidecar

.. |build-status-image| image:: https://api.travis-ci.com/tfranzel/drf-spectacular.svg?branch=master
:target: https://travis-ci.com/tfranzel/drf-spectacular?branch=master
.. |pypi-version| image:: https://img.shields.io/pypi/v/drf-spectacular.svg
Expand Down
29 changes: 24 additions & 5 deletions drf_spectacular/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ class SpectacularJSONAPIView(SpectacularAPIView):
renderer_classes = [OpenApiJsonRenderer, OpenApiJsonRenderer2]


def _get_sidecar_url(package):
return f'{settings.STATIC_URL}drf_spectacular_sidecar/{package}'


class SpectacularSwaggerView(APIView):
renderer_classes = [TemplateHTMLRenderer]
permission_classes = spectacular_settings.SERVE_PERMISSIONS
Expand All @@ -104,8 +108,8 @@ def get(self, request, *args, **kwargs):
return Response(
data={
'title': self.title,
'dist': spectacular_settings.SWAGGER_UI_DIST,
'favicon_href': spectacular_settings.SWAGGER_UI_FAVICON_HREF,
'dist': self._swagger_ui_dist(),
'favicon_href': self._swagger_ui_favicon(),
'schema_url': set_query_parameters(
url=schema_url,
lang=request.GET.get('lang')
Expand All @@ -120,6 +124,16 @@ def get(self, request, *args, **kwargs):
def _dump(self, data):
return data if isinstance(data, str) else json.dumps(data)

def _swagger_ui_dist(self):
if spectacular_settings.SWAGGER_UI_DIST == 'SIDECAR':
return _get_sidecar_url('swagger-ui-dist')
return spectacular_settings.SWAGGER_UI_DIST

def _swagger_ui_favicon(self):
if spectacular_settings.SWAGGER_UI_FAVICON_HREF == 'SIDECAR':
return _get_sidecar_url('swagger-ui-dist') + '/favicon-32x32.png'
return spectacular_settings.SWAGGER_UI_FAVICON_HREF


class SpectacularSwaggerSplitView(SpectacularSwaggerView):
"""
Expand Down Expand Up @@ -149,8 +163,8 @@ def get(self, request, *args, **kwargs):
return Response(
data={
'title': self.title,
'dist': spectacular_settings.SWAGGER_UI_DIST,
'favicon_href': spectacular_settings.SWAGGER_UI_FAVICON_HREF,
'dist': self._swagger_ui_dist(),
'favicon_href': self._swagger_ui_favicon(),
'script_url': set_query_parameters(
url=script_url,
lang=request.GET.get('lang'),
Expand All @@ -177,8 +191,13 @@ def get(self, request, *args, **kwargs):
return Response(
data={
'title': self.title,
'dist': spectacular_settings.REDOC_DIST,
'dist': self._redoc_dist(),
'schema_url': schema_url,
},
template_name=self.template_name
)

def _redoc_dist(self):
if spectacular_settings.REDOC_DIST == 'SIDECAR':
return _get_sidecar_url('redoc')
return spectacular_settings.SWAGGER_UI_DIST
3 changes: 2 additions & 1 deletion requirements/optionals.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ django-oauth-toolkit>=1.2.0
djangorestframework-camel-case>=1.1.2
django-filter>=2.3.0
psycopg2-binary>=2.7.3.2
drf-nested-routers>=0.93.3
drf-nested-routers>=0.93.3
drf_spectacular_sidecar
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ def get_packages(package):
include_package_data=True,
python_requires=">=3.6",
install_requires=requirements,
extras_require={
"offline": ["drf_spectacular_sidecar"],
"sidecar": ["drf_spectacular_sidecar"],
},
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
Expand Down
40 changes: 40 additions & 0 deletions tests/contrib/test_drf_spectacular_sidecar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import inspect
import os
from unittest import mock

import pytest
from django.urls import path
from rest_framework.test import APIClient

from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView

urlpatterns = [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(), name='swagger'),
path('api/schema/redoc/', SpectacularRedocView.as_view(), name='redoc'),
]

BUNDLE_URL = "static/drf_spectacular_sidecar/swagger-ui-dist/swagger-ui-bundle.js"


@mock.patch('drf_spectacular.settings.spectacular_settings.SWAGGER_UI_DIST', 'SIDECAR')
@mock.patch('drf_spectacular.settings.spectacular_settings.SWAGGER_UI_FAVICON_HREF', 'SIDECAR')
@mock.patch('drf_spectacular.settings.spectacular_settings.REDOC_DIST', 'SIDECAR')
@pytest.mark.urls(__name__)
@pytest.mark.contrib('drf_spectacular_sidecar')
def test_sidecar_shortcut_urls_are_resolved(no_warnings):
response = APIClient().get('/api/schema/swagger-ui/')
assert b'"/' + BUNDLE_URL.encode() + b'"' in response.content
assert b'"/static/drf_spectacular_sidecar/swagger-ui-dist/favicon-32x32.png"' in response.content
response = APIClient().get('/api/schema/redoc/')
assert b'"/static/drf_spectacular_sidecar/redoc/bundles/redoc.standalone.js"' in response.content


@pytest.mark.contrib('drf_spectacular_sidecar')
def test_sidecar_package_urls_matching(no_warnings):
# poor man's test to make sure the sidecar package contents match with what
# collectstatic is going to compile. cannot be tested directly.
import drf_spectacular_sidecar # type: ignore[import]
module_root = os.path.dirname(inspect.getfile(drf_spectacular_sidecar))
bundle_path = os.path.join(module_root, BUNDLE_URL)
assert os.path.isfile(bundle_path)

0 comments on commit aeda969

Please sign in to comment.