Rewrite schema generation (#1)
* Completeley rewritten schema generation * Added support for python 2.7 and 3.4 * Restructured testing and build configuration * Added nested request schemas This rewrite completely replaces the public interface of the django rest schema generation library, so further changes will be needed to re-enable and further extend the customization points one might want.openapi3
parent
5658910711
commit
dce00156d5
|
|
@ -0,0 +1,32 @@
|
||||||
|
[run]
|
||||||
|
source = drf_swagger
|
||||||
|
branch = True
|
||||||
|
|
||||||
|
[report]
|
||||||
|
# Regexes for lines to exclude from consideration
|
||||||
|
exclude_lines =
|
||||||
|
# Have to re-enable the standard pragma
|
||||||
|
pragma: no cover
|
||||||
|
|
||||||
|
# Don't complain about missing debug-only code:
|
||||||
|
def __repr__
|
||||||
|
if self/.debug
|
||||||
|
|
||||||
|
# Don't complain if tests don't hit defensive assertion code:
|
||||||
|
raise AssertionError
|
||||||
|
raise NotImplementedError
|
||||||
|
warnings.warn
|
||||||
|
|
||||||
|
# Don't complain if non-runnable code isn't run:
|
||||||
|
if 0:
|
||||||
|
if __name__ == .__main__.:
|
||||||
|
|
||||||
|
ignore_errors = True
|
||||||
|
precision = 0
|
||||||
|
|
||||||
|
[paths]
|
||||||
|
source =
|
||||||
|
src/drf_swagger/
|
||||||
|
.tox/*/Lib/site-packages/drf_swagger/
|
||||||
|
.tox/*/lib/*/site-packages/drf_swagger/
|
||||||
|
/home/travis/virtualenv/*/lib/*/site-packages/drf_swagger/
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="FacetManager">
|
||||||
|
<facet type="django" name="Django">
|
||||||
|
<configuration>
|
||||||
|
<option name="rootFolder" value="$MODULE_DIR$/testproj" />
|
||||||
|
<option name="settingsModule" value="testproj/settings.py" />
|
||||||
|
<option name="manageScript" value="manage.py" />
|
||||||
|
<option name="environment" value="<map/>" />
|
||||||
|
<option name="doNotUseTestRunner" value="false" />
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
|
</component>
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/testproj" isTestSource="false" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.6 (drf-swagger)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="TemplatesService">
|
||||||
|
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||||
|
<option name="TEMPLATE_FOLDERS">
|
||||||
|
<list>
|
||||||
|
<option value="$MODULE_DIR$/drf_swagger/templates" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="TestRunnerService">
|
||||||
|
<option name="projectConfiguration" value="py.test" />
|
||||||
|
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="CssUnusedSymbol" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="JSCheckFunctionSignatures" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="JSUnresolvedVariable" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="MarkdownUnresolvedFileReference" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PyAbstractClassInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PyBroadExceptionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ourVersions">
|
||||||
|
<value>
|
||||||
|
<list size="4">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="2.7" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="3.4" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="3.5" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="3.6" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyMethodMayBeStaticInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<value>
|
||||||
|
<list size="0" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="N806" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyProtectedMemberInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PyShadowingBuiltinsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredNames">
|
||||||
|
<list>
|
||||||
|
<option value="license" />
|
||||||
|
<option value="format" />
|
||||||
|
<option value="type" />
|
||||||
|
<option value="filter" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyShadowingNamesInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PyUnusedLocalInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false">
|
||||||
|
<option name="ignoreTupleUnpacking" value="true" />
|
||||||
|
<option name="ignoreLambdaParameters" value="true" />
|
||||||
|
<option name="ignoreLoopIterationVariables" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||||
|
<option name="processCode" value="true" />
|
||||||
|
<option name="processLiterals" value="true" />
|
||||||
|
<option name="processComments" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MarkdownProjectSettings">
|
||||||
|
<PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.0" maxImageWidth="0" showGitHubPageIfSynced="false" allowBrowsingInPreview="false" synchronizePreviewPosition="true" highlightPreviewType="NONE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="false" showSelectionInPreview="true">
|
||||||
|
<PanelProvider>
|
||||||
|
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.panel" providerName="Default - Swing" />
|
||||||
|
</PanelProvider>
|
||||||
|
</PreviewSettings>
|
||||||
|
<ParserSettings gitHubSyntaxChange="false">
|
||||||
|
<PegdownExtensions>
|
||||||
|
<option name="ABBREVIATIONS" value="false" />
|
||||||
|
<option name="ANCHORLINKS" value="true" />
|
||||||
|
<option name="ASIDE" value="false" />
|
||||||
|
<option name="ATXHEADERSPACE" value="true" />
|
||||||
|
<option name="AUTOLINKS" value="true" />
|
||||||
|
<option name="DEFINITIONS" value="false" />
|
||||||
|
<option name="DEFINITION_BREAK_DOUBLE_BLANK_LINE" value="false" />
|
||||||
|
<option name="FENCED_CODE_BLOCKS" value="true" />
|
||||||
|
<option name="FOOTNOTES" value="false" />
|
||||||
|
<option name="HARDWRAPS" value="false" />
|
||||||
|
<option name="HTML_DEEP_PARSER" value="false" />
|
||||||
|
<option name="INSERTED" value="false" />
|
||||||
|
<option name="QUOTES" value="false" />
|
||||||
|
<option name="RELAXEDHRULES" value="true" />
|
||||||
|
<option name="SMARTS" value="false" />
|
||||||
|
<option name="STRIKETHROUGH" value="true" />
|
||||||
|
<option name="SUBSCRIPT" value="false" />
|
||||||
|
<option name="SUPERSCRIPT" value="false" />
|
||||||
|
<option name="SUPPRESS_HTML_BLOCKS" value="false" />
|
||||||
|
<option name="SUPPRESS_INLINE_HTML" value="false" />
|
||||||
|
<option name="TABLES" value="true" />
|
||||||
|
<option name="TASKLISTITEMS" value="true" />
|
||||||
|
<option name="TOC" value="false" />
|
||||||
|
<option name="WIKILINKS" value="true" />
|
||||||
|
</PegdownExtensions>
|
||||||
|
<ParserOptions>
|
||||||
|
<option name="COMMONMARK_LISTS" value="true" />
|
||||||
|
<option name="DUMMY" value="false" />
|
||||||
|
<option name="EMOJI_SHORTCUTS" value="true" />
|
||||||
|
<option name="FLEXMARK_FRONT_MATTER" value="false" />
|
||||||
|
<option name="GFM_LOOSE_BLANK_LINE_AFTER_ITEM_PARA" value="false" />
|
||||||
|
<option name="GFM_TABLE_RENDERING" value="true" />
|
||||||
|
<option name="GITBOOK_URL_ENCODING" value="false" />
|
||||||
|
<option name="GITHUB_EMOJI_URL" value="false" />
|
||||||
|
<option name="GITHUB_LISTS" value="false" />
|
||||||
|
<option name="GITHUB_WIKI_LINKS" value="true" />
|
||||||
|
<option name="JEKYLL_FRONT_MATTER" value="false" />
|
||||||
|
<option name="SIM_TOC_BLANK_LINE_SPACER" value="true" />
|
||||||
|
</ParserOptions>
|
||||||
|
</ParserSettings>
|
||||||
|
<HtmlSettings headerTopEnabled="false" headerBottomEnabled="false" bodyTopEnabled="false" bodyBottomEnabled="false" embedUrlContent="false" addPageHeader="true">
|
||||||
|
<GeneratorProvider>
|
||||||
|
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.generator" providerName="Default Swing HTML Generator" />
|
||||||
|
</GeneratorProvider>
|
||||||
|
<headerTop />
|
||||||
|
<headerBottom />
|
||||||
|
<bodyTop />
|
||||||
|
<bodyBottom />
|
||||||
|
</HtmlSettings>
|
||||||
|
<CssSettings previewScheme="UI_SCHEME" cssUri="" isCssUriEnabled="false" isCssTextEnabled="false" isDynamicPageWidth="true">
|
||||||
|
<StylesheetProvider>
|
||||||
|
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.css" providerName="Default Swing Stylesheet" />
|
||||||
|
</StylesheetProvider>
|
||||||
|
<ScriptProviders />
|
||||||
|
<cssText />
|
||||||
|
</CssSettings>
|
||||||
|
<HtmlExportSettings updateOnSave="false" parentDir="$ProjectFileDir$" targetDir="$ProjectFileDir$" cssDir="" scriptDir="" plainHtml="false" imageDir="" copyLinkedImages="false" imageUniquifyType="0" targetExt="" useTargetExt="false" noCssNoScripts="false" linkToExportedHtml="true" exportOnSettingsChange="true" regenerateOnProjectOpen="false" />
|
||||||
|
<LinkMapSettings>
|
||||||
|
<textMaps />
|
||||||
|
</LinkMapSettings>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.6 (drf-swagger)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/drf-swagger.iml" filepath="$PROJECT_DIR$/.idea/drf-swagger.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
20
.travis.yml
20
.travis.yml
|
|
@ -1,21 +1,31 @@
|
||||||
language: python
|
language: python
|
||||||
cache: pip
|
cache: pip
|
||||||
python:
|
python:
|
||||||
|
- '2.7'
|
||||||
|
- '3.4'
|
||||||
- '3.5'
|
- '3.5'
|
||||||
- '3.6'
|
- '3.6'
|
||||||
- '3.7-dev'
|
- '3.7-dev'
|
||||||
|
|
||||||
|
env:
|
||||||
|
- DRF=3.7
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
include:
|
include:
|
||||||
- python: "3.5"
|
- python: '2.7'
|
||||||
env: TOXENV=flake8
|
env: TOXENV=flake8
|
||||||
|
- python: '3.6'
|
||||||
|
env: DRF=master
|
||||||
|
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- env: TOXENV=flake8
|
- env: TOXENV=flake8
|
||||||
|
- env: DRF=master
|
||||||
|
- python: '2.7'
|
||||||
|
- python: '3.7'
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install -r requirements_dev.txt
|
- pip install -r requirements/ci.txt
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- coverage erase
|
- coverage erase
|
||||||
|
|
@ -24,4 +34,8 @@ script:
|
||||||
- tox
|
- tox
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- coveralls
|
- codecov
|
||||||
|
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
include README.md
|
include README.md
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include requirements*
|
recursive-include requirements *
|
||||||
recursive-include src/drf_swagger/static *
|
recursive-include src/drf_swagger/static *
|
||||||
recursive-include src/drf_swagger/templates *
|
recursive-include src/drf_swagger/templates *
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
djangorestframework>=3.7.3
|
|
||||||
django>=1.11.7
|
|
||||||
coreapi>=2.3.3
|
coreapi>=2.3.3
|
||||||
|
coreschema>=0.0.4
|
||||||
openapi_codec>=1.3.2
|
openapi_codec>=1.3.2
|
||||||
ruamel.yaml>=0.15.34
|
ruamel.yaml>=0.15.34
|
||||||
|
inflection>=0.3.1
|
||||||
|
future>=0.16.0
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# requirements for CI test suite
|
||||||
|
-r dev.txt
|
||||||
|
tox-travis>=0.10
|
||||||
|
codecov>=2.0.9
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# requirements for local development
|
||||||
|
tox>=2.9.1
|
||||||
|
tox-battery>=0.5
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# pytest runner + plugins
|
||||||
|
pytest-django>=3.1.2
|
||||||
|
pytest-pythonpath>=0.7.1
|
||||||
|
pytest-cov>=2.5.1
|
||||||
|
|
||||||
|
# test project requirements
|
||||||
|
Pillow>=4.3.0
|
||||||
|
pygments>=2.2.0
|
||||||
|
django-cors-headers>=2.1.0
|
||||||
|
django-filter>=1.1.0,<2.0; python_version == "2.7"
|
||||||
|
django-filter>=1.1.0; python_version >= "3.4"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# requirements for the validation feature
|
||||||
|
flex>=6.11.1
|
||||||
|
swagger-spec-validator>=2.1.0
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# Packages required for development and CI
|
|
||||||
tox>=2.9.1
|
|
||||||
tox-battery>=0.5
|
|
||||||
tox-travis>=0.10
|
|
||||||
python-coveralls>=2.9.1
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
# Packages required for running the tests
|
|
||||||
pygments>=2.2.0
|
|
||||||
django-cors-headers>=2.1.0
|
|
||||||
pytest-django>=3.1.2
|
|
||||||
pytest-pythonpath>=0.7.1
|
|
||||||
pytest-cov>=2.5.1
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Packages required for the validation feature
|
|
||||||
flex>=6.11.1
|
|
||||||
swagger-spec-validator>=2.1.0
|
|
||||||
12
setup.py
12
setup.py
|
|
@ -1,17 +1,18 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import os
|
||||||
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
def read_req(req_file):
|
def read_req(req_file):
|
||||||
with open(req_file) as req:
|
with open(os.path.join('requirements', req_file)) as req:
|
||||||
return [line for line in req.readlines() if line and not line.isspace()]
|
return [line for line in req.readlines() if line and not line.isspace()]
|
||||||
|
|
||||||
|
|
||||||
requirements = read_req('requirements.txt')
|
requirements = ['djangorestframework>=3.7.3'] + read_req('base.txt')
|
||||||
requirements_validation = read_req('requirements_validation.txt')
|
requirements_validation = read_req('validation.txt')
|
||||||
requirements_dev = read_req('requirements_dev.txt')
|
requirements_test = read_req('test.txt')
|
||||||
requirements_test = read_req('requirements_test.txt')
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='drf-swagger',
|
name='drf-swagger',
|
||||||
|
|
@ -24,7 +25,6 @@ setup(
|
||||||
extras_require={
|
extras_require={
|
||||||
'validation': requirements_validation,
|
'validation': requirements_validation,
|
||||||
'test': requirements_test,
|
'test': requirements_test,
|
||||||
'dev': requirements_dev,
|
|
||||||
},
|
},
|
||||||
license='BSD License',
|
license='BSD License',
|
||||||
description='Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code.',
|
description='Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code.',
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,42 @@
|
||||||
import json
|
import json
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from coreapi.codecs import BaseCodec
|
from coreapi.compat import force_bytes
|
||||||
from coreapi.compat import force_bytes, urlparse
|
from future.utils import raise_from
|
||||||
from drf_swagger.app_settings import swagger_settings
|
|
||||||
from openapi_codec import encode
|
|
||||||
from ruamel import yaml
|
from ruamel import yaml
|
||||||
|
|
||||||
|
from drf_swagger.app_settings import swagger_settings
|
||||||
|
from drf_swagger.errors import SwaggerValidationError
|
||||||
from . import openapi
|
from . import openapi
|
||||||
|
|
||||||
|
|
||||||
class SwaggerValidationError(Exception):
|
def _validate_flex(spec, codec):
|
||||||
def __init__(self, msg, validator_name, spec, *args) -> None:
|
|
||||||
super(SwaggerValidationError, self).__init__(msg, *args)
|
|
||||||
self.validator_name = validator_name
|
|
||||||
self.spec = spec
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.validator_name) + ": " + super(SwaggerValidationError, self).__str__()
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_flex(spec):
|
|
||||||
from flex.core import parse as validate_flex
|
from flex.core import parse as validate_flex
|
||||||
from flex.exceptions import ValidationError
|
from flex.exceptions import ValidationError
|
||||||
try:
|
try:
|
||||||
validate_flex(spec)
|
validate_flex(spec)
|
||||||
except ValidationError as ex:
|
except ValidationError as ex:
|
||||||
raise SwaggerValidationError(str(ex), 'flex', spec) from ex
|
raise_from(SwaggerValidationError(str(ex), 'flex', spec, codec), ex)
|
||||||
|
|
||||||
|
|
||||||
def _validate_swagger_spec_validator(spec):
|
def _validate_swagger_spec_validator(spec, codec):
|
||||||
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
|
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
|
||||||
from swagger_spec_validator.common import SwaggerValidationError as SSVErr
|
from swagger_spec_validator.common import SwaggerValidationError as SSVErr
|
||||||
try:
|
try:
|
||||||
validate_ssv(spec)
|
validate_ssv(spec)
|
||||||
except SSVErr as ex:
|
except SSVErr as ex:
|
||||||
raise SwaggerValidationError(str(ex), 'swagger_spec_validator', spec) from ex
|
raise_from(SwaggerValidationError(str(ex), 'swagger_spec_validator', spec, codec), ex)
|
||||||
|
|
||||||
|
|
||||||
VALIDATORS = {
|
VALIDATORS = {
|
||||||
'flex': _validate_flex,
|
'flex': _validate_flex,
|
||||||
'swagger_spec_validator': _validate_swagger_spec_validator,
|
|
||||||
'ssv': _validate_swagger_spec_validator,
|
'ssv': _validate_swagger_spec_validator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _OpenAPICodec(BaseCodec):
|
class _OpenAPICodec(object):
|
||||||
format = 'openapi'
|
format = 'openapi'
|
||||||
|
media_type = None
|
||||||
|
|
||||||
def __init__(self, validators):
|
def __init__(self, validators):
|
||||||
self._validators = validators
|
self._validators = validators
|
||||||
|
|
@ -61,43 +51,30 @@ class _OpenAPICodec(BaseCodec):
|
||||||
|
|
||||||
spec = self.generate_swagger_object(document)
|
spec = self.generate_swagger_object(document)
|
||||||
for validator in self.validators:
|
for validator in self.validators:
|
||||||
VALIDATORS[validator](spec)
|
VALIDATORS[validator](spec, self)
|
||||||
return force_bytes(self._dump_spec(spec))
|
return force_bytes(self._dump_dict(spec))
|
||||||
|
|
||||||
def _dump_spec(self, spec):
|
def encode_error(self, err):
|
||||||
return NotImplementedError("override this method")
|
return force_bytes(self._dump_dict(err))
|
||||||
|
|
||||||
|
def _dump_dict(self, spec):
|
||||||
|
raise NotImplementedError("override this method")
|
||||||
|
|
||||||
def generate_swagger_object(self, swagger):
|
def generate_swagger_object(self, swagger):
|
||||||
"""
|
"""
|
||||||
Generates root of the Swagger spec.
|
Generates the root Swagger object.
|
||||||
|
|
||||||
:param openapi.Swagger swagger:
|
:param openapi.Swagger swagger:
|
||||||
:return OrderedDict: swagger spec as dict
|
:return OrderedDict: swagger spec as dict
|
||||||
"""
|
"""
|
||||||
parsed_url = urlparse.urlparse(swagger.url)
|
swagger.security_definitions = swagger_settings.SECURITY_DEFINITIONS
|
||||||
|
return swagger
|
||||||
spec = OrderedDict()
|
|
||||||
|
|
||||||
spec['swagger'] = '2.0'
|
|
||||||
spec['info'] = swagger.info.to_swagger(swagger.version)
|
|
||||||
|
|
||||||
if parsed_url.netloc:
|
|
||||||
spec['host'] = parsed_url.netloc
|
|
||||||
if parsed_url.scheme:
|
|
||||||
spec['schemes'] = [parsed_url.scheme]
|
|
||||||
spec['basePath'] = '/'
|
|
||||||
|
|
||||||
spec['paths'] = encode._get_paths_object(swagger)
|
|
||||||
|
|
||||||
spec['securityDefinitions'] = swagger_settings.SECURITY_DEFINITIONS
|
|
||||||
|
|
||||||
return spec
|
|
||||||
|
|
||||||
|
|
||||||
class OpenAPICodecJson(_OpenAPICodec):
|
class OpenAPICodecJson(_OpenAPICodec):
|
||||||
media_type = 'application/openapi+json'
|
media_type = 'application/json'
|
||||||
|
|
||||||
def _dump_spec(self, spec):
|
def _dump_dict(self, spec):
|
||||||
return json.dumps(spec)
|
return json.dumps(spec)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -109,7 +86,7 @@ class SaneYamlDumper(yaml.SafeDumper):
|
||||||
return super(SaneYamlDumper, self).increase_indent(flow=flow, indentless=False, **kwargs)
|
return super(SaneYamlDumper, self).increase_indent(flow=flow, indentless=False, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def represent_odict(dump, mapping, flow_style=None):
|
def represent_odict(dump, mapping, flow_style=None): # pragma: no cover
|
||||||
"""https://gist.github.com/miracle2k/3184458
|
"""https://gist.github.com/miracle2k/3184458
|
||||||
Make PyYAML output an OrderedDict.
|
Make PyYAML output an OrderedDict.
|
||||||
|
|
||||||
|
|
@ -142,11 +119,11 @@ class SaneYamlDumper(yaml.SafeDumper):
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
||||||
SaneYamlDumper.add_representer(OrderedDict, SaneYamlDumper.represent_odict)
|
SaneYamlDumper.add_multi_representer(OrderedDict, SaneYamlDumper.represent_odict)
|
||||||
|
|
||||||
|
|
||||||
class OpenAPICodecYaml(_OpenAPICodec):
|
class OpenAPICodecYaml(_OpenAPICodec):
|
||||||
media_type = 'application/openapi+yaml'
|
media_type = 'application/yaml'
|
||||||
|
|
||||||
def _dump_spec(self, spec):
|
def _dump_dict(self, spec):
|
||||||
return yaml.dump(spec, Dumper=SaneYamlDumper, default_flow_style=False, encoding='utf-8')
|
return yaml.dump(spec, Dumper=SaneYamlDumper, default_flow_style=False, encoding='utf-8')
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
class SwaggerError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SwaggerValidationError(SwaggerError):
|
||||||
|
def __init__(self, msg, validator_name, spec, source_codec, *args):
|
||||||
|
super(SwaggerValidationError, self).__init__(msg, *args)
|
||||||
|
self.validator_name = validator_name
|
||||||
|
self.spec = spec
|
||||||
|
self.source_codec = source_codec
|
||||||
|
|
||||||
|
|
||||||
|
class SwaggerGenerationError(SwaggerError):
|
||||||
|
pass
|
||||||
|
|
@ -1,15 +1,118 @@
|
||||||
from rest_framework.schemas import SchemaGenerator as _SchemaGenerator
|
from collections import defaultdict
|
||||||
|
|
||||||
|
import django.db.models
|
||||||
|
import uritemplate
|
||||||
|
from coreapi.compat import force_text
|
||||||
|
from rest_framework.schemas.generators import SchemaGenerator
|
||||||
|
from rest_framework.schemas.inspectors import get_pk_description
|
||||||
|
|
||||||
|
from drf_swagger.inspectors import SwaggerAutoSchema
|
||||||
from . import openapi
|
from . import openapi
|
||||||
|
|
||||||
|
|
||||||
class OpenAPISchemaGenerator(_SchemaGenerator):
|
class OpenAPISchemaGenerator(object):
|
||||||
|
"""
|
||||||
|
This class iterates over all registered API endpoints and returns an appropriate OpenAPI 2.0 compliant schema.
|
||||||
|
Method implementations shamelessly stolen and adapted from rest_framework SchemaGenerator.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, info, version, url=None, patterns=None, urlconf=None):
|
def __init__(self, info, version, url=None, patterns=None, urlconf=None):
|
||||||
super(OpenAPISchemaGenerator, self).__init__(info.title, url, info.description, patterns, urlconf)
|
self._gen = SchemaGenerator(info.title, url, info.get('description', ''), patterns, urlconf)
|
||||||
self.info = info
|
self.info = info
|
||||||
self.version = version
|
self.version = version
|
||||||
|
self.endpoints = None
|
||||||
|
self.url = url
|
||||||
|
|
||||||
def get_schema(self, request=None, public=False):
|
def get_schema(self, request=None, public=False):
|
||||||
document = super(OpenAPISchemaGenerator, self).get_schema(request, public)
|
"""Generate an openapi.Swagger representing the API schema."""
|
||||||
swagger = openapi.Swagger.from_coreapi(document, self.info, self.version)
|
if self.endpoints is None:
|
||||||
return swagger
|
inspector = self._gen.endpoint_inspector_cls(self._gen.patterns, self._gen.urlconf)
|
||||||
|
self.endpoints = inspector.get_api_endpoints()
|
||||||
|
|
||||||
|
self.get_endpoints(None if public else request)
|
||||||
|
paths = self.get_paths()
|
||||||
|
|
||||||
|
url = self._gen.url
|
||||||
|
if not url and request is not None:
|
||||||
|
url = request.build_absolute_uri()
|
||||||
|
|
||||||
|
# distribute_links(links)
|
||||||
|
return openapi.Swagger(
|
||||||
|
info=self.info, paths=paths,
|
||||||
|
_url=url, _version=self.version,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_endpoints(self, request):
|
||||||
|
"""Generate {path: (view_class, [(method, view)]) given (path, method, callback)."""
|
||||||
|
view_paths = defaultdict(list)
|
||||||
|
view_cls = {}
|
||||||
|
for path, method, callback in self.endpoints:
|
||||||
|
view = self._gen.create_view(callback, method, request)
|
||||||
|
path = self._gen.coerce_path(path, method, view)
|
||||||
|
view_paths[path].append((method, view))
|
||||||
|
view_cls[path] = callback.cls
|
||||||
|
self.endpoints = {path: (view_cls[path], methods) for path, methods in view_paths.items()}
|
||||||
|
|
||||||
|
def get_paths(self):
|
||||||
|
if not self.endpoints:
|
||||||
|
return []
|
||||||
|
prefix = self._gen.determine_path_prefix(self.endpoints.keys())
|
||||||
|
paths = {}
|
||||||
|
|
||||||
|
for path, (view_cls, methods) in self.endpoints.items():
|
||||||
|
path_parameters = self.get_path_parameters(path, view_cls)
|
||||||
|
operations = {}
|
||||||
|
for method, view in methods:
|
||||||
|
if not self._gen.has_view_permissions(path, method, view):
|
||||||
|
continue
|
||||||
|
|
||||||
|
schema = SwaggerAutoSchema(view)
|
||||||
|
operation_keys = self._gen.get_keys(path[len(prefix):], method, view)
|
||||||
|
operations[method.lower()] = schema.get_operation(operation_keys, path, method)
|
||||||
|
|
||||||
|
paths[path] = openapi.PathItem(parameters=path_parameters, **operations)
|
||||||
|
|
||||||
|
return openapi.Paths(paths=paths)
|
||||||
|
|
||||||
|
def get_path_parameters(self, path, view_cls):
|
||||||
|
"""Return a list of Parameter instances corresponding to any templated path variables.
|
||||||
|
|
||||||
|
:param str path: templated request path
|
||||||
|
:param type view_cls: the view class associated with the path
|
||||||
|
:return list[openapi.Parameter]: path parameters
|
||||||
|
"""
|
||||||
|
parameters = []
|
||||||
|
model = getattr(getattr(view_cls, 'queryset', None), 'model', None)
|
||||||
|
|
||||||
|
for variable in uritemplate.variables(path):
|
||||||
|
pattern = None
|
||||||
|
type = openapi.TYPE_STRING
|
||||||
|
description = None
|
||||||
|
if model is not None:
|
||||||
|
# Attempt to infer a field description if possible.
|
||||||
|
try:
|
||||||
|
model_field = model._meta.get_field(variable)
|
||||||
|
except Exception:
|
||||||
|
model_field = None
|
||||||
|
|
||||||
|
if model_field is not None and model_field.help_text:
|
||||||
|
description = force_text(model_field.help_text)
|
||||||
|
elif model_field is not None and model_field.primary_key:
|
||||||
|
description = get_pk_description(model, model_field)
|
||||||
|
|
||||||
|
if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable:
|
||||||
|
pattern = view_cls.lookup_value_regex
|
||||||
|
elif isinstance(model_field, django.db.models.AutoField):
|
||||||
|
type = openapi.TYPE_INTEGER
|
||||||
|
|
||||||
|
field = openapi.Parameter(
|
||||||
|
name=variable,
|
||||||
|
required=True,
|
||||||
|
in_=openapi.IN_PATH,
|
||||||
|
type=type,
|
||||||
|
pattern=pattern,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
parameters.append(field)
|
||||||
|
|
||||||
|
return parameters
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,362 @@
|
||||||
|
import functools
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import coreschema
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
from rest_framework import serializers
|
||||||
from rest_framework.schemas import AutoSchema
|
from rest_framework.schemas import AutoSchema
|
||||||
|
from rest_framework.schemas.utils import is_list_view
|
||||||
|
|
||||||
|
from drf_swagger.errors import SwaggerGenerationError
|
||||||
|
from . import openapi
|
||||||
|
|
||||||
|
|
||||||
class SwaggerAutoSchema(AutoSchema):
|
def find_regex(regex_field):
|
||||||
pass
|
regex_validator = None
|
||||||
|
for validator in regex_field.validators:
|
||||||
|
if isinstance(validator, RegexValidator):
|
||||||
|
if regex_validator is not None:
|
||||||
|
# bail if multiple validators are found - no obvious way to choose
|
||||||
|
return None
|
||||||
|
regex_validator = validator
|
||||||
|
|
||||||
|
# regex_validator.regex should be a compiled re object...
|
||||||
|
return getattr(getattr(regex_validator, 'regex', None), 'pattern', None)
|
||||||
|
|
||||||
|
|
||||||
|
class SwaggerAutoSchema(object):
|
||||||
|
def __init__(self, view):
|
||||||
|
super(SwaggerAutoSchema, self).__init__()
|
||||||
|
self._sch = AutoSchema()
|
||||||
|
self.view = view
|
||||||
|
self._sch.view = view
|
||||||
|
|
||||||
|
def get_operation(self, operation_keys, path, method):
|
||||||
|
"""Get an Operation for the given API endpoint (path, method).
|
||||||
|
This includes query, body parameters and response schemas.
|
||||||
|
|
||||||
|
:param tuple[str] operation_keys: an array of keys describing the hierarchical layout of this view in the API;
|
||||||
|
e.g. ('snippets', 'list'), ('snippets', 'retrieve'), etc.
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:return openapi.Operation: the resulting Operation object
|
||||||
|
"""
|
||||||
|
body = self.get_request_body_parameters(path, method)
|
||||||
|
query = self.get_query_parameters(path, method)
|
||||||
|
parameters = body + query
|
||||||
|
|
||||||
|
parameters = [param for param in parameters if param is not None]
|
||||||
|
description = self.get_description(path, method)
|
||||||
|
responses = self.get_responses(path, method)
|
||||||
|
return openapi.Operation(
|
||||||
|
operation_id='_'.join(operation_keys),
|
||||||
|
description=description,
|
||||||
|
responses=responses,
|
||||||
|
parameters=parameters,
|
||||||
|
tags=[operation_keys[0]]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_request_body_parameters(self, path, method):
|
||||||
|
"""Return the request body parameters for this view.
|
||||||
|
This is either:
|
||||||
|
- a list with a single object Parameter with a Schema derived from the request serializer
|
||||||
|
- a list of primitive Parameters parsed as form data
|
||||||
|
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:return list[Parameter]: a (potentially empty) list of openapi.Parameter in: either `body` or `formData`
|
||||||
|
"""
|
||||||
|
# only PUT, PATCH or POST can have a request body
|
||||||
|
if method not in ('PUT', 'PATCH', 'POST'):
|
||||||
|
return []
|
||||||
|
|
||||||
|
serializer = self.get_request_serializer(path, method)
|
||||||
|
if serializer is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
encoding = self._sch.get_encoding(path, method)
|
||||||
|
if 'form' in encoding:
|
||||||
|
return [
|
||||||
|
self.field_to_swagger(value, openapi.Parameter, name=key, in_=openapi.IN_FORM)
|
||||||
|
for key, value
|
||||||
|
in serializer.fields.items()
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
schema = self.get_request_body_schema(path, method, serializer)
|
||||||
|
return [openapi.Parameter(name='data', in_=openapi.IN_BODY, schema=schema)]
|
||||||
|
|
||||||
|
def get_request_serializer(self, path, method):
|
||||||
|
"""Return the request serializer (used for parsing the request payload) for this endpoint.
|
||||||
|
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:return serializers.Serializer: the request serializer
|
||||||
|
"""
|
||||||
|
# TODO: only GenericAPIViews have defined serializers;
|
||||||
|
# APIViews and plain ViewSets will need some kind of manual treatment
|
||||||
|
if not hasattr(self.view, 'get_serializer'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.view.get_serializer()
|
||||||
|
|
||||||
|
def get_request_body_schema(self, path, method, serializer):
|
||||||
|
"""Return the Schema for a given request's body data. Only applies to PUT, PATCH and POST requests.
|
||||||
|
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:param serializer: the view's request serialzier
|
||||||
|
:return openapi.Schema: the request body schema
|
||||||
|
"""
|
||||||
|
return self.field_to_swagger(serializer, openapi.Schema)
|
||||||
|
|
||||||
|
def get_responses(self, path, method):
|
||||||
|
"""Get the possible responses for this view as a swagger Responses object.
|
||||||
|
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:return Responses: the documented responses
|
||||||
|
"""
|
||||||
|
response_serializers = self.get_response_serializers(path, method)
|
||||||
|
return openapi.Responses(
|
||||||
|
responses=self.get_response_schemas(path, method, response_serializers)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_response_serializers(self, path, method):
|
||||||
|
"""Return the response codes that this view is expected to return, and the serializer for each response body.
|
||||||
|
The return value should be a dict where the keys are possible status codes, and values are either strings,
|
||||||
|
`Serializer`s or `openapi.Response` objects.
|
||||||
|
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:return dict: the response serializers
|
||||||
|
"""
|
||||||
|
if method.lower() == 'post':
|
||||||
|
return {'201': ''}
|
||||||
|
if method.lower() == 'delete':
|
||||||
|
return {'204': ''}
|
||||||
|
return {'200': ''}
|
||||||
|
|
||||||
|
def get_response_schemas(self, path, method, response_serializers):
|
||||||
|
"""Return the `openapi.Response` objects calculated for this view.
|
||||||
|
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:param dict response_serializers: result of get_response_serializers
|
||||||
|
:return dict[str, openapi.Response]: a dictionary of status code to Response object
|
||||||
|
"""
|
||||||
|
responses = {}
|
||||||
|
for status, serializer in response_serializers.items():
|
||||||
|
if isinstance(serializer, str):
|
||||||
|
response = openapi.Response(
|
||||||
|
description=serializer
|
||||||
|
)
|
||||||
|
elif isinstance(serializer, openapi.Response):
|
||||||
|
response = serializer
|
||||||
|
else:
|
||||||
|
response = openapi.Response(
|
||||||
|
description='',
|
||||||
|
schema=self.field_to_swagger(serializer, openapi.Schema)
|
||||||
|
)
|
||||||
|
|
||||||
|
responses[str(status)] = response
|
||||||
|
|
||||||
|
return responses
|
||||||
|
|
||||||
|
def get_query_parameters(self, path, method):
|
||||||
|
"""Return the query parameters accepted by this view.
|
||||||
|
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:return list[openapi.Parameter]: the query parameters
|
||||||
|
"""
|
||||||
|
return self.get_filter_parameters(path, method) + self.get_pagination_parameters(path, method)
|
||||||
|
|
||||||
|
def get_filter_parameters(self, path, method):
|
||||||
|
"""Return the parameters added to the view by its filter backends.
|
||||||
|
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:return list[openapi.Parameter]: the filter query parameters
|
||||||
|
"""
|
||||||
|
if not self._sch._allows_filters(path, method):
|
||||||
|
return []
|
||||||
|
|
||||||
|
fields = []
|
||||||
|
for filter_backend in self.view.filter_backends:
|
||||||
|
filter = filter_backend()
|
||||||
|
if hasattr(filter, 'get_schema_fields'):
|
||||||
|
fields += filter.get_schema_fields(self.view)
|
||||||
|
return [self.coreapi_field_to_parameter(field) for field in fields]
|
||||||
|
|
||||||
|
def get_pagination_parameters(self, path, method):
|
||||||
|
"""Return the parameters added to the view by its paginator.
|
||||||
|
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:return list[openapi.Parameter]: the pagination query parameters
|
||||||
|
"""
|
||||||
|
if not is_list_view(path, method, self.view):
|
||||||
|
return []
|
||||||
|
|
||||||
|
paginator = getattr(self.view, 'paginator', None)
|
||||||
|
if paginator is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
self.coreapi_field_to_parameter(field)
|
||||||
|
for field in paginator.get_schema_fields(self.view)
|
||||||
|
]
|
||||||
|
|
||||||
|
def coreapi_field_to_parameter(self, field):
|
||||||
|
"""Convert an instance of `coreapi.Field` to a swagger Parameter object.
|
||||||
|
|
||||||
|
:param coreapi.Field field: the coreapi field
|
||||||
|
:return openapi.Parameter: the equivalent openapi primitive Parameter
|
||||||
|
"""
|
||||||
|
location_to_in = {
|
||||||
|
'query': openapi.IN_QUERY,
|
||||||
|
'path': openapi.IN_PATH,
|
||||||
|
'form': openapi.IN_FORM,
|
||||||
|
'body': openapi.IN_FORM,
|
||||||
|
}
|
||||||
|
coreapi_types = {
|
||||||
|
coreschema.Integer: openapi.TYPE_INTEGER,
|
||||||
|
coreschema.Number: openapi.TYPE_NUMBER,
|
||||||
|
coreschema.String: openapi.TYPE_STRING,
|
||||||
|
coreschema.Boolean: openapi.TYPE_BOOLEAN,
|
||||||
|
}
|
||||||
|
return openapi.Parameter(
|
||||||
|
name=field.name,
|
||||||
|
in_=location_to_in[field.location],
|
||||||
|
type=coreapi_types.get(field.schema.__class__, openapi.TYPE_STRING),
|
||||||
|
required=field.required,
|
||||||
|
description=field.schema.description,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_description(self, path, method):
|
||||||
|
"""Return an operation description determined as appropriate from the view's method and class docstrings.
|
||||||
|
|
||||||
|
:param str path: the view's path
|
||||||
|
:param str method: HTTP request method
|
||||||
|
:return str: the operation description
|
||||||
|
"""
|
||||||
|
return self._sch.get_description(path, method)
|
||||||
|
|
||||||
|
def field_to_swagger(self, field, swagger_object_type, **kwargs):
|
||||||
|
"""Convert a drf Serializer or Field instance into a Swagger object.
|
||||||
|
|
||||||
|
:param rest_framework.serializers.Field field: the source field
|
||||||
|
:param type swagger_object_type: should be one of Schema, Parameter, Items
|
||||||
|
:param kwargs: extra attributes for constructing the object;
|
||||||
|
if swagger_object_type is Parameter, `name` and `in_` should be provided
|
||||||
|
:return Swagger,Parameter,Items: the swagger object
|
||||||
|
"""
|
||||||
|
assert swagger_object_type in (openapi.Schema, openapi.Parameter, openapi.Items)
|
||||||
|
title = force_text(field.label) if field.label else None
|
||||||
|
title = title if swagger_object_type == openapi.Schema else None # only Schema has title
|
||||||
|
title = None
|
||||||
|
description = force_text(field.help_text) if field.help_text else None
|
||||||
|
description = description if swagger_object_type != openapi.Items else None # Items has no description either
|
||||||
|
|
||||||
|
SwaggerType = functools.partial(swagger_object_type, title=title, description=description, **kwargs)
|
||||||
|
# arrays in Schema have Schema elements, arrays in Parameter and Items have Items elements
|
||||||
|
ChildSwaggerType = openapi.Schema if swagger_object_type == openapi.Schema else openapi.Items
|
||||||
|
|
||||||
|
# ------ NESTED
|
||||||
|
if isinstance(field, (serializers.ListSerializer, serializers.ListField)):
|
||||||
|
child_schema = self.field_to_swagger(field.child, ChildSwaggerType)
|
||||||
|
return SwaggerType(
|
||||||
|
type=openapi.TYPE_ARRAY,
|
||||||
|
items=child_schema,
|
||||||
|
)
|
||||||
|
elif isinstance(field, serializers.Serializer):
|
||||||
|
if swagger_object_type != openapi.Schema:
|
||||||
|
raise SwaggerGenerationError("cannot instantiate nested serializer as "
|
||||||
|
+ swagger_object_type.__name__)
|
||||||
|
return SwaggerType(
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties=OrderedDict(
|
||||||
|
(key, self.field_to_swagger(value, ChildSwaggerType))
|
||||||
|
for key, value
|
||||||
|
in field.fields.items()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif isinstance(field, serializers.ManyRelatedField):
|
||||||
|
child_schema = self.field_to_swagger(field.child_relation, ChildSwaggerType)
|
||||||
|
return SwaggerType(
|
||||||
|
type=openapi.TYPE_ARRAY,
|
||||||
|
items=child_schema,
|
||||||
|
unique_items=True, # is this OK?
|
||||||
|
)
|
||||||
|
elif isinstance(field, serializers.RelatedField):
|
||||||
|
# TODO: infer type for PrimaryKeyRelatedField?
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING)
|
||||||
|
# ------ CHOICES
|
||||||
|
elif isinstance(field, serializers.MultipleChoiceField):
|
||||||
|
return SwaggerType(
|
||||||
|
type=openapi.TYPE_ARRAY,
|
||||||
|
items=ChildSwaggerType(
|
||||||
|
type=openapi.TYPE_STRING,
|
||||||
|
enum=list(field.choices.keys())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif isinstance(field, serializers.ChoiceField):
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys()))
|
||||||
|
# ------ BOOL
|
||||||
|
elif isinstance(field, serializers.BooleanField):
|
||||||
|
return SwaggerType(type=openapi.TYPE_BOOLEAN)
|
||||||
|
# ------ NUMERIC
|
||||||
|
elif isinstance(field, (serializers.DecimalField, serializers.FloatField)):
|
||||||
|
# TODO: min_value max_value
|
||||||
|
return SwaggerType(type=openapi.TYPE_NUMBER)
|
||||||
|
elif isinstance(field, serializers.IntegerField):
|
||||||
|
# TODO: min_value max_value
|
||||||
|
return SwaggerType(type=openapi.TYPE_INTEGER)
|
||||||
|
# ------ STRING
|
||||||
|
elif isinstance(field, serializers.EmailField):
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_EMAIL)
|
||||||
|
elif isinstance(field, serializers.RegexField):
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING, pattern=find_regex(field))
|
||||||
|
elif isinstance(field, serializers.SlugField):
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_SLUG)
|
||||||
|
elif isinstance(field, serializers.URLField):
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_URI)
|
||||||
|
elif isinstance(field, serializers.IPAddressField):
|
||||||
|
format = {'ipv4': openapi.FORMAT_IPV4, 'ipv6': openapi.FORMAT_IPV6}.get(field.protocol, None)
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING, format=format)
|
||||||
|
elif isinstance(field, serializers.CharField):
|
||||||
|
# TODO: min_length max_length (for all CharField subclasses above too)
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING)
|
||||||
|
elif isinstance(field, serializers.UUIDField):
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_UUID)
|
||||||
|
# ------ DATE & TIME
|
||||||
|
elif isinstance(field, serializers.DateField):
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATE)
|
||||||
|
elif isinstance(field, serializers.DateTimeField):
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATETIME)
|
||||||
|
# ------ OTHERS
|
||||||
|
elif isinstance(field, serializers.FileField):
|
||||||
|
# swagger 2.0 does not support specifics about file fields, so ImageFile gets no special treatment
|
||||||
|
# OpenAPI 3.0 does support it, so a future implementation could handle this better
|
||||||
|
# TODO: appropriate produces/consumes somehow/somewhere?
|
||||||
|
if swagger_object_type != openapi.Parameter:
|
||||||
|
raise SwaggerGenerationError("parameter of type file is supported only in formData Parameter")
|
||||||
|
return SwaggerType(type=openapi.TYPE_FILE)
|
||||||
|
elif isinstance(field, serializers.JSONField):
|
||||||
|
return SwaggerType(
|
||||||
|
type=openapi.TYPE_STRING,
|
||||||
|
format=openapi.FORMAT_BINARY if field.binary else None
|
||||||
|
)
|
||||||
|
elif isinstance(field, serializers.DictField) and swagger_object_type == openapi.Schema:
|
||||||
|
child_schema = self.field_to_swagger(field.child, ChildSwaggerType)
|
||||||
|
return SwaggerType(
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
additional_properties=child_schema
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO unhandled fields: TimeField DurationField HiddenField ModelField NullBooleanField?
|
||||||
|
# TODO: return info about required/allowed empty
|
||||||
|
|
||||||
|
# everything else gets string by default
|
||||||
|
return SwaggerType(type=openapi.TYPE_STRING)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
|
||||||
|
from drf_swagger.errors import SwaggerValidationError
|
||||||
|
from .codecs import _OpenAPICodec
|
||||||
|
|
||||||
|
|
||||||
|
class SwaggerExceptionMiddleware(MiddlewareMixin):
|
||||||
|
def process_exception(self, request, exception):
|
||||||
|
if isinstance(exception, SwaggerValidationError):
|
||||||
|
err = {'errors': {exception.validator_name: str(exception)}}
|
||||||
|
codec = exception.source_codec
|
||||||
|
if isinstance(codec, _OpenAPICodec):
|
||||||
|
err = codec.encode_error(err)
|
||||||
|
content_type = codec.media_type
|
||||||
|
return HttpResponse(err, status=500, content_type=content_type)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
@ -1,10 +1,105 @@
|
||||||
import warnings
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import coreapi
|
from coreapi.compat import urlparse
|
||||||
|
from future.utils import raise_from
|
||||||
|
from inflection import camelize
|
||||||
|
|
||||||
|
TYPE_OBJECT = "object"
|
||||||
|
TYPE_STRING = "string"
|
||||||
|
TYPE_NUMBER = "number"
|
||||||
|
TYPE_INTEGER = "integer"
|
||||||
|
TYPE_BOOLEAN = "boolean"
|
||||||
|
TYPE_ARRAY = "array"
|
||||||
|
TYPE_FILE = "file"
|
||||||
|
|
||||||
|
# officially supported by Swagger 2.0 spec
|
||||||
|
FORMAT_DATE = "date"
|
||||||
|
FORMAT_DATETIME = "date-time"
|
||||||
|
FORMAT_PASSWORD = "password"
|
||||||
|
FORMAT_BINARY = "binary"
|
||||||
|
FORMAT_BASE64 = "bytes"
|
||||||
|
FORMAT_FLOAT = "float"
|
||||||
|
FORMAT_DOUBLE = "double"
|
||||||
|
FORMAT_INT32 = "int32"
|
||||||
|
FORMAT_INT64 = "int64"
|
||||||
|
|
||||||
|
# defined in JSON-schema
|
||||||
|
FORMAT_EMAIL = "email"
|
||||||
|
FORMAT_IPV4 = "ipv4"
|
||||||
|
FORMAT_IPV6 = "ipv6"
|
||||||
|
FORMAT_URI = "uri"
|
||||||
|
|
||||||
|
# pulled out of my ass
|
||||||
|
FORMAT_UUID = "uuid"
|
||||||
|
FORMAT_SLUG = "slug"
|
||||||
|
|
||||||
|
IN_BODY = 'body'
|
||||||
|
IN_PATH = 'path'
|
||||||
|
IN_QUERY = 'query'
|
||||||
|
IN_FORM = 'formData'
|
||||||
|
IN_HEADER = 'header'
|
||||||
|
|
||||||
|
|
||||||
class Contact(object):
|
def make_swagger_name(attribute_name):
|
||||||
|
"""
|
||||||
|
Convert a python variable name into a Swagger spec attribute name.
|
||||||
|
|
||||||
|
In particular,
|
||||||
|
* if name starts with x_, return "x-{camelCase}"
|
||||||
|
* if name is 'ref', return "$ref"
|
||||||
|
* else return the name converted to camelCase, with trailing underscores stripped
|
||||||
|
|
||||||
|
:param str attribute_name: python attribute name
|
||||||
|
:return: swagger name
|
||||||
|
"""
|
||||||
|
if attribute_name == 'ref':
|
||||||
|
return "$ref"
|
||||||
|
if attribute_name.startswith("x_"):
|
||||||
|
return "x-" + camelize(attribute_name[2:], uppercase_first_letter=False)
|
||||||
|
return camelize(attribute_name.rstrip('_'), uppercase_first_letter=False)
|
||||||
|
|
||||||
|
|
||||||
|
class SwaggerDict(OrderedDict):
|
||||||
|
def __init__(self, **attrs):
|
||||||
|
super(SwaggerDict, self).__init__()
|
||||||
|
self._extras__ = attrs
|
||||||
|
if self.__class__ == SwaggerDict:
|
||||||
|
self._insert_extras__()
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
if key.startswith('_'):
|
||||||
|
super(SwaggerDict, self).__setattr__(key, value)
|
||||||
|
return
|
||||||
|
if value is not None:
|
||||||
|
self[make_swagger_name(key)] = value
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
if item.startswith('_'):
|
||||||
|
raise AttributeError
|
||||||
|
try:
|
||||||
|
return self[make_swagger_name(item)]
|
||||||
|
except KeyError as e:
|
||||||
|
raise_from(AttributeError("no attribute " + item), e)
|
||||||
|
|
||||||
|
def __delattr__(self, item):
|
||||||
|
if item.startswith('_'):
|
||||||
|
super(SwaggerDict, self).__delattr__(item)
|
||||||
|
return
|
||||||
|
del self[make_swagger_name(item)]
|
||||||
|
|
||||||
|
def _insert_extras__(self):
|
||||||
|
"""
|
||||||
|
From an ordering perspective, it is desired that extra attributes such as vendor extensions stay at the
|
||||||
|
bottom of the object. However, python2.7's OrderdDict craps out if you try to insert into it before calling
|
||||||
|
init. This means that subclasses must call super().__init__ as the first statement of their own __init__,
|
||||||
|
which would result in the extra attributes being added first. For this reason, we defer the insertion of the
|
||||||
|
attributes and require that subclasses call ._insert_extras__ at the end of their __init__ method.
|
||||||
|
"""
|
||||||
|
for attr, val in self._extras__.items():
|
||||||
|
setattr(self, attr, val)
|
||||||
|
|
||||||
|
|
||||||
|
class Contact(SwaggerDict):
|
||||||
"""Swagger Contact object
|
"""Swagger Contact object
|
||||||
At least one of the following fields is required:
|
At least one of the following fields is required:
|
||||||
|
|
||||||
|
|
@ -12,47 +107,34 @@ class Contact(object):
|
||||||
:param str url: contact url
|
:param str url: contact url
|
||||||
:param str email: contact e-mail
|
:param str email: contact e-mail
|
||||||
"""
|
"""
|
||||||
def __init__(self, name=None, url=None, email=None):
|
|
||||||
|
def __init__(self, name=None, url=None, email=None, **extra):
|
||||||
|
super(Contact, self).__init__(**extra)
|
||||||
|
if name is None and url is None and email is None:
|
||||||
|
raise ValueError("one of name, url or email is requires for Swagger Contact object")
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = url
|
self.url = url
|
||||||
self.email = email
|
self.email = email
|
||||||
if name is None and url is None and email is None:
|
self._insert_extras__()
|
||||||
raise ValueError("one of name, url or email is requires for Swagger Contact object")
|
|
||||||
|
|
||||||
def to_swagger(self):
|
|
||||||
contact = OrderedDict()
|
|
||||||
if self.name is not None:
|
|
||||||
contact['name'] = self.name
|
|
||||||
if self.url is not None:
|
|
||||||
contact['url'] = self.url
|
|
||||||
if self.email is not None:
|
|
||||||
contact['email'] = self.email
|
|
||||||
|
|
||||||
return contact
|
|
||||||
|
|
||||||
|
|
||||||
class License(object):
|
class License(SwaggerDict):
|
||||||
"""Swagger License object
|
"""Swagger License object
|
||||||
|
|
||||||
:param str name: Requird. License name
|
:param str name: Requird. License name
|
||||||
:param str url: link to detailed license information
|
:param str url: link to detailed license information
|
||||||
"""
|
"""
|
||||||
def __init__(self, name, url=None):
|
|
||||||
self.name = name
|
def __init__(self, name, url=None, **extra):
|
||||||
self.url = url
|
super(License, self).__init__(**extra)
|
||||||
if name is None:
|
if name is None:
|
||||||
raise ValueError("name is required for Swagger License object")
|
raise ValueError("name is required for Swagger License object")
|
||||||
|
self.name = name
|
||||||
def to_swagger(self):
|
self.url = url
|
||||||
license = OrderedDict()
|
self._insert_extras__()
|
||||||
license['name'] = self.name
|
|
||||||
if self.url is not None:
|
|
||||||
license['url'] = self.url
|
|
||||||
|
|
||||||
return license
|
|
||||||
|
|
||||||
|
|
||||||
class Info(object):
|
class Info(SwaggerDict):
|
||||||
"""Swagger Info object
|
"""Swagger Info object
|
||||||
|
|
||||||
:param str title: Required. API title.
|
:param str title: Required. API title.
|
||||||
|
|
@ -62,7 +144,10 @@ class Info(object):
|
||||||
:param Contact contact: contact object
|
:param Contact contact: contact object
|
||||||
:param License license: license object
|
:param License license: license object
|
||||||
"""
|
"""
|
||||||
def __init__(self, title, default_version, description=None, terms_of_service=None, contact=None, license=None):
|
|
||||||
|
def __init__(self, title, default_version, description=None, terms_of_service=None, contact=None, license=None,
|
||||||
|
**extra):
|
||||||
|
super(Info, self).__init__(**extra)
|
||||||
if title is None or default_version is None:
|
if title is None or default_version is None:
|
||||||
raise ValueError("title and version are required for Swagger info object")
|
raise ValueError("title and version are required for Swagger info object")
|
||||||
if contact is not None and not isinstance(contact, Contact):
|
if contact is not None and not isinstance(contact, Contact):
|
||||||
|
|
@ -70,67 +155,139 @@ class Info(object):
|
||||||
if license is not None and not isinstance(license, License):
|
if license is not None and not isinstance(license, License):
|
||||||
raise ValueError("license must be a License object")
|
raise ValueError("license must be a License object")
|
||||||
self.title = title
|
self.title = title
|
||||||
self.default_version = default_version
|
self._default_version = default_version
|
||||||
self.description = description
|
self.description = description
|
||||||
self.terms_of_service = terms_of_service
|
self.terms_of_service = terms_of_service
|
||||||
self.contact = contact
|
self.contact = contact
|
||||||
self.license = license
|
self.license = license
|
||||||
|
self._insert_extras__()
|
||||||
def to_swagger(self, version):
|
|
||||||
info = OrderedDict()
|
|
||||||
info['title'] = self.title
|
|
||||||
if self.description is not None:
|
|
||||||
info['description'] = self.description
|
|
||||||
if self.terms_of_service is not None:
|
|
||||||
info['termsOfService'] = self.terms_of_service
|
|
||||||
if self.contact is not None:
|
|
||||||
info['contact'] = self.contact.to_swagger()
|
|
||||||
if self.license is not None:
|
|
||||||
info['license'] = self.license.to_swagger()
|
|
||||||
info['version'] = version or self.default_version
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
class Swagger(coreapi.Document):
|
class Swagger(SwaggerDict):
|
||||||
@classmethod
|
def __init__(self, info=None, _url=None, _version=None, paths=None, **extra):
|
||||||
def from_coreapi(cls, document, info, version):
|
super(Swagger, self).__init__(**extra)
|
||||||
"""
|
self.swagger = '2.0'
|
||||||
Create an openapi.Swagger from the fields of a coreapi.Document.
|
self.info = info
|
||||||
|
self.info.version = _version or info._default_version
|
||||||
|
self.paths = paths
|
||||||
|
|
||||||
:param coreapi.Document document: source coreapi.Document
|
if _url:
|
||||||
:param openapi.Info info: Swagger info object
|
url = urlparse.urlparse(_url)
|
||||||
:param string version: API version string
|
if url.netloc:
|
||||||
:return: an openapi.Swagger
|
self.host = url.netloc
|
||||||
"""
|
if url.scheme:
|
||||||
if document.title and document.title != info.title:
|
self.schemes = [url.scheme]
|
||||||
warnings.warn("document title is overriden by Swagger Info")
|
|
||||||
if document.description and document.description != info.description:
|
|
||||||
warnings.warn("document description is overriden by Swagger Info")
|
|
||||||
return Swagger(
|
|
||||||
info=info,
|
|
||||||
version=version,
|
|
||||||
url=document.url,
|
|
||||||
media_type=document.media_type,
|
|
||||||
content=document.data
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, info=None, version=None, url=None, media_type=None, content=None):
|
self.base_path = '/'
|
||||||
super(Swagger, self).__init__(url, info.title, info.description, media_type, content)
|
self._insert_extras__()
|
||||||
self._info = info
|
|
||||||
self._version = version
|
|
||||||
|
|
||||||
@property
|
|
||||||
def info(self):
|
|
||||||
return self._info
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version(self):
|
|
||||||
return self._version
|
|
||||||
|
|
||||||
|
|
||||||
class Field(coreapi.Field):
|
class Paths(SwaggerDict):
|
||||||
pass
|
def __init__(self, paths, **extra):
|
||||||
|
super(Paths, self).__init__(**extra)
|
||||||
|
for path, path_obj in paths.items():
|
||||||
|
assert path.startswith("/")
|
||||||
|
if path_obj is not None:
|
||||||
|
self[path] = path_obj
|
||||||
|
self._insert_extras__()
|
||||||
|
|
||||||
|
|
||||||
class Link(coreapi.Link):
|
class PathItem(SwaggerDict):
|
||||||
pass
|
def __init__(self, get=None, put=None, post=None, delete=None, options=None,
|
||||||
|
head=None, patch=None, parameters=None, **extra):
|
||||||
|
super(PathItem, self).__init__(**extra)
|
||||||
|
self.get = get
|
||||||
|
self.put = put
|
||||||
|
self.post = post
|
||||||
|
self.delete = delete
|
||||||
|
self.options = options
|
||||||
|
self.head = head
|
||||||
|
self.patch = patch
|
||||||
|
self.parameters = parameters
|
||||||
|
self._insert_extras__()
|
||||||
|
|
||||||
|
|
||||||
|
class Operation(SwaggerDict):
|
||||||
|
def __init__(self, operation_id, responses, parameters=None, consumes=None,
|
||||||
|
produces=None, description=None, tags=None, **extra):
|
||||||
|
super(Operation, self).__init__(**extra)
|
||||||
|
self.operation_id = operation_id
|
||||||
|
self.responses = responses
|
||||||
|
self.parameters = [param for param in parameters if param is not None]
|
||||||
|
self.consumes = consumes
|
||||||
|
self.produces = produces
|
||||||
|
self.description = description
|
||||||
|
self.tags = tags
|
||||||
|
self._insert_extras__()
|
||||||
|
|
||||||
|
|
||||||
|
class Items(SwaggerDict):
|
||||||
|
def __init__(self, type=None, format=None, enum=None, pattern=None, items=None, **extra):
|
||||||
|
super(Items, self).__init__(**extra)
|
||||||
|
self.type = type
|
||||||
|
self.format = format
|
||||||
|
self.enum = enum
|
||||||
|
self.pattern = pattern
|
||||||
|
self.items = items
|
||||||
|
self._insert_extras__()
|
||||||
|
|
||||||
|
|
||||||
|
class Parameter(SwaggerDict):
|
||||||
|
def __init__(self, name, in_, description=None, required=None, schema=None,
|
||||||
|
type=None, format=None, enum=None, pattern=None, items=None, **extra):
|
||||||
|
super(Parameter, self).__init__(**extra)
|
||||||
|
if (not schema and not type) or (schema and type):
|
||||||
|
raise ValueError("either schema or type are required for Parameter object!")
|
||||||
|
self.name = name
|
||||||
|
self.in_ = in_
|
||||||
|
self.description = description
|
||||||
|
self.required = required
|
||||||
|
self.schema = schema
|
||||||
|
self.type = type
|
||||||
|
self.format = format
|
||||||
|
self.enum = enum
|
||||||
|
self.pattern = pattern
|
||||||
|
self.items = items
|
||||||
|
self._insert_extras__()
|
||||||
|
|
||||||
|
|
||||||
|
class Schema(SwaggerDict):
|
||||||
|
def __init__(self, description=None, required=None, type=None, properties=None, additional_properties=None,
|
||||||
|
format=None, enum=None, pattern=None, items=None, **extra):
|
||||||
|
super(Schema, self).__init__(**extra)
|
||||||
|
self.description = description
|
||||||
|
self.required = required
|
||||||
|
self.type = type
|
||||||
|
self.properties = properties
|
||||||
|
self.additional_properties = additional_properties
|
||||||
|
self.format = format
|
||||||
|
self.enum = enum
|
||||||
|
self.pattern = pattern
|
||||||
|
self.items = items
|
||||||
|
self._insert_extras__()
|
||||||
|
|
||||||
|
|
||||||
|
class Ref(SwaggerDict):
|
||||||
|
def __init__(self, ref):
|
||||||
|
super(Ref, self).__init__()
|
||||||
|
self.ref = ref
|
||||||
|
self._insert_extras__()
|
||||||
|
|
||||||
|
|
||||||
|
class Responses(SwaggerDict):
|
||||||
|
def __init__(self, responses, default=None, **extra):
|
||||||
|
super(Responses, self).__init__(**extra)
|
||||||
|
for status, response in responses.items():
|
||||||
|
if response is not None:
|
||||||
|
self[str(status)] = response
|
||||||
|
self.default = default
|
||||||
|
self._insert_extras__()
|
||||||
|
|
||||||
|
|
||||||
|
class Response(SwaggerDict):
|
||||||
|
def __init__(self, description, schema=None, examples=None, **extra):
|
||||||
|
super(Response, self).__init__(**extra)
|
||||||
|
self.description = description
|
||||||
|
self.schema = schema
|
||||||
|
self.examples = examples
|
||||||
|
self._insert_extras__()
|
||||||
|
|
|
||||||
|
|
@ -45,17 +45,17 @@ class _UIRenderer(BaseRenderer):
|
||||||
charset = 'utf-8'
|
charset = 'utf-8'
|
||||||
template = ''
|
template = ''
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, swagger, accepted_media_type=None, renderer_context=None):
|
||||||
self.set_context(renderer_context, data)
|
self.set_context(renderer_context, swagger)
|
||||||
return render(
|
return render(
|
||||||
renderer_context['request'],
|
renderer_context['request'],
|
||||||
self.template,
|
self.template,
|
||||||
renderer_context
|
renderer_context
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_context(self, renderer_context, data):
|
def set_context(self, renderer_context, swagger):
|
||||||
renderer_context['title'] = data.title
|
renderer_context['title'] = swagger.info.title
|
||||||
renderer_context['version'] = data.version
|
renderer_context['version'] = swagger.info.version
|
||||||
renderer_context['swagger_settings'] = json.dumps(self.get_swagger_ui_settings())
|
renderer_context['swagger_settings'] = json.dumps(self.get_swagger_ui_settings())
|
||||||
renderer_context['redoc_settings'] = json.dumps(self.get_redoc_settings())
|
renderer_context['redoc_settings'] = json.dumps(self.get_redoc_settings())
|
||||||
renderer_context['USE_SESSION_AUTH'] = swagger_settings.USE_SESSION_AUTH
|
renderer_context['USE_SESSION_AUTH'] = swagger_settings.USE_SESSION_AUTH
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#django-session-auth.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,14 +126,31 @@
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<div id="swagger-ui"></div>
|
<div id="swagger-ui"></div>
|
||||||
|
<div id="spec-error" class="hidden alert alert-danger"></div>
|
||||||
|
|
||||||
<script src="{% static 'drf-swagger/swagger-ui-dist/swagger-ui-bundle.js' %}"></script>
|
|
||||||
<script src="{% static 'drf-swagger/swagger-ui-dist/swagger-ui-standalone-preset.js' %}"></script>
|
|
||||||
<script src="{% static 'drf-swagger/insQ.min.js' %}"></script>
|
|
||||||
<script>
|
<script>
|
||||||
window.onload = function () {
|
"use strict";
|
||||||
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
|
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
|
||||||
var specURL = currentPath + '?format=openapi';
|
var specURL = currentPath + '?format=openapi';
|
||||||
|
|
||||||
|
function patchSwaggerUi() {
|
||||||
|
var authWrapper = document.querySelector('.auth-wrapper');
|
||||||
|
var authorizeButton = document.querySelector('.auth-wrapper .authorize');
|
||||||
|
var djangoSessionAuth = document.querySelector('#django-session-auth');
|
||||||
|
if (document.querySelector('.auth-wrapper #django-session-auth')) {
|
||||||
|
console.log("session auth already patched");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
authWrapper.insertBefore(djangoSessionAuth, authorizeButton);
|
||||||
|
djangoSessionAuth.classList.remove("hidden");
|
||||||
|
|
||||||
|
var divider = document.createElement("div");
|
||||||
|
divider.classList.add("divider");
|
||||||
|
authWrapper.insertBefore(divider, authorizeButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSwaggerUi() {
|
||||||
var swaggerConfig = {
|
var swaggerConfig = {
|
||||||
url: specURL,
|
url: specURL,
|
||||||
dom_id: '#swagger-ui',
|
dom_id: '#swagger-ui',
|
||||||
|
|
@ -158,22 +175,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.ui = SwaggerUIBundle(swaggerConfig);
|
window.ui = SwaggerUIBundle(swaggerConfig);
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
authWrapper.insertBefore(djangoSessionAuth, authorizeButton);
|
|
||||||
djangoSessionAuth.classList.remove("hidden");
|
|
||||||
|
|
||||||
var divider = document.createElement("div");
|
|
||||||
divider.classList.add("divider");
|
|
||||||
authWrapper.insertBefore(divider, authorizeButton);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.onload = function () {
|
||||||
|
insertionQ('.auth-wrapper .authorize').every(patchSwaggerUi);
|
||||||
|
initSwaggerUi();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script src="{% static 'drf-swagger/swagger-ui-dist/swagger-ui-bundle.js' %}"></script>
|
||||||
|
<script src="{% static 'drf-swagger/swagger-ui-dist/swagger-ui-standalone-preset.js' %}"></script>
|
||||||
|
<script src="{% static 'drf-swagger/insQ.min.js' %}"></script>
|
||||||
|
|
||||||
<div id="django-session-auth" class="hidden">
|
<div id="django-session-auth" class="hidden">
|
||||||
{% if USE_SESSION_AUTH %}
|
{% if USE_SESSION_AUTH %}
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ def deferred_never_cache(view_func):
|
||||||
return _wrapped_view_func
|
return _wrapped_view_func
|
||||||
|
|
||||||
|
|
||||||
def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False, validators=None,
|
def get_schema_view(info, url=None, patterns=None, urlconf=None, public=False, validators=None,
|
||||||
|
generator_class=OpenAPISchemaGenerator,
|
||||||
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
|
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
|
||||||
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
|
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
|
||||||
"""
|
"""
|
||||||
|
|
@ -58,13 +59,15 @@ def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False
|
||||||
:param str urlconf: passed to SchemaGenerator
|
:param str urlconf: passed to SchemaGenerator
|
||||||
:param bool public: if False, includes only endpoints the current user has access to
|
: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 list validators: a list of validator names to apply on the generated schema; allowed values are `flex`, `ssv`
|
||||||
|
:param type generator_class: schema generator class to use; should be a subclass of OpenAPISchemaGenerator
|
||||||
:param tuple authentication_classes: authentication classes for the schema view itself
|
:param tuple authentication_classes: authentication classes for the schema view itself
|
||||||
:param tuple permission_classes: permission classes for the schema view itself
|
:param tuple permission_classes: permission classes for the schema view itself
|
||||||
:return: SchemaView class
|
:return: SchemaView class
|
||||||
"""
|
"""
|
||||||
|
_public = public
|
||||||
|
_generator_class = generator_class
|
||||||
_auth_classes = authentication_classes
|
_auth_classes = authentication_classes
|
||||||
_perm_classes = permission_classes
|
_perm_classes = permission_classes
|
||||||
_public = public
|
|
||||||
validators = validators or []
|
validators = validators or []
|
||||||
_spec_renderers = tuple(renderer.with_validators(validators) for renderer in SPEC_RENDERERS)
|
_spec_renderers = tuple(renderer.with_validators(validators) for renderer in SPEC_RENDERERS)
|
||||||
|
|
||||||
|
|
@ -72,7 +75,7 @@ def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
schema = None # exclude from schema
|
schema = None # exclude from schema
|
||||||
public = _public
|
public = _public
|
||||||
generator_class = OpenAPISchemaGenerator
|
generator_class = _generator_class
|
||||||
authentication_classes = _auth_classes
|
authentication_classes = _auth_classes
|
||||||
permission_classes = _perm_classes
|
permission_classes = _perm_classes
|
||||||
renderer_classes = _spec_renderers
|
renderer_classes = _spec_renderers
|
||||||
|
|
@ -129,7 +132,7 @@ def get_schema_view(info, url=None, patterns=None, urlconf=None, *, public=False
|
||||||
:return: a view instance
|
:return: a view instance
|
||||||
"""
|
"""
|
||||||
assert renderer in UI_RENDERERS, "supported default renderers are " + ", ".join(UI_RENDERERS)
|
assert renderer in UI_RENDERERS, "supported default renderers are " + ", ".join(UI_RENDERERS)
|
||||||
renderer_classes = (*UI_RENDERERS[renderer], *_spec_renderers)
|
renderer_classes = UI_RENDERERS[renderer] + _spec_renderers
|
||||||
|
|
||||||
return cls.as_cached_view(cache_timeout, cache_kwargs, renderer_classes=renderer_classes)
|
return cls.as_cached_view(cache_timeout, cache_kwargs, renderer_classes=renderer_classes)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 2.0 on 2017-12-05 04:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Article',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(help_text='Main article headline', max_length=255, unique=True)),
|
||||||
|
('body', models.TextField(help_text='Article content', max_length=5000)),
|
||||||
|
('slug', models.SlugField(blank=True, help_text='Unique URL slug identifying the article', unique=True)),
|
||||||
|
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('date_modified', models.DateTimeField(auto_now=True)),
|
||||||
|
('cover', models.ImageField(blank=True, upload_to='article/original/')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Article(models.Model):
|
||||||
|
title = models.CharField(help_text="title model help_text", max_length=255, blank=False, unique=True)
|
||||||
|
body = models.TextField(help_text="article model help_text", max_length=5000, blank=False)
|
||||||
|
slug = models.SlugField(help_text="slug model help_text", unique=True, blank=True)
|
||||||
|
date_created = models.DateTimeField(auto_now_add=True)
|
||||||
|
date_modified = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
cover = models.ImageField(upload_to='article/original/', blank=True)
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from articles.models import Article
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
fields = ('title', 'body', 'slug', 'date_created', 'date_modified')
|
||||||
|
read_only_fields = ('date_created', 'date_modified')
|
||||||
|
lookup_field = 'slug'
|
||||||
|
extra_kwargs = {'body': {'help_text': 'body serializer help_text'}}
|
||||||
|
|
||||||
|
|
||||||
|
class ImageUploadSerializer(serializers.Serializer):
|
||||||
|
upload = serializers.ImageField(help_text="image serializer help_text")
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.conf.urls import include, url
|
||||||
|
from rest_framework.routers import SimpleRouter
|
||||||
|
|
||||||
|
from articles import views
|
||||||
|
|
||||||
|
router = SimpleRouter()
|
||||||
|
router.register('', views.ArticleViewSet)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^', include(router.urls)),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend, filters
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.decorators import detail_route, list_route
|
||||||
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
|
from rest_framework.parsers import MultiPartParser
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from articles import serializers
|
||||||
|
from articles.models import Article
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
ArticleViewSet class docstring
|
||||||
|
|
||||||
|
retrieve:
|
||||||
|
retrieve class docstring
|
||||||
|
|
||||||
|
destroy:
|
||||||
|
destroy class docstring
|
||||||
|
"""
|
||||||
|
queryset = Article.objects.all()
|
||||||
|
lookup_field = 'slug'
|
||||||
|
serializer_class = serializers.ArticleSerializer
|
||||||
|
|
||||||
|
pagination_class = LimitOffsetPagination
|
||||||
|
max_page_size = 5
|
||||||
|
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||||
|
filter_fields = ('title',)
|
||||||
|
ordering_fields = ('date_modified',)
|
||||||
|
ordering = ('username',)
|
||||||
|
|
||||||
|
@list_route(methods=['get'])
|
||||||
|
def today(self, request):
|
||||||
|
today_min = datetime.datetime.combine(datetime.date.today(), datetime.time.min)
|
||||||
|
today_max = datetime.datetime.combine(datetime.date.today(), datetime.time.max)
|
||||||
|
articles = self.get_queryset().filter(date_created__range=(today_min, today_max)).all()
|
||||||
|
serializer = self.serializer_class(articles, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@detail_route(
|
||||||
|
methods=['get', 'post'],
|
||||||
|
parser_classes=(MultiPartParser,),
|
||||||
|
serializer_class=serializers.ImageUploadSerializer,
|
||||||
|
)
|
||||||
|
def image(self, request, slug=None):
|
||||||
|
"""
|
||||||
|
image method docstring
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
"""update method docstring"""
|
||||||
|
return super(ArticleViewSet, self).update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
"""destroy method docstring"""
|
||||||
|
return super(ArticleViewSet, self).destroy(request, *args, **kwargs)
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 2.0 on 2017-12-05 04:05
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('snippets', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='snippet',
|
||||||
|
name='owner',
|
||||||
|
field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='snippets', to=settings.AUTH_USER_MODEL),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='snippet',
|
||||||
|
name='code',
|
||||||
|
field=models.TextField(help_text='code model help text'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -9,8 +9,9 @@ STYLE_CHOICES = sorted((item, item) for item in get_all_styles())
|
||||||
|
|
||||||
class Snippet(models.Model):
|
class Snippet(models.Model):
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
|
||||||
title = models.CharField(max_length=100, blank=True, default='')
|
title = models.CharField(max_length=100, blank=True, default='')
|
||||||
code = models.TextField()
|
code = models.TextField(help_text="code model help text")
|
||||||
linenos = models.BooleanField(default=False)
|
linenos = models.BooleanField(default=False)
|
||||||
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
|
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
|
||||||
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
|
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ class SnippetSerializer(serializers.Serializer):
|
||||||
|
|
||||||
create: docstring for create from serializer classdoc
|
create: docstring for create from serializer classdoc
|
||||||
"""
|
"""
|
||||||
id = serializers.IntegerField(read_only=True, help_text="id help text")
|
id = serializers.IntegerField(read_only=True, help_text="id serializer help text")
|
||||||
|
owner = serializers.ReadOnlyField(source='owner.username')
|
||||||
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
|
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
|
||||||
code = serializers.CharField(style={'base_template': 'textarea.html'})
|
code = serializers.CharField(style={'base_template': 'textarea.html'})
|
||||||
linenos = serializers.BooleanField(required=False)
|
linenos = serializers.BooleanField(required=False)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
|
|
||||||
from snippets.models import Snippet
|
from snippets.models import Snippet
|
||||||
from snippets.serializers import SnippetSerializer
|
from snippets.serializers import SnippetSerializer
|
||||||
|
|
||||||
|
|
@ -8,9 +9,12 @@ class SnippetList(generics.ListCreateAPIView):
|
||||||
queryset = Snippet.objects.all()
|
queryset = Snippet.objects.all()
|
||||||
serializer_class = SnippetSerializer
|
serializer_class = SnippetSerializer
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(owner=self.request.user)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""post method docstring"""
|
"""post method docstring"""
|
||||||
return super().post(request, *args, **kwargs)
|
return super(SnippetList, self).post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
|
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
|
@ -28,8 +32,8 @@ class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
|
||||||
def patch(self, request, *args, **kwargs):
|
def patch(self, request, *args, **kwargs):
|
||||||
"""patch method docstring"""
|
"""patch method docstring"""
|
||||||
return super().patch(request, *args, **kwargs)
|
return super(SnippetDetail, self).patch(request, *args, **kwargs)
|
||||||
|
|
||||||
def delete(self, request, *args, **kwargs):
|
def delete(self, request, *args, **kwargs):
|
||||||
"""delete method docstring"""
|
"""delete method docstring"""
|
||||||
return super().patch(request, *args, **kwargs)
|
return super(SnippetDetail, self).patch(request, *args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,9 @@ INSTALLED_APPS = [
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
|
|
||||||
'drf_swagger',
|
'drf_swagger',
|
||||||
'snippets'
|
'snippets',
|
||||||
|
'users',
|
||||||
|
'articles',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
@ -42,6 +44,7 @@ MIDDLEWARE = [
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'drf_swagger.middleware.SwaggerExceptionMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'testproj.urls'
|
ROOT_URLCONF = 'testproj.urls'
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import json
|
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
from ruamel import yaml
|
|
||||||
|
|
||||||
from drf_swagger import openapi, codecs
|
|
||||||
from drf_swagger.generators import OpenAPISchemaGenerator
|
|
||||||
|
|
||||||
|
|
||||||
class SchemaGeneratorTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.generator = OpenAPISchemaGenerator(
|
|
||||||
info=openapi.Info("Test generator", "v1"),
|
|
||||||
version="v2",
|
|
||||||
)
|
|
||||||
self.codec_json = codecs.OpenAPICodecJson(['flex', 'ssv'])
|
|
||||||
self.codec_yaml = codecs.OpenAPICodecYaml(['ssv', 'flex'])
|
|
||||||
|
|
||||||
def _validate_schema(self, swagger):
|
|
||||||
from flex.core import parse as validate_flex
|
|
||||||
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
|
|
||||||
|
|
||||||
validate_flex(swagger)
|
|
||||||
validate_ssv(swagger)
|
|
||||||
|
|
||||||
def test_schema_generates_without_errors(self):
|
|
||||||
self.generator.get_schema(None, True)
|
|
||||||
|
|
||||||
def test_schema_is_valid(self):
|
|
||||||
swagger = self.generator.get_schema(None, True)
|
|
||||||
self.codec_yaml.encode(swagger)
|
|
||||||
|
|
||||||
def test_invalid_schema_fails(self):
|
|
||||||
bad_generator = OpenAPISchemaGenerator(
|
|
||||||
info=openapi.Info(
|
|
||||||
"Test generator", "v1",
|
|
||||||
contact=openapi.Contact(name=69, email=[])
|
|
||||||
),
|
|
||||||
version="v2",
|
|
||||||
)
|
|
||||||
|
|
||||||
swagger = bad_generator.get_schema(None, True)
|
|
||||||
with self.assertRaises(codecs.SwaggerValidationError):
|
|
||||||
self.codec_json.encode(swagger)
|
|
||||||
|
|
||||||
def test_json_codec_roundtrip(self):
|
|
||||||
swagger = self.generator.get_schema(None, True)
|
|
||||||
json_bytes = self.codec_json.encode(swagger)
|
|
||||||
self._validate_schema(json.loads(json_bytes.decode('utf-8')))
|
|
||||||
|
|
||||||
def test_yaml_codec_roundtrip(self):
|
|
||||||
swagger = self.generator.get_schema(None, True)
|
|
||||||
json_bytes = self.codec_yaml.encode(swagger)
|
|
||||||
self._validate_schema(yaml.safe_load(json_bytes.decode('utf-8')))
|
|
||||||
|
|
||||||
|
|
||||||
class SchemaTest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.generator = OpenAPISchemaGenerator(
|
|
||||||
info=openapi.Info("Test generator", "v1"),
|
|
||||||
version="v2",
|
|
||||||
)
|
|
||||||
self.codec_json = codecs.OpenAPICodecJson(['flex', 'ssv'])
|
|
||||||
self.codec_yaml = codecs.OpenAPICodecYaml(['ssv', 'flex'])
|
|
||||||
|
|
||||||
self.swagger = self.generator.get_schema(None, True)
|
|
||||||
json_bytes = self.codec_yaml.encode(self.swagger)
|
|
||||||
self.swagger_dict = yaml.safe_load(json_bytes.decode('utf-8'))
|
|
||||||
|
|
||||||
def test_paths_not_empty(self):
|
|
||||||
self.assertTrue(bool(self.swagger_dict['paths']))
|
|
||||||
|
|
||||||
def test_appropriate_status_codes(self):
|
|
||||||
snippets_list = self.swagger_dict['paths']['/snippets/']
|
|
||||||
self.assertTrue('200' in snippets_list['get']['responses'])
|
|
||||||
self.assertTrue('201' in snippets_list['post']['responses'])
|
|
||||||
snippets_detail = self.swagger_dict['paths']['/snippets/{id}/']
|
|
||||||
self.assertTrue('200' in snippets_detail['get']['responses'])
|
|
||||||
self.assertTrue('200' in snippets_detail['put']['responses'])
|
|
||||||
self.assertTrue('200' in snippets_detail['patch']['responses'])
|
|
||||||
self.assertTrue('204' in snippets_detail['delete']['responses'])
|
|
||||||
|
|
||||||
def test_operation_docstrings(self):
|
|
||||||
snippets_list = self.swagger_dict['paths']['/snippets/']
|
|
||||||
self.assertEqual(snippets_list['get']['description'], "SnippetList classdoc")
|
|
||||||
self.assertEqual(snippets_list['post']['description'], "post method docstring")
|
|
||||||
snippets_detail = self.swagger_dict['paths']['/snippets/{id}/']
|
|
||||||
self.assertEqual(snippets_detail['get']['description'], "SnippetDetail classdoc")
|
|
||||||
self.assertEqual(snippets_detail['put']['description'], "put class docstring")
|
|
||||||
self.assertEqual(snippets_detail['patch']['description'], "patch method docstring")
|
|
||||||
self.assertEqual(snippets_detail['delete']['description'], "delete method docstring")
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from drf_swagger.views import get_schema_view
|
|
||||||
from drf_swagger import openapi
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
from drf_swagger import openapi
|
||||||
|
from drf_swagger.views import get_schema_view
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
schema_view = get_schema_view(
|
||||||
openapi.Info(
|
openapi.Info(
|
||||||
title="Snippets API",
|
title="Snippets API",
|
||||||
|
|
@ -14,16 +14,18 @@ schema_view = get_schema_view(
|
||||||
contact=openapi.Contact(email="contact@snippets.local"),
|
contact=openapi.Contact(email="contact@snippets.local"),
|
||||||
license=openapi.License(name="BSD License"),
|
license=openapi.License(name="BSD License"),
|
||||||
),
|
),
|
||||||
validators=['flex', 'ssv'],
|
validators=['ssv', 'flex'],
|
||||||
public=False,
|
public=True,
|
||||||
permission_classes=(permissions.AllowAny,),
|
permission_classes=(permissions.AllowAny,),
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'),
|
url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||||
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
|
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||||
url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
|
url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
|
||||||
|
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin.site.urls),
|
||||||
url(r'^snippets/', include('snippets.urls')),
|
url(r'^snippets/', include('snippets.urls')),
|
||||||
|
url(r'^articles/', include('articles.urls')),
|
||||||
|
url(r'^users/', include('users.urls')),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from snippets.models import Snippet
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ('id', 'username', 'snippets')
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from users import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.UserList.as_view()),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/$', views.user_detail),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework.decorators import api_view
|
||||||
|
from rest_framework.generics import get_object_or_404
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from users.serializers import UserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class UserList(APIView):
|
||||||
|
"""UserList cbv classdoc"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer = UserSerializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
def user_detail(request, pk):
|
||||||
|
"""user_detail fbv docstring"""
|
||||||
|
user = get_object_or_404(User.objects, pk=pk)
|
||||||
|
serializer = UserSerializer(user)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from ruamel import yaml
|
||||||
|
|
||||||
from drf_swagger import openapi, codecs
|
from drf_swagger import openapi, codecs
|
||||||
from drf_swagger.generators import OpenAPISchemaGenerator
|
from drf_swagger.generators import OpenAPISchemaGenerator
|
||||||
from ruamel import yaml
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def generator():
|
def generator():
|
||||||
return OpenAPISchemaGenerator(
|
return OpenAPISchemaGenerator(
|
||||||
info=openapi.Info("Test generator", "v1"),
|
info=openapi.Info(title="Test generator", default_version="v1"),
|
||||||
version="v2",
|
version="v2",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -27,3 +28,28 @@ def swagger_dict():
|
||||||
swagger = generator().get_schema(None, True)
|
swagger = generator().get_schema(None, True)
|
||||||
json_bytes = codec_yaml().encode(swagger)
|
json_bytes = codec_yaml().encode(swagger)
|
||||||
return yaml.safe_load(json_bytes.decode('utf-8'))
|
return yaml.safe_load(json_bytes.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def validate_schema():
|
||||||
|
def validate_schema(swagger):
|
||||||
|
from flex.core import parse as validate_flex
|
||||||
|
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
|
||||||
|
|
||||||
|
validate_flex(swagger)
|
||||||
|
validate_ssv(swagger)
|
||||||
|
|
||||||
|
return validate_schema
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bad_settings():
|
||||||
|
from drf_swagger.app_settings import swagger_settings, SWAGGER_DEFAULTS
|
||||||
|
bad_security = {
|
||||||
|
'bad': {
|
||||||
|
'bad_attribute': 'should not be accepted'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SWAGGER_DEFAULTS['SECURITY_DEFINITIONS'].update(bad_security)
|
||||||
|
yield swagger_settings
|
||||||
|
del SWAGGER_DEFAULTS['SECURITY_DEFINITIONS']['bad']
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
def test_operation_docstrings(swagger_dict):
|
||||||
|
users_list = swagger_dict['paths']['/users/']
|
||||||
|
assert users_list['get']['description'] == "UserList cbv classdoc"
|
||||||
|
|
||||||
|
users_detail = swagger_dict['paths']['/users/{id}/']
|
||||||
|
assert users_detail['get']['description'] == "user_detail fbv docstring"
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
def test_appropriate_status_codes(swagger_dict):
|
||||||
|
snippets_list = swagger_dict['paths']['/snippets/']
|
||||||
|
assert '200' in snippets_list['get']['responses']
|
||||||
|
assert '201' in snippets_list['post']['responses']
|
||||||
|
|
||||||
|
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
|
||||||
|
assert '200' in snippets_detail['get']['responses']
|
||||||
|
assert '200' in snippets_detail['put']['responses']
|
||||||
|
assert '200' in snippets_detail['patch']['responses']
|
||||||
|
assert '204' in snippets_detail['delete']['responses']
|
||||||
|
|
||||||
|
|
||||||
|
def test_operation_docstrings(swagger_dict):
|
||||||
|
snippets_list = swagger_dict['paths']['/snippets/']
|
||||||
|
assert snippets_list['get']['description'] == "SnippetList classdoc"
|
||||||
|
assert snippets_list['post']['description'] == "post method docstring"
|
||||||
|
|
||||||
|
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
|
||||||
|
assert snippets_detail['get']['description'] == "SnippetDetail classdoc"
|
||||||
|
assert snippets_detail['put']['description'] == "put class docstring"
|
||||||
|
assert snippets_detail['patch']['description'] == "patch method docstring"
|
||||||
|
assert snippets_detail['delete']['description'] == "delete method docstring"
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
def test_appropriate_status_codes(swagger_dict):
|
||||||
|
articles_list = swagger_dict['paths']['/articles/']
|
||||||
|
assert '200' in articles_list['get']['responses']
|
||||||
|
assert '201' in articles_list['post']['responses']
|
||||||
|
|
||||||
|
articles_detail = swagger_dict['paths']['/articles/{slug}/']
|
||||||
|
assert '200' in articles_detail['get']['responses']
|
||||||
|
assert '200' in articles_detail['put']['responses']
|
||||||
|
assert '200' in articles_detail['patch']['responses']
|
||||||
|
assert '204' in articles_detail['delete']['responses']
|
||||||
|
|
||||||
|
|
||||||
|
def test_operation_docstrings(swagger_dict):
|
||||||
|
articles_list = swagger_dict['paths']['/articles/']
|
||||||
|
assert articles_list['get']['description'] == "ArticleViewSet class docstring"
|
||||||
|
assert articles_list['post']['description'] == "ArticleViewSet class docstring"
|
||||||
|
|
||||||
|
articles_detail = swagger_dict['paths']['/articles/{slug}/']
|
||||||
|
assert articles_detail['get']['description'] == "retrieve class docstring"
|
||||||
|
assert articles_detail['put']['description'] == "update method docstring"
|
||||||
|
assert articles_detail['patch']['description'] == "ArticleViewSet class docstring"
|
||||||
|
assert articles_detail['delete']['description'] == "destroy method docstring"
|
||||||
|
|
||||||
|
articles_today = swagger_dict['paths']['/articles/today/']
|
||||||
|
assert articles_today['get']['description'] == "ArticleViewSet class docstring"
|
||||||
|
|
||||||
|
articles_image = swagger_dict['paths']['/articles/{slug}/image/']
|
||||||
|
assert articles_image['get']['description'] == "image method docstring"
|
||||||
|
assert articles_image['post']['description'] == "image method docstring"
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from drf_swagger import openapi, codecs
|
|
||||||
from drf_swagger.generators import OpenAPISchemaGenerator
|
|
||||||
from ruamel import yaml
|
from ruamel import yaml
|
||||||
|
|
||||||
|
from drf_swagger import openapi, codecs
|
||||||
def validate_schema(swagger):
|
from drf_swagger.generators import OpenAPISchemaGenerator
|
||||||
from flex.core import parse as validate_flex
|
|
||||||
from swagger_spec_validator.validator20 import validate_spec as validate_ssv
|
|
||||||
|
|
||||||
validate_flex(swagger)
|
|
||||||
validate_ssv(swagger)
|
|
||||||
|
|
||||||
|
|
||||||
def test_schema_generates_without_errors(generator):
|
def test_schema_generates_without_errors(generator):
|
||||||
|
|
@ -24,9 +17,10 @@ def test_schema_is_valid(generator, codec_yaml):
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_schema_fails(codec_json):
|
def test_invalid_schema_fails(codec_json):
|
||||||
|
# noinspection PyTypeChecker
|
||||||
bad_generator = OpenAPISchemaGenerator(
|
bad_generator = OpenAPISchemaGenerator(
|
||||||
info=openapi.Info(
|
info=openapi.Info(
|
||||||
"Test generator", "v1",
|
title="Test generator", default_version="v1",
|
||||||
contact=openapi.Contact(name=69, email=[])
|
contact=openapi.Contact(name=69, email=[])
|
||||||
),
|
),
|
||||||
version="v2",
|
version="v2",
|
||||||
|
|
@ -37,13 +31,13 @@ def test_invalid_schema_fails(codec_json):
|
||||||
codec_json.encode(swagger)
|
codec_json.encode(swagger)
|
||||||
|
|
||||||
|
|
||||||
def test_json_codec_roundtrip(codec_json, generator):
|
def test_json_codec_roundtrip(codec_json, generator, validate_schema):
|
||||||
swagger = generator.get_schema(None, True)
|
swagger = generator.get_schema(None, True)
|
||||||
json_bytes = codec_json.encode(swagger)
|
json_bytes = codec_json.encode(swagger)
|
||||||
validate_schema(json.loads(json_bytes.decode('utf-8')))
|
validate_schema(json.loads(json_bytes.decode('utf-8')))
|
||||||
|
|
||||||
|
|
||||||
def test_yaml_codec_roundtrip(codec_yaml, generator):
|
def test_yaml_codec_roundtrip(codec_yaml, generator, validate_schema):
|
||||||
swagger = generator.get_schema(None, True)
|
swagger = generator.get_schema(None, True)
|
||||||
json_bytes = codec_yaml.encode(swagger)
|
json_bytes = codec_yaml.encode(swagger)
|
||||||
validate_schema(yaml.safe_load(json_bytes.decode('utf-8')))
|
validate_schema(yaml.safe_load(json_bytes.decode('utf-8')))
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,2 @@
|
||||||
def test_paths_not_empty(swagger_dict):
|
def test_paths_not_empty(swagger_dict):
|
||||||
assert bool(swagger_dict['paths'])
|
assert len(swagger_dict['paths']) > 0
|
||||||
|
|
||||||
|
|
||||||
def test_appropriate_status_codes(swagger_dict):
|
|
||||||
snippets_list = swagger_dict['paths']['/snippets/']
|
|
||||||
assert '200' in snippets_list['get']['responses']
|
|
||||||
assert '201' in snippets_list['post']['responses']
|
|
||||||
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
|
|
||||||
assert '200' in snippets_detail['get']['responses']
|
|
||||||
assert '200' in snippets_detail['put']['responses']
|
|
||||||
assert '200' in snippets_detail['patch']['responses']
|
|
||||||
assert '204' in snippets_detail['delete']['responses']
|
|
||||||
|
|
||||||
|
|
||||||
def test_operation_docstrings(swagger_dict):
|
|
||||||
snippets_list = swagger_dict['paths']['/snippets/']
|
|
||||||
assert snippets_list['get']['description'] == "SnippetList classdoc"
|
|
||||||
assert snippets_list['post']['description'] == "post method docstring"
|
|
||||||
snippets_detail = swagger_dict['paths']['/snippets/{id}/']
|
|
||||||
assert snippets_detail['get']['description'] == "SnippetDetail classdoc"
|
|
||||||
assert snippets_detail['put']['description'] == "put class docstring"
|
|
||||||
assert snippets_detail['patch']['description'] == "patch method docstring"
|
|
||||||
assert snippets_detail['delete']['description'] == "delete method docstring"
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from ruamel import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_text_schema_view(client, validate_schema, path, loader):
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == 200
|
||||||
|
validate_schema(loader(response.content.decode('utf-8')))
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_ui_schema_view(client, path, string):
|
||||||
|
response = client.get(path)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert string in response.content.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def test_swagger_json(client, validate_schema):
|
||||||
|
_validate_text_schema_view(client, validate_schema, "/swagger.json", json.loads)
|
||||||
|
|
||||||
|
|
||||||
|
def test_swagger_yaml(client, validate_schema):
|
||||||
|
_validate_text_schema_view(client, validate_schema, "/swagger.yaml", yaml.safe_load)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exception_middleware(client, bad_settings):
|
||||||
|
response = client.get('/swagger.json')
|
||||||
|
assert response.status_code == 500
|
||||||
|
assert 'errors' in json.loads(response.content.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_swagger_ui(client, validate_schema):
|
||||||
|
_validate_ui_schema_view(client, '/swagger/', 'swagger-ui-dist/swagger-ui-bundle.js')
|
||||||
|
_validate_text_schema_view(client, validate_schema, '/swagger/?format=openapi', json.loads)
|
||||||
|
|
||||||
|
|
||||||
|
def test_redoc(client, validate_schema):
|
||||||
|
_validate_ui_schema_view(client, '/redoc/', 'redoc/redoc.min.js')
|
||||||
|
_validate_text_schema_view(client, validate_schema, '/redoc/?format=openapi', json.loads)
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
from drf_swagger import openapi
|
||||||
|
|
||||||
|
|
||||||
|
def test_vendor_extensions():
|
||||||
|
"""Any attribute starting with x_ should map to a vendor property of the form x-camelCase"""
|
||||||
|
sd = openapi.SwaggerDict(x_vendor_ext_1='test')
|
||||||
|
sd.x_vendor_ext_2 = 'test'
|
||||||
|
assert 'x-vendorExt1' in sd
|
||||||
|
assert sd.x_vendor_ext_1 == 'test'
|
||||||
|
assert sd['x-vendorExt2'] == 'test'
|
||||||
|
|
||||||
|
del sd.x_vendor_ext_1
|
||||||
|
assert 'x-vendorExt1' not in sd
|
||||||
|
|
||||||
|
|
||||||
|
def test_ref():
|
||||||
|
"""The attribute 'ref' maps to the swagger key '$ref'"""
|
||||||
|
sd = openapi.SwaggerDict(ref='reftest')
|
||||||
|
assert '$ref' in sd
|
||||||
|
assert sd['$ref'] == sd.ref == 'reftest'
|
||||||
|
|
||||||
|
del sd['$ref']
|
||||||
|
assert not hasattr(sd, 'ref')
|
||||||
|
|
||||||
|
|
||||||
|
def test_leading_underscore_ignored():
|
||||||
|
"""Attributes with a leading underscore are set on the object as-is and are not added to its dict form"""
|
||||||
|
sd = openapi.SwaggerDict(_private_attr_1='not_camelised')
|
||||||
|
initial_len = len(sd)
|
||||||
|
sd._nope = 'not camelised either'
|
||||||
|
assert len(sd) == initial_len
|
||||||
|
assert 'privateAttr1' not in sd
|
||||||
|
assert sd._private_attr_1 == 'not_camelised'
|
||||||
|
assert '_private_attr_1' not in sd
|
||||||
|
assert hasattr(sd, '_nope')
|
||||||
|
|
||||||
|
del sd._nope
|
||||||
|
assert not hasattr(sd, '_nope')
|
||||||
|
|
||||||
|
|
||||||
|
def test_trailing_underscore_stripped():
|
||||||
|
"""Trailing underscores are stripped when converting attribute names.
|
||||||
|
This allows, for example, python keywords to function as SwaggerDict attributes."""
|
||||||
|
sd = openapi.SwaggerDict(trailing_underscore_='trailing')
|
||||||
|
sd.in_ = 'trailing'
|
||||||
|
assert 'in' in sd
|
||||||
|
assert 'trailingUnderscore' in sd
|
||||||
|
assert sd.trailing_underscore == sd['in']
|
||||||
|
assert hasattr(sd, 'in___')
|
||||||
|
|
||||||
|
del sd.in_
|
||||||
|
assert 'in' not in sd
|
||||||
|
assert not hasattr(sd, 'in__')
|
||||||
38
tox.ini
38
tox.ini
|
|
@ -1,20 +1,46 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py35,py36,py37,flake8
|
envlist =
|
||||||
|
py27-drf37,
|
||||||
|
py{34,35,36,37}-drf37,
|
||||||
|
py36-drfmaster,
|
||||||
|
flake8
|
||||||
|
|
||||||
|
[travis:env]
|
||||||
|
DRF =
|
||||||
|
3.7: drf37
|
||||||
|
master: drfmaster
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
-rrequirements.txt
|
drf37: djangorestframework>=3.7.3,<3.8
|
||||||
-rrequirements_validation.txt
|
|
||||||
-rrequirements_test.txt
|
# py27 is tested with Django <2.0 (Django 2.0 no longer supports python 2)
|
||||||
|
py27: Django>=1.11,<2.0
|
||||||
|
|
||||||
|
# main testing configurations
|
||||||
|
py{34,35,36,37}-drf37: Django>=1.11,<2.1
|
||||||
|
|
||||||
|
# py3 with the latest build of Django and django-rest-framework to get early warning of compatibility issues
|
||||||
|
drfmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz
|
||||||
|
drfmaster: https://github.com/django/django/archive/master.tar.gz
|
||||||
|
|
||||||
|
# other dependencies
|
||||||
|
-rrequirements/base.txt
|
||||||
|
-rrequirements/validation.txt
|
||||||
|
-rrequirements/test.txt
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
pytest --cov-append --cov=drf_swagger
|
pytest --cov-config .coveragerc --cov-append --cov
|
||||||
|
|
||||||
|
[testenv:py36-drfmaster]
|
||||||
|
pip_pre = True
|
||||||
|
|
||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
skip_install = true
|
skip_install = true
|
||||||
deps =
|
deps =
|
||||||
flake8
|
flake8
|
||||||
commands=
|
commands=
|
||||||
flake8 drf_swagger testproj
|
flake8 src/drf_swagger testproj tests setup.py
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue