Add README and documentation

openapi3
Cristi Vîjdea 2017-11-30 22:20:26 +01:00
parent ed02e3c3a1
commit 207973ae5a
18 changed files with 387 additions and 58 deletions

View File

@ -1,4 +1,4 @@
include README.md
include LICENSE
recursive-include drf_swagger/static *
recursive-include drf_swagger/templates *
recursive-include drf_swagger/templates *

288
README.md
View File

@ -1 +1,287 @@
# drf-swagger
# drf-swagger - Yet another Swagger generator for Django Rest Framework
_**WARNING**: this project is under rapid development; the APIs described here might change at any time without notice_
## Background
`OpenAPI 2.0`, 'formerly known as' `Swagger`, is a format designed to encode information about a Web API into an
easily parsable schema that can then be used for rendering documentation, generating code, etc.
More details are available on [swagger.io](https://swagger.io/) and on the
[OpenAPI 2.0 specification page](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md).
From here on, the terms "OpenAPI" and "Swagger" are used interchangeably.
#### Swagger in Django Rest Framework
Since Django Rest 3.7, there is now [built in support](http://www.django-rest-framework.org/api-guide/schemas/) for
automatic OpenAPI (Swagger) 2.0 schema generation. However, this generation is based on the
[coreapi](http://www.coreapi.org/) standard, which for the moment is vastly inferior to OpenAPI in both support and
features. In particular, the OpenAPI codec/compatibility layer provided has a few major problems:
* there is no support for documenting response schemas and status codes
* nested schemas do not work properly
* does not handle more complex fields such as `FileField`, `ChoiceField`, ...
In short this makes the generated schema unusable for code generation, and mediocre at best for documentation.
#### Third-party libraries
There are currently two decent Swagger schema generators that I could find for django-rest-framework:
* [django-rest-swagger](https://github.com/marcgibbons/django-rest-swagger)
* [drf-openapi](https://github.com/limdauto/drf_openapi)
Out of the two, `django-rest-swagger` is just a wrapper around DRF 3.7 schema generation with an added UI, and thus has
the same problems. `drf-openapi` is a bit more involved and implements some custom handling for response schemas, but
ultimately still falls short in code generation because the responses are plain `object`s.
Both projects are also relatively dead and stagnant.
## Table of contents
<!-- toc -->
- [Design](#design)
* [Aim](#aim)
* [Implementation progress](#implementation-progress)
+ [Features](#features)
- [Usage](#usage)
* [1. Quickstart](#1-quickstart)
* [2. Configuration](#2-configuration)
- [a. `get_schema_view` parameters](#a-get_schema_view-parameters)
- [b. `SchemaView` instantiators](#b-schemaview-instantiators)
- [c. `SWAGGER_SETTINGS` and `REDOC_SETTINGS`](#c-swagger_settings-and-redoc_settings)
* [3. More customization](#3-more-customization)
* [4. Caching](#4-caching)
* [5. Validation](#5-validation)
+ [`swagger-ui` validation badge](#swagger-ui-validation-badge)
- [Online](#online)
- [Offline](#offline)
+ [using `swagger-cli`](#using-swagger-cli)
+ [manually, on `editor.swagger.io`](#manually-on-editorswaggerio)
- [Planned feature support](#planned-feature-support)
<!-- tocstop -->
## Design
### Aim
This project aims for full compatibility with Swagger/OpenAPI 2.0 code generation tools. More precisely, this means:
* support documentation of response schemas for multiple possible status codes
* support unbounded schema nesting
* generate real OpenAPI Schema definitions instead of inline models
* allow easy and pluggable manual overrides for all schema components
### Implementation progress
For the first release, most of `django-rest-swagger`'s functionality is implemented. A lot of inspiration and code was
drawn from the existing implementations mentioned above, so thanks are due to their respective authors.
#### Features
* schema generation is a wrapper around coreapi & drf
* bundled latest version of [swagger-ui](https://github.com/swagger-api/swagger-ui) and
[redoc](https://github.com/Rebilly/ReDoc)
* supports dumping of schema in JSON and YAML
* schema view is cacheable out of the box
* generated Swagger schema can be automatically validated by [flex](https://github.com/pipermerriam/flex) or
[swagger-spec-validator](https://github.com/Yelp/swagger_spec_validator)
![Swagger UI](/screenshots/snippets-swagger-ui.png?raw=true "Snippets API in Swagger UI")
![ReDoc](/screenshots/snippets-redoc.png?raw=true "Snippets API in ReDoc")
## Usage
### 1. Quickstart
In `settings.py`:
```python
INSTALLED_APPS = [
...
'drf_swagger',
...
]
```
In `urls.py`:
```python
...
from drf_swagger.views import get_schema_view
from drf_swagger import openapi
...
schema_view = get_schema_view(
openapi.Info(
title="Snippets API",
default_version='v1',
description="Test description",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="contact@snippets.local"),
license=openapi.License(name="BSD License"),
),
validators=['flex', 'ssv'],
public=False,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'),
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
...
]
```
You've just created:
* A JSON view of your schema at `/swagger.json`
* A YAML view of your schema at `/swagger.yaml`
* A swagger-ui view of your schema at `/swagger/`
* A ReDoc view of your schema at `/redoc/`
### 2. Configuration
##### a. `get_schema_view` parameters
* `info` - Required. Swagger API Info object
* `url` - API base url; if left blank will be deduced from the location the view is served at
* `patterns` - passed to SchemaGenerator
* `urlconf` - passed to SchemaGenerator
* `public` - if False, includes only endpoints the current user has access to
* `validators` - a list of validator names to apply on the generated schema; allowed values are `flex`, `ssv`
* `authentication_classes` - authentication classes for the schema view itself
* `permission_classes` - permission classes for the schema view itself
##### b. `SchemaView` instantiators
* `SchemaView.with_ui(renderer, ...)` - get a view instance using the specified UI renderer; one of `swagger`, `redoc`
* `SchemaView.without_ui(...)` - get a view instance with no UI renderer; same as `as_cached_view` with no kwargs
* `SchemaView.as_cached_view(...)` - same as `as_view`, but with optional caching
* you can, of course, call `as_view` as usual
All of the first 3 methods take two optional arguments, `cache_timeout` and `cache_kwargs`; if present, these are
passed on to Django's `cached_page` decorator in order to enable caching on the resulting viewl.
See [4. Caching](#4-caching).
##### c. `SWAGGER_SETTINGS` and `REDOC_SETTINGS`
Additionally, you can include some more settings in your `settings.py` file.
The possible settings and their default values are as follows:
```python
SWAGGER_SETTINGS = {
'USE_SESSION_AUTH': True, # add Django Login and Django Logout buttons, CSRF token to swagger UI page
'LOGIN_URL': getattr(django.conf.settings, 'LOGIN_URL', None), # URL for the login button
'LOGOUT_URL': getattr(django.conf.settings, 'LOGOUT_URL', None), # URL for the logout button
# Swagger security definitions to include in the schema;
# see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-definitions-object
'SECURITY_DEFINITIONS': {
'basic': {
'type': 'basic'
}
},
# url to an external Swagger validation service; defaults to 'http://online.swagger.io/validator/'
# set to None to disable the schema validation badge in the UI
'VALIDATOR_URL': '',
# swagger-ui configuration settings, see https://github.com/swagger-api/swagger-ui#parameters of the same name
'OPERATIONS_SORTER': None,
'TAGS_SORTER': None,
'DOC_EXPANSION': 'list',
'DEEP_LINKING': False,
'SHOW_EXTENSIONS': True,
'DEFAULT_MODEL_RENDERING': 'model',
'DEFAULT_MODEL_DEPTH': 2,
}
```
```python
REDOC_SETTINGS = {
# ReDoc UI configuration settings, see https://github.com/Rebilly/ReDoc#redoc-tag-attributes
'LAZY_RENDERING': True,
'HIDE_HOSTNAME': False,
'EXPAND_RESPONSES': 'all',
'PATH_IN_MIDDLE': False,
}
```
### 3. More customization
Should you have need of more fine-grained customization over the schema view and generation, you are on your own to
figure out where you need to subclass and plug your functionality. Here are a few high-level hints:
* `OpenAPISchemaGenerator` enumerates all the API endpoints registered in Django Rest Framework, inspects their view
classes and generates an appropriate `Swagger` object describing the API structure
* `SchemaView` gets a `drf_swagger.openapi.Swagger` schema object from a generator and renders it into an HTTP response
* you can subclass `SchemaView` by extending the return value of `get_schema_view`, e.g.:
```python
SchemaView = get_schema_view(info, ...)
class CustomSchemaView(SchemaView):
generator_class = CustomSchemaGenerator
renderer_classes = (CustomRenderer1, CustomRenderer2,)
```
* `drf_swagger.renderers` take a `Swagger` object and transform it into an OpenAPI 2.0 specification document
using `OpenAPICodecJson`, `OpenAPICodecYaml`, or into a web interface using an OpenAPI renderer library.
* `drf_swagger.codecs` take a `Swagger` object and encode it in an exportable format (json or yaml by default).
### 4. Caching
Since the schema does not usually change during the lifetime of the django process, there is out of the box support
for caching the schema view in-memory, with some sane defaults:
* caching is enabled by the [`cache_page`](https://docs.djangoproject.com/en/1.11/topics/cache/#the-per-view-cache)
decorator, using the default Django cache backend but this can be changed using the `cache_kwargs` argument
* HTTP caching of the response is blocked to avoid confusing situations caused by being served stale schemas
* the cached schema varies on the `Cookie` and `Authorization` HTTP headers to enable filtering of visible endpoints
according to the authentication credentials of each user; note that this means that every user accessing the schema
will have a separate schema cached in memory.
### 5. Validation
Given the numerous methods to manually customzie the generated schema, it makes sense to validate the result to ensure
it still conforms to OpenAPI 2.0. To this end, validation is provided at the generation point using python swagger
libraries, and can be activated by passing `validators=['flex', 'ssv']` to get_schema_view; if the generated schema
is not valid, a `SwaggerValidationError` is raised by the handling codec and nothing is returned.
**Warning:** This internal validation is quite slow and can be a DOS vector if left activated on a publicly available view.
Caching can mitigate the speed impact of validation on restricted views.
The provided validation will catch syntactic errors, but more subtle violations of the spec might slip by them. To
ensure compatibility with code generation tools, it is recommended to also employ one or more of the following methods:
- #### `swagger-ui` validation badge
##### Online
If your schema is publicly accessible, `swagger-ui` will automatically validate it against the official swagger
online validator and display the result in the bottom-right validation badge.
##### Offline
If your schema is not accessible from the internet, you can run a local copy of
[swagger-validator](https://hub.docker.com/r/swaggerapi/swagger-validator/) and set the `VALIDATOR_URL` accordingly:
```python
SWAGGER_SETTINGS = {
...
'VALIDATOR_URL': 'http://localhost:8189',
...
}
```
```bash
$ docker run --name swagger-validator -d -p 8189:8080 --add-host test.local:10.0.75.1 swaggerapi/swagger-validator
84dabd52ba967c32ae6b660934fa6a429ca6bc9e594d56e822a858b57039c8a2
$ curl http://localhost:8189/debug?url=http://test.local:8002/swagger/?format=openapi
{}
```
- #### using `swagger-cli`
[https://www.npmjs.com/package/swagger-cli](https://www.npmjs.com/package/swagger-cli)
```bash
$ npm install -g swagger-cli
[...]
$ swagger-cli validate http://test.local:8002/swagger.yaml
http://test.local:8002/swagger.yaml is valid
```
- #### manually, on `editor.swagger.io`
Importing the generated spec into [https://editor.swagger.io/](https://editor.swagger.io/) will automatically
trigger validation on it.
## Planned feature support
* **OpenAPI 3.0** - if I get 2.0 working like I want, and it's not too hard to adapt to 3.0

View File

@ -10,8 +10,8 @@ SWAGGER_DEFAULTS = {
},
'LOGIN_URL': getattr(settings, 'LOGIN_URL', None),
'LOGOUT_URL': getattr(settings, 'LOGOUT_URL', None),
'VALIDATOR_URL': '',
'OPERATIONS_SORTER': None,
'TAGS_SORTER': None,
'DOC_EXPANSION': 'list',

View File

@ -5,6 +5,13 @@ import coreapi
class Contact(object):
"""Swagger Contact object
At least one of the following fields is required:
:param str name: contact name
:param str url: contact url
:param str email: contact e-mail
"""
def __init__(self, name=None, url=None, email=None):
self.name = name
self.url = url
@ -25,6 +32,11 @@ class Contact(object):
class License(object):
"""Swagger License object
:param str name: Requird. License name
:param str url: link to detailed license information
"""
def __init__(self, name, url=None):
self.name = name
self.url = url
@ -41,6 +53,15 @@ class License(object):
class Info(object):
"""Swagger Info object
:param str title: Required. API title.
:param str default_version: Required. API version string (not to be confused with Swagger spec version)
:param str description: API description; markdown supported
:param str terms_of_service: API terms of service; should be a URL
:param Contact contact: contact object
:param License license: license object
"""
def __init__(self, title, default_version, description=None, terms_of_service=None, contact=None, license=None):
if title is None or default_version is None:
raise ValueError("title and version are required for Swagger info object")

View File

@ -18,7 +18,6 @@
</style>
</head>
<body>
<script id="redoc-settings">{{ swagger_settings | safe }}</script>
<script>
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
var specURL = currentPath + '?format=openapi';

View File

@ -159,7 +159,7 @@
}
window.ui = SwaggerUIBundle(swaggerConfig);
insertionQ('.auth-wrapper .authorize').every(function (element) {
insertionQ('.auth-wrapper .authorize').every(function () {
var authWrapper = document.querySelector('.auth-wrapper');
var authorizeButton = document.querySelector('.auth-wrapper .authorize');
var djangoSessionAuth = document.querySelector('#django-session-auth');

View File

@ -14,12 +14,9 @@ from .generators import OpenAPISchemaGenerator
from .renderers import (
SwaggerJSONRenderer, SwaggerYAMLRenderer, SwaggerUIRenderer, ReDocRenderer, OpenAPIRenderer,
)
from .openapi import Info
SPEC_RENDERERS = (SwaggerYAMLRenderer, SwaggerJSONRenderer, OpenAPIRenderer)
SPEC_RENDERERS = {
False: tuple(renderer.with_validators([]) for renderer in SPEC_RENDERERS),
True: SPEC_RENDERERS,
}
UI_RENDERERS = {
'swagger': (SwaggerUIRenderer, ReDocRenderer),
'redoc': (ReDocRenderer, SwaggerUIRenderer),
@ -50,41 +47,57 @@ def deferred_never_cache(view_func):
return _wrapped_view_func
def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False, validate=False,
def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False, validators=None,
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
"""
Create a SchemaView class with default renderers and generators.
:param Info info: Required. Swagger API Info object
:param str url: API base url; if left blank will be deduced from the location the view is served at
:param str patterns: passed to SchemaGenerator
:param str urlconf: passed to SchemaGenerator
:param bool public: if False, includes only endpoints the current user has access to
:param list validators: a list of validator names to apply on the generated schema; allowed values are `flex`, `ssv`
:param tuple authentication_classes: authentication classes for the schema view itself
:param tuple permission_classes: permission classes for the schema view itself
:return: SchemaView class
"""
_auth_classes = authentication_classes
_perm_classes = permission_classes
_public = public
validators = validators or []
_spec_renderers = tuple(renderer.with_validators(validators) for renderer in SPEC_RENDERERS)
class SchemaView(APIView):
_ignore_model_permissions = True
schema = None # exclude from schema
public = _public
generator_class = OpenAPISchemaGenerator
authentication_classes = _auth_classes
permission_classes = _perm_classes
renderer_classes = SPEC_RENDERERS[bool(validate)]
def __init__(self, **kwargs):
super(SchemaView, self).__init__(**kwargs)
if self.renderer_classes is None:
if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES:
self.renderer_classes = [
renderers.CoreJSONRenderer,
renderers.BrowsableAPIRenderer,
]
else:
self.renderer_classes = [renderers.CoreJSONRenderer]
renderer_classes = _spec_renderers
def get(self, request, version='', format=None):
generator = OpenAPISchemaGenerator(info, version, url, patterns, urlconf)
generator = self.generator_class(info, version, url, patterns, urlconf)
schema = generator.get_schema(request, self.public)
if schema is None:
raise exceptions.PermissionDenied()
return Response(schema)
@classmethod
def _cached(cls, view, cache_timeout, cache_kwargs):
def as_cached_view(cls, cache_timeout=0, cache_kwargs=None, **initkwargs):
"""
Calls .as_view() and wraps the result in a cache_page decorator.
See https://docs.djangoproject.com/en/1.11/topics/cache/
:param int cache_timeout: same as cache_page; set to 0 for no cache
:param dict cache_kwargs: dictionary of kwargs to be passed to cache_page
:param initkwargs: kwargs for .as_view()
:return: a view instance
"""
cache_kwargs = cache_kwargs or {}
view = cls.as_view(**initkwargs)
if cache_timeout != 0:
view = vary_on_headers('Cookie', 'Authorization', 'Accept')(view)
view = cache_page(cache_timeout, **cache_kwargs)(view)
@ -94,20 +107,31 @@ def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False
return view
@classmethod
def as_cached_view(cls, cache_timeout=0, **cache_kwargs):
return cls._cached(cls.as_view(), cache_timeout, cache_kwargs)
def without_ui(cls, cache_timeout=0, cache_kwargs=None):
"""
Instantiate this view with just JSON and YAML renderers, optionally wrapped with cache_page.
See https://docs.djangoproject.com/en/1.11/topics/cache/.
:param int cache_timeout: same as cache_page; set to 0 for no cache
:param dict cache_kwargs: dictionary of kwargs to be passed to cache_page
:return: a view instance
"""
return cls.as_cached_view(cache_timeout, cache_kwargs, renderer_classes=_spec_renderers)
@classmethod
def without_ui(cls, cache_timeout=0, **cache_kwargs):
renderer_classes = SPEC_RENDERERS[bool(validate)]
return cls._cached(cls.as_view(renderer_classes=renderer_classes), cache_timeout, cache_kwargs)
def with_ui(cls, renderer='swagger', cache_timeout=0, cache_kwargs=None):
"""
Instantiate this view with a Web UI renderer, optionally wrapped with cache_page.
See https://docs.djangoproject.com/en/1.11/topics/cache/.
@classmethod
def with_ui(cls, renderer='swagger', cache_timeout=0, **cache_kwargs):
:param str renderer: UI renderer; allowed values are `swagger`, `redoc`
:param int cache_timeout: same as cache_page; set to 0 for no cache
:param dict cache_kwargs: dictionary of kwargs to be passed to cache_page
:return: a view instance
"""
assert renderer in UI_RENDERERS, "supported default renderers are " + ", ".join(UI_RENDERERS)
renderer_classes = (*UI_RENDERERS[renderer], *SPEC_RENDERERS[bool(validate)])
renderer_classes = (*UI_RENDERERS[renderer], *_spec_renderers)
view = cls.as_view(renderer_classes=renderer_classes)
return cls._cached(view, cache_timeout, cache_kwargs)
return cls.as_cached_view(cache_timeout, cache_kwargs, renderer_classes=renderer_classes)
return SchemaView

View File

@ -1,7 +1,6 @@
{
"name": "drf-swagger",
"dependencies": {
"redoc": "^2.0.0-alpha.4",
"swagger-ui-dist": "^3.5.0"
}
}

View File

@ -2,4 +2,4 @@ djangorestframework>=3.7.3
django>=1.11.7
coreapi>=2.3.3
openapi_codec>=1.3.2
ruamel.yaml>=0.15.34
ruamel.yaml>=0.15.34

View File

@ -0,0 +1,2 @@
pygments>=2.2.0
django-cors-headers>=2.1.0

View File

@ -1 +0,0 @@
pygments>=2.2.0

View File

@ -1,2 +1,2 @@
flex>=6.11.1
swagger-spec-validator>=2.1.0
swagger-spec-validator>=2.1.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -10,13 +10,16 @@ def read_req(req_file):
requirements = read_req('requirements.txt')
requirements_validation = read_req('requirements_validation.txt')
requirements_dev = read_req('requirements_dev.txt')
requirements_test = read_req('requirements_test.txt')
0
setup(
name='drf-swagger',
version='1.0.0rc1',
packages=find_packages(include=['drf_swagger']),
include_package_data=True,
install_requires=requirements,
tests_require=requirements_test,
extras_require={
'validation': requirements_validation
},

View File

@ -24,11 +24,6 @@ class SnippetSerializer(serializers.Serializer):
lines = serializers.ListField(child=serializers.IntegerField(), allow_empty=True, allow_null=True, required=False)
example_projects = serializers.ListSerializer(child=ExampleProjectsSerializer())
class Meta:
error_status_codes = {
HTTP_400_BAD_REQUEST: 'Bad Request'
}
def create(self, validated_data):
"""
Create and return a new `Snippet` instance, given the validated data.

View File

@ -3,7 +3,6 @@ import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
@ -13,8 +12,10 @@ SECRET_KEY = '!z1yj(9uz)zk0gg@5--j)bc4h^i!8))r^dezco8glf190e0&#p'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = [
'test.local'
]
CORS_ORIGIN_ALLOW_ALL = True
# Application definition
@ -26,12 +27,14 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'drf_swagger',
'snippets'
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@ -61,7 +64,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'testproj.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
@ -72,7 +74,6 @@ DATABASES = {
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
@ -91,6 +92,11 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
SWAGGER_SETTINGS = {
'LOGIN_URL': '/admin/login',
'LOGOUT_URL': '/admin/logout',
'VALIDATOR_URL': 'http://localhost:8189',
}
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
@ -105,12 +111,7 @@ USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning'
}

View File

@ -10,11 +10,11 @@ schema_view = get_schema_view(
title="Snippets API",
default_version='v1',
description="Test description",
terms_of_service="*Some TOS*",
contact=openapi.Contact(email="cristi@cvjd.me"),
license=openapi.License("BSD License"),
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="contact@snippets.local"),
license=openapi.License(name="BSD License"),
),
validate=True,
validators=['flex', 'ssv'],
public=False,
permission_classes=(permissions.AllowAny,),
)