Updates
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
import json
|
||||
from collections import namedtuple
|
||||
from importlib import import_module
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from django.conf import settings
|
||||
from django.templatetags.static import static
|
||||
from django.utils import translation
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import RedirectView
|
||||
from rest_framework.renderers import TemplateHTMLRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from drf_spectacular.generators import SchemaGenerator
|
||||
from drf_spectacular.plumbing import get_relative_url, set_query_parameters
|
||||
from drf_spectacular.renderers import (
|
||||
OpenApiJsonRenderer, OpenApiJsonRenderer2, OpenApiYamlRenderer, OpenApiYamlRenderer2,
|
||||
)
|
||||
from drf_spectacular.settings import patched_settings, spectacular_settings
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
|
||||
if spectacular_settings.SERVE_INCLUDE_SCHEMA:
|
||||
SCHEMA_KWARGS: Dict[str, Any] = {'responses': {200: OpenApiTypes.OBJECT}}
|
||||
|
||||
if settings.USE_I18N:
|
||||
SCHEMA_KWARGS['parameters'] = [
|
||||
OpenApiParameter(
|
||||
'lang', str, OpenApiParameter.QUERY, enum=list(dict(settings.LANGUAGES).keys())
|
||||
)
|
||||
]
|
||||
else:
|
||||
SCHEMA_KWARGS = {'exclude': True}
|
||||
|
||||
if spectacular_settings.SERVE_AUTHENTICATION is not None:
|
||||
AUTHENTICATION_CLASSES = spectacular_settings.SERVE_AUTHENTICATION
|
||||
else:
|
||||
AUTHENTICATION_CLASSES = api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
||||
|
||||
|
||||
class SpectacularAPIView(APIView):
|
||||
__doc__ = _("""
|
||||
OpenApi3 schema for this API. Format can be selected via content negotiation.
|
||||
|
||||
- YAML: application/vnd.oai.openapi
|
||||
- JSON: application/vnd.oai.openapi+json
|
||||
""") # type: ignore
|
||||
renderer_classes = [
|
||||
OpenApiYamlRenderer, OpenApiYamlRenderer2, OpenApiJsonRenderer, OpenApiJsonRenderer2
|
||||
]
|
||||
permission_classes = spectacular_settings.SERVE_PERMISSIONS
|
||||
authentication_classes = AUTHENTICATION_CLASSES
|
||||
generator_class: Type[SchemaGenerator] = spectacular_settings.DEFAULT_GENERATOR_CLASS
|
||||
serve_public: bool = spectacular_settings.SERVE_PUBLIC
|
||||
urlconf: Optional[str] = spectacular_settings.SERVE_URLCONF
|
||||
api_version: Optional[str] = None
|
||||
custom_settings: Optional[Dict[str, Any]] = None
|
||||
patterns: Optional[List[Any]] = None
|
||||
|
||||
@extend_schema(**SCHEMA_KWARGS)
|
||||
def get(self, request, *args, **kwargs):
|
||||
# special handling of custom urlconf parameter
|
||||
if isinstance(self.urlconf, list) or isinstance(self.urlconf, tuple):
|
||||
ModuleWrapper = namedtuple('ModuleWrapper', ['urlpatterns'])
|
||||
if all(isinstance(i, str) for i in self.urlconf):
|
||||
# list of import string for urlconf
|
||||
patterns = []
|
||||
for item in self.urlconf:
|
||||
url = import_module(item)
|
||||
patterns += url.urlpatterns
|
||||
self.urlconf = ModuleWrapper(tuple(patterns))
|
||||
else:
|
||||
# explicitly resolved urlconf
|
||||
self.urlconf = ModuleWrapper(tuple(self.urlconf))
|
||||
|
||||
with patched_settings(self.custom_settings):
|
||||
if settings.USE_I18N and request.GET.get('lang'):
|
||||
with translation.override(request.GET.get('lang')):
|
||||
return self._get_schema_response(request)
|
||||
else:
|
||||
return self._get_schema_response(request)
|
||||
|
||||
def _get_schema_response(self, request):
|
||||
# version specified as parameter to the view always takes precedence. after
|
||||
# that we try to source version through the schema view's own versioning_class.
|
||||
version = self.api_version or request.version or self._get_version_parameter(request)
|
||||
generator = self.generator_class(urlconf=self.urlconf, api_version=version, patterns=self.patterns)
|
||||
return Response(
|
||||
data=generator.get_schema(request=request, public=self.serve_public),
|
||||
headers={"Content-Disposition": f'inline; filename="{self._get_filename(request, version)}"'}
|
||||
)
|
||||
|
||||
def _get_filename(self, request, version):
|
||||
return "{title}{version}.{suffix}".format(
|
||||
title=spectacular_settings.TITLE or 'schema',
|
||||
version=f' ({version})' if version else '',
|
||||
suffix=self.perform_content_negotiation(request, force=True)[0].format
|
||||
)
|
||||
|
||||
def _get_version_parameter(self, request):
|
||||
version = request.GET.get('version')
|
||||
if not api_settings.ALLOWED_VERSIONS or version in api_settings.ALLOWED_VERSIONS:
|
||||
return version
|
||||
return None
|
||||
|
||||
|
||||
class SpectacularYAMLAPIView(SpectacularAPIView):
|
||||
renderer_classes = [OpenApiYamlRenderer, OpenApiYamlRenderer2]
|
||||
|
||||
|
||||
class SpectacularJSONAPIView(SpectacularAPIView):
|
||||
renderer_classes = [OpenApiJsonRenderer, OpenApiJsonRenderer2]
|
||||
|
||||
|
||||
def _get_sidecar_url(filepath):
|
||||
return static(f'drf_spectacular_sidecar/{filepath}')
|
||||
|
||||
|
||||
class SpectacularSwaggerView(APIView):
|
||||
renderer_classes = [TemplateHTMLRenderer]
|
||||
permission_classes = spectacular_settings.SERVE_PERMISSIONS
|
||||
authentication_classes = AUTHENTICATION_CLASSES
|
||||
url_name: str = 'schema'
|
||||
url: Optional[str] = None
|
||||
template_name: str = 'drf_spectacular/swagger_ui.html'
|
||||
template_name_js: str = 'drf_spectacular/swagger_ui.js'
|
||||
title: str = spectacular_settings.TITLE
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response(
|
||||
data={
|
||||
'title': self.title,
|
||||
'swagger_ui_css': self._swagger_ui_resource('swagger-ui.css'),
|
||||
'swagger_ui_bundle': self._swagger_ui_resource('swagger-ui-bundle.js'),
|
||||
'swagger_ui_standalone': self._swagger_ui_resource('swagger-ui-standalone-preset.js'),
|
||||
'favicon_href': self._swagger_ui_favicon(),
|
||||
'schema_url': self._get_schema_url(request),
|
||||
'settings': self._dump(spectacular_settings.SWAGGER_UI_SETTINGS),
|
||||
'oauth2_config': self._dump(spectacular_settings.SWAGGER_UI_OAUTH2_CONFIG),
|
||||
'template_name_js': self.template_name_js,
|
||||
'script_url': None,
|
||||
'csrf_header_name': self._get_csrf_header_name(),
|
||||
'schema_auth_names': self._dump(self._get_schema_auth_names()),
|
||||
},
|
||||
template_name=self.template_name,
|
||||
headers={
|
||||
"Cross-Origin-Opener-Policy": "unsafe-none",
|
||||
}
|
||||
)
|
||||
|
||||
def _dump(self, data):
|
||||
return data if isinstance(data, str) else json.dumps(data, indent=2)
|
||||
|
||||
def _get_schema_url(self, request):
|
||||
schema_url = self.url or get_relative_url(reverse(self.url_name, request=request))
|
||||
return set_query_parameters(
|
||||
url=schema_url,
|
||||
lang=request.GET.get('lang'),
|
||||
version=request.GET.get('version')
|
||||
)
|
||||
|
||||
def _get_csrf_header_name(self):
|
||||
csrf_header_name = settings.CSRF_HEADER_NAME
|
||||
if csrf_header_name.startswith('HTTP_'):
|
||||
csrf_header_name = csrf_header_name[5:]
|
||||
return csrf_header_name.replace('_', '-')
|
||||
|
||||
def _get_schema_auth_names(self):
|
||||
from drf_spectacular.extensions import OpenApiAuthenticationExtension
|
||||
if spectacular_settings.SERVE_PUBLIC:
|
||||
return []
|
||||
auth_extensions = [
|
||||
OpenApiAuthenticationExtension.get_match(klass)
|
||||
for klass in self.authentication_classes
|
||||
]
|
||||
return [auth.name for auth in auth_extensions if auth]
|
||||
|
||||
@staticmethod
|
||||
def _swagger_ui_resource(filename):
|
||||
if spectacular_settings.SWAGGER_UI_DIST == 'SIDECAR':
|
||||
return _get_sidecar_url(f'swagger-ui-dist/{filename}')
|
||||
return f'{spectacular_settings.SWAGGER_UI_DIST}/{filename}'
|
||||
|
||||
@staticmethod
|
||||
def _swagger_ui_favicon():
|
||||
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):
|
||||
"""
|
||||
Alternate Swagger UI implementation that separates the html request from the
|
||||
javascript request to cater to web servers with stricter CSP policies.
|
||||
"""
|
||||
url_self: Optional[str] = None
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.GET.get('script') is not None:
|
||||
return Response(
|
||||
data={
|
||||
'schema_url': self._get_schema_url(request),
|
||||
'settings': self._dump(spectacular_settings.SWAGGER_UI_SETTINGS),
|
||||
'oauth2_config': self._dump(spectacular_settings.SWAGGER_UI_OAUTH2_CONFIG),
|
||||
'csrf_header_name': self._get_csrf_header_name(),
|
||||
'schema_auth_names': self._dump(self._get_schema_auth_names()),
|
||||
},
|
||||
template_name=self.template_name_js,
|
||||
content_type='application/javascript',
|
||||
)
|
||||
else:
|
||||
script_url = self.url_self or request.get_full_path()
|
||||
return Response(
|
||||
data={
|
||||
'title': self.title,
|
||||
'swagger_ui_css': self._swagger_ui_resource('swagger-ui.css'),
|
||||
'swagger_ui_bundle': self._swagger_ui_resource('swagger-ui-bundle.js'),
|
||||
'swagger_ui_standalone': self._swagger_ui_resource('swagger-ui-standalone-preset.js'),
|
||||
'favicon_href': self._swagger_ui_favicon(),
|
||||
'script_url': set_query_parameters(
|
||||
url=script_url,
|
||||
lang=request.GET.get('lang'),
|
||||
script='' # signal to deliver init script
|
||||
)
|
||||
},
|
||||
template_name=self.template_name,
|
||||
)
|
||||
|
||||
|
||||
class SpectacularRedocView(APIView):
|
||||
renderer_classes = [TemplateHTMLRenderer]
|
||||
permission_classes = spectacular_settings.SERVE_PERMISSIONS
|
||||
authentication_classes = AUTHENTICATION_CLASSES
|
||||
url_name: str = 'schema'
|
||||
url: Optional[str] = None
|
||||
template_name: str = 'drf_spectacular/redoc.html'
|
||||
title: Optional[str] = spectacular_settings.TITLE
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response(
|
||||
data={
|
||||
'title': self.title,
|
||||
'redoc_standalone': self._redoc_standalone(),
|
||||
'schema_url': self._get_schema_url(request),
|
||||
'settings': self._dump(spectacular_settings.REDOC_UI_SETTINGS),
|
||||
},
|
||||
template_name=self.template_name
|
||||
)
|
||||
|
||||
def _dump(self, data):
|
||||
if not data:
|
||||
return None
|
||||
elif isinstance(data, str):
|
||||
return data
|
||||
else:
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
@staticmethod
|
||||
def _redoc_standalone():
|
||||
if spectacular_settings.REDOC_DIST == 'SIDECAR':
|
||||
return _get_sidecar_url('redoc/bundles/redoc.standalone.js')
|
||||
return f'{spectacular_settings.REDOC_DIST}/bundles/redoc.standalone.js'
|
||||
|
||||
def _get_schema_url(self, request):
|
||||
schema_url = self.url or get_relative_url(reverse(self.url_name, request=request))
|
||||
return set_query_parameters(
|
||||
url=schema_url,
|
||||
lang=request.GET.get('lang'),
|
||||
version=request.GET.get('version')
|
||||
)
|
||||
|
||||
|
||||
class SpectacularSwaggerOauthRedirectView(RedirectView):
|
||||
"""
|
||||
A view that serves the SwaggerUI oauth2-redirect.html file so that SwaggerUI can authenticate itself using Oauth2
|
||||
|
||||
This view should be served as ``./oauth2-redirect.html`` relative to the SwaggerUI itself.
|
||||
If that is not possible, this views absolute url can also be set via the
|
||||
``SPECTACULAR_SETTINGS.SWAGGER_UI_SETTINGS.oauth2RedirectUrl`` django settings.
|
||||
"""
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
return _get_sidecar_url("swagger-ui-dist/oauth2-redirect.html") + "?" + self.request.GET.urlencode()
|
||||
Reference in New Issue
Block a user