"""Various utilities for parsing OpenAPI operations from docstrings and validating against
the OpenAPI spec.
"""
import re
import json
from distutils import version
from apispec import exceptions
COMPONENT_SUBSECTIONS = {
2: {
"schema": "definitions",
"response": "responses",
"parameter": "parameters",
"security_scheme": "securityDefinitions",
},
3: {
"schema": "schemas",
"response": "responses",
"parameter": "parameters",
"header": "headers",
"example": "examples",
"security_scheme": "securitySchemes",
},
}
[docs]def build_reference(component_type, openapi_major_version, component_name):
"""Return path to reference
:param str component_type: Component type (schema, parameter, response, security_scheme)
:param int openapi_major_version: OpenAPI major version (2 or 3)
:param str component_name: Name of component to reference
"""
return {
"$ref": "#/{}{}/{}".format(
"components/" if openapi_major_version >= 3 else "",
COMPONENT_SUBSECTIONS[openapi_major_version][component_type],
component_name,
)
}
[docs]def validate_spec(spec):
"""Validate the output of an :class:`APISpec` object against the
OpenAPI specification.
Note: Requires installing apispec with the ``[validation]`` extras.
::
pip install 'apispec[validation]'
:raise: apispec.exceptions.OpenAPIError if validation fails.
"""
try:
import prance
except ImportError as error: # re-raise with a more verbose message
exc_class = type(error)
raise exc_class(
"validate_spec requires prance to be installed. "
"You can install all validation requirements using:\n"
" pip install 'apispec[validation]'"
)
parser_kwargs = {}
if spec.openapi_version.version[0] == 3:
parser_kwargs["backend"] = "openapi-spec-validator"
try:
prance.BaseParser(spec_string=json.dumps(spec.to_dict()), **parser_kwargs)
except prance.ValidationError as err:
raise exceptions.OpenAPIError(*err.args)
else:
return True
[docs]class OpenAPIVersion(version.LooseVersion):
"""OpenAPI version
:param str|OpenAPIVersion openapi_version: OpenAPI version
Parses an OpenAPI version expressed as string. Provides shortcut to digits
(major, minor, patch).
Example: ::
ver = OpenAPIVersion('3.0.2')
assert ver.major == 3
assert ver.minor == 0
assert ver.patch == 1
assert ver.vstring == '3.0.2'
assert str(ver) == '3.0.2'
"""
MIN_INCLUSIVE_VERSION = version.LooseVersion("2.0")
MAX_EXCLUSIVE_VERSION = version.LooseVersion("4.0")
def __init__(self, openapi_version):
if isinstance(openapi_version, version.LooseVersion):
openapi_version = openapi_version.vstring
if (
not self.MIN_INCLUSIVE_VERSION
<= openapi_version
< self.MAX_EXCLUSIVE_VERSION
):
raise exceptions.APISpecError(
f"Not a valid OpenAPI version number: {openapi_version}"
)
super().__init__(openapi_version)
@property
def major(self):
return self.version[0]
@property
def minor(self):
return self.version[1]
@property
def patch(self):
return self.version[2]
# from django.contrib.admindocs.utils
[docs]def trim_docstring(docstring):
"""Uniformly trims leading/trailing whitespace from docstrings.
Based on http://www.python.org/peps/pep-0257.html#handling-docstring-indentation
"""
if not docstring or not docstring.strip():
return ""
# Convert tabs to spaces and split into lines
lines = docstring.expandtabs().splitlines()
indent = min(len(line) - len(line.lstrip()) for line in lines if line.lstrip())
trimmed = [lines[0].lstrip()] + [line[indent:].rstrip() for line in lines[1:]]
return "\n".join(trimmed).strip()
# from rest_framework.utils.formatting
[docs]def dedent(content):
"""
Remove leading indent from a block of text.
Used when generating descriptions from docstrings.
Note that python's `textwrap.dedent` doesn't quite cut it,
as it fails to dedent multiline docstrings that include
unindented text on the initial line.
"""
whitespace_counts = [
len(line) - len(line.lstrip(" "))
for line in content.splitlines()[1:]
if line.lstrip()
]
# unindent the content if needed
if whitespace_counts:
whitespace_pattern = "^" + (" " * min(whitespace_counts))
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), "", content)
return content.strip()
# http://stackoverflow.com/a/8310229
[docs]def deepupdate(original, update):
"""Recursively update a dict.
Subdict's won't be overwritten but also updated.
"""
for key, value in original.items():
if key not in update:
update[key] = value
elif isinstance(value, dict):
deepupdate(value, update[key])
return update