Merge pull request #1930 from davcamer/drf-yasg

Use drf_yasg to generate swagger
This commit is contained in:
Jeremy Stretch 2018-03-21 15:43:05 -04:00 committed by GitHub
commit 05b5609d86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 133 additions and 7 deletions

View File

@ -6,6 +6,9 @@ from django.conf import settings
from django.db import transaction from django.db import transaction
from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from drf_yasg import openapi
from drf_yasg.openapi import Parameter
from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin from rest_framework.mixins import ListModelMixin
from rest_framework.response import Response from rest_framework.response import Response
@ -418,14 +421,20 @@ class ConnectedDeviceViewSet(ViewSet):
* `peer-interface`: The name of the peer interface * `peer-interface`: The name of the peer interface
""" """
permission_classes = [IsAuthenticatedOrLoginNotRequired] permission_classes = [IsAuthenticatedOrLoginNotRequired]
_device_param = Parameter('peer-device', 'query',
description='The name of the peer device', required=True, type=openapi.TYPE_STRING)
_interface_param = Parameter('peer-interface', 'query',
description='The name of the peer interface', required=True, type=openapi.TYPE_STRING)
def get_view_name(self): def get_view_name(self):
return "Connected Device Locator" return "Connected Device Locator"
@swagger_auto_schema(
manual_parameters=[_device_param, _interface_param], responses={'200': serializers.DeviceSerializer})
def list(self, request): def list(self, request):
peer_device_name = request.query_params.get('peer-device') peer_device_name = request.query_params.get(self._device_param.name)
peer_interface_name = request.query_params.get('peer-interface') peer_interface_name = request.query_params.get(self._interface_param.name)
if not peer_device_name or not peer_interface_name: if not peer_device_name or not peer_interface_name:
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.') raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')

View File

@ -133,7 +133,6 @@ INSTALLED_APPS = (
'django_tables2', 'django_tables2',
'mptt', 'mptt',
'rest_framework', 'rest_framework',
'rest_framework_swagger',
'timezone_field', 'timezone_field',
'circuits', 'circuits',
'dcim', 'dcim',
@ -144,6 +143,7 @@ INSTALLED_APPS = (
'users', 'users',
'utilities', 'utilities',
'virtualization', 'virtualization',
'drf_yasg',
) )
# Middleware # Middleware
@ -246,6 +246,32 @@ REST_FRAMEWORK = {
'VIEW_NAME_FUNCTION': 'netbox.api.get_view_name', 'VIEW_NAME_FUNCTION': 'netbox.api.get_view_name',
} }
# drf_yasg settings for Swagger
SWAGGER_SETTINGS = {
'DEFAULT_FIELD_INSPECTORS': [
'utilities.custom_inspectors.NullableBooleanFieldInspector',
'utilities.custom_inspectors.CustomChoiceFieldInspector',
'drf_yasg.inspectors.CamelCaseJSONFilter',
'drf_yasg.inspectors.ReferencingSerializerInspector',
'drf_yasg.inspectors.RelatedFieldInspector',
'drf_yasg.inspectors.ChoiceFieldInspector',
'drf_yasg.inspectors.FileFieldInspector',
'drf_yasg.inspectors.DictFieldInspector',
'drf_yasg.inspectors.SimpleFieldInspector',
'drf_yasg.inspectors.StringDefaultFieldInspector',
],
'DEFAULT_FILTER_INSPECTORS': [
'utilities.custom_inspectors.IdInFilterInspector',
'drf_yasg.inspectors.CoreAPICompatInspector',
],
'DEFAULT_PAGINATOR_INSPECTORS': [
'utilities.custom_inspectors.NullablePaginatorInspector',
'drf_yasg.inspectors.DjangoRestResponsePagination',
'drf_yasg.inspectors.CoreAPICompatInspector',
]
}
# Django debug toolbar # Django debug toolbar
INTERNAL_IPS = ( INTERNAL_IPS = (
'127.0.0.1', '127.0.0.1',

View File

@ -4,12 +4,24 @@ from django.conf import settings
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.views.static import serve from django.views.static import serve
from rest_framework_swagger.views import get_swagger_view from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from netbox.views import APIRootView, HomeView, SearchView from netbox.views import APIRootView, HomeView, SearchView
from users.views import LoginView, LogoutView from users.views import LoginView, LogoutView
swagger_view = get_swagger_view(title='NetBox API') schema_view = get_schema_view(
openapi.Info(
title="NetBox API",
default_version='v2',
description="API to access NetBox",
terms_of_service="https://github.com/digitalocean/netbox",
contact=openapi.Contact(email="netbox@digitalocean.com"),
license=openapi.License(name="Apache v2 License"),
),
validators=['flex', 'ssv'],
public=True,
)
_patterns = [ _patterns = [
@ -40,7 +52,9 @@ _patterns = [
url(r'^api/secrets/', include('secrets.api.urls')), url(r'^api/secrets/', include('secrets.api.urls')),
url(r'^api/tenancy/', include('tenancy.api.urls')), url(r'^api/tenancy/', include('tenancy.api.urls')),
url(r'^api/virtualization/', include('virtualization.api.urls')), url(r'^api/virtualization/', include('virtualization.api.urls')),
url(r'^api/docs/', swagger_view, name='api_docs'), url(r'^api/docs/$', schema_view.with_ui('swagger', cache_timeout=None), name='api_docs'),
url(r'^api/redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='api_redocs'),
url(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema_swagger'),
# Serving static media in Django to pipe it through LoginRequiredMiddleware # Serving static media in Django to pipe it through LoginRequiredMiddleware
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}), url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),

View File

@ -0,0 +1,76 @@
from drf_yasg import openapi
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, FilterInspector
from rest_framework.fields import ChoiceField
from extras.api.customfields import CustomFieldsSerializer
from utilities.api import ChoiceFieldSerializer
class CustomChoiceFieldInspector(FieldInspector):
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
# this returns a callable which extracts title, description and other stuff
# https://drf-yasg.readthedocs.io/en/stable/_modules/drf_yasg/inspectors/base.html#FieldInspector._get_partial_types
SwaggerType, _ = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, ChoiceFieldSerializer):
value_schema = openapi.Schema(type=openapi.TYPE_INTEGER)
choices = list(field._choices.keys())
if set([None] + choices) == {None, True, False}:
# DeviceType.subdevice_role, Device.face and InterfaceConnection.connection_status all need to be
# differentiated since they each have subtly different values in their choice keys.
# - subdevice_role and connection_status are booleans, although subdevice_role includes None
# - face is an integer set {0, 1} which is easily confused with {False, True}
schema_type = openapi.TYPE_INTEGER
if all(type(x) == bool for x in [c for c in choices if c is not None]):
schema_type = openapi.TYPE_BOOLEAN
value_schema = openapi.Schema(type=schema_type)
value_schema['x-nullable'] = True
schema = SwaggerType(type=openapi.TYPE_OBJECT, required=["label", "value"], properties={
"label": openapi.Schema(type=openapi.TYPE_STRING),
"value": value_schema
})
return schema
elif isinstance(field, CustomFieldsSerializer):
schema = SwaggerType(type=openapi.TYPE_OBJECT)
return schema
return NotHandled
class NullableBooleanFieldInspector(FieldInspector):
def process_result(self, result, method_name, obj, **kwargs):
if isinstance(result, openapi.Schema) and isinstance(obj, ChoiceField) and result.type == 'boolean':
keys = obj.choices.keys()
if set(keys) == {None, True, False}:
result['x-nullable'] = True
result.type = 'boolean'
return result
class IdInFilterInspector(FilterInspector):
def process_result(self, result, method_name, obj, **kwargs):
if isinstance(result, list):
params = [p for p in result if isinstance(p, openapi.Parameter) and p.name == 'id__in']
for p in params:
p.type = 'string'
return result
class NullablePaginatorInspector(PaginatorInspector):
def process_result(self, result, method_name, obj, **kwargs):
if method_name == 'get_paginated_response' and isinstance(result, openapi.Schema):
next = result.properties['next']
if isinstance(next, openapi.Schema):
next['x-nullable'] = True
previous = result.properties['previous']
if isinstance(previous, openapi.Schema):
previous['x-nullable'] = True
return result

View File

@ -1,2 +1,3 @@
django-rest-swagger
psycopg2 psycopg2
pycrypto pycrypto

View File

@ -3,10 +3,10 @@ django-cors-headers>=2.1.0
django-debug-toolbar>=1.9.0 django-debug-toolbar>=1.9.0
django-filter>=1.1.0 django-filter>=1.1.0
django-mptt>=0.9.0 django-mptt>=0.9.0
django-rest-swagger>=2.1.0
django-tables2>=1.19.0 django-tables2>=1.19.0
django-timezone-field>=2.0 django-timezone-field>=2.0
djangorestframework>=3.7.7 djangorestframework>=3.7.7
drf-yasg>=1.4.4
graphviz>=0.8.2 graphviz>=0.8.2
Markdown>=2.6.11 Markdown>=2.6.11
natsort>=5.2.0 natsort>=5.2.0