|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import itertools
|
|
|
|
import re
|
|
|
|
|
|
|
|
from inspect import isclass, getdoc
|
|
|
|
from collections import OrderedDict
|
|
|
|
|
|
|
|
from collections.abc import Hashable
|
|
|
|
|
|
|
|
from flask import current_app
|
|
|
|
|
|
|
|
from . import fields
|
|
|
|
from .model import Model, ModelBase, OrderedModel
|
|
|
|
from .reqparse import RequestParser
|
|
|
|
from .utils import merge, not_none, not_none_sorted
|
|
|
|
from ._http import HTTPStatus
|
|
|
|
|
|
|
|
from urllib.parse import quote
|
|
|
|
|
|
|
|
#: Maps Flask/Werkzeug rooting types to Swagger ones
|
|
|
|
PATH_TYPES = {
|
|
|
|
"int": "integer",
|
|
|
|
"float": "number",
|
|
|
|
"string": "string",
|
|
|
|
"default": "string",
|
|
|
|
}
|
|
|
|
|
|
|
|
#: Maps Python primitives types to Swagger ones
|
|
|
|
PY_TYPES = {
|
|
|
|
int: "integer",
|
|
|
|
float: "number",
|
|
|
|
str: "string",
|
|
|
|
bool: "boolean",
|
|
|
|
None: "void",
|
|
|
|
}
|
|
|
|
|
|
|
|
RE_URL = re.compile(r"<(?:[^:<>]+:)?([^<>]+)>")
|
|
|
|
|
|
|
|
DEFAULT_RESPONSE_DESCRIPTION = "Success"
|
|
|
|
DEFAULT_RESPONSE = {"description": DEFAULT_RESPONSE_DESCRIPTION}
|
|
|
|
|
|
|
|
RE_RAISES = re.compile(
|
|
|
|
r"^:raises\s+(?P<name>[\w\d_]+)\s*:\s*(?P<description>.*)$", re.MULTILINE
|
|
|
|
)
|
|
|
|
|
|
|
|
RE_PARSE_RULE = re.compile(
|
|
|
|
r"""
|
|
|
|
(?P<static>[^<]*) # static rule data
|
|
|
|
<
|
|
|
|
(?:
|
|
|
|
(?P<converter>[a-zA-Z_][a-zA-Z0-9_]*) # converter name
|
|
|
|
(?:\((?P<args>.*?)\))? # converter arguments
|
|
|
|
\: # variable delimiter
|
|
|
|
)?
|
|
|
|
(?P<variable>[a-zA-Z_][a-zA-Z0-9_]*) # variable name
|
|
|
|
>
|
|
|
|
""",
|
|
|
|
re.VERBOSE,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def ref(model):
|
|
|
|
"""Return a reference to model in definitions"""
|
|
|
|
name = model.name if isinstance(model, ModelBase) else model
|
|
|
|
return {"$ref": "#/definitions/{0}".format(quote(name, safe=""))}
|
|
|
|
|
|
|
|
|
|
|
|
def _v(value):
|
|
|
|
"""Dereference values (callable)"""
|
|
|
|
return value() if callable(value) else value
|
|
|
|
|
|
|
|
|
|
|
|
def extract_path(path):
|
|
|
|
"""
|
|
|
|
Transform a Flask/Werkzeug URL pattern in a Swagger one.
|
|
|
|
"""
|
|
|
|
return RE_URL.sub(r"{\1}", path)
|
|
|
|
|
|
|
|
|
|
|
|
def parse_rule(rule):
|
|
|
|
"""
|
|
|
|
Parse a rule and return it as generator. Each iteration yields tuples in the form
|
|
|
|
``(converter, arguments, variable)``. If the converter is `None` it's a static url part, otherwise it's a dynamic
|
|
|
|
one.
|
|
|
|
|
|
|
|
Note: This originally lived in werkzeug.routing.parse_rule until it was removed in werkzeug 2.2.0.
|
|
|
|
"""
|
|
|
|
pos = 0
|
|
|
|
end = len(rule)
|
|
|
|
do_match = RE_PARSE_RULE.match
|
|
|
|
used_names = set()
|
|
|
|
while pos < end:
|
|
|
|
m = do_match(rule, pos)
|
|
|
|
if m is None:
|
|
|
|
break
|
|
|
|
data = m.groupdict()
|
|
|
|
if data["static"]:
|
|
|
|
yield None, None, data["static"]
|
|
|
|
variable = data["variable"]
|
|
|
|
converter = data["converter"] or "default"
|
|
|
|
if variable in used_names:
|
|
|
|
raise ValueError(f"variable name {variable!r} used twice.")
|
|
|
|
used_names.add(variable)
|
|
|
|
yield converter, data["args"] or None, variable
|
|
|
|
pos = m.end()
|
|
|
|
if pos < end:
|
|
|
|
remaining = rule[pos:]
|
|
|
|
if ">" in remaining or "<" in remaining:
|
|
|
|
raise ValueError(f"malformed url rule: {rule!r}")
|
|
|
|
yield None, None, remaining
|
|
|
|
|
|
|
|
|
|
|
|
def extract_path_params(path):
|
|
|
|
"""
|
|
|
|
Extract Flask-style parameters from an URL pattern as Swagger ones.
|
|
|
|
"""
|
|
|
|
params = OrderedDict()
|
|
|
|
for converter, arguments, variable in parse_rule(path):
|
|
|
|
if not converter:
|
|
|
|
continue
|
|
|
|
param = {"name": variable, "in": "path", "required": True}
|
|
|
|
|
|
|
|
if converter in PATH_TYPES:
|
|
|
|
param["type"] = PATH_TYPES[converter]
|
|
|
|
elif converter in current_app.url_map.converters:
|
|
|
|
param["type"] = "string"
|
|
|
|
else:
|
|
|
|
raise ValueError("Unsupported type converter: %s" % converter)
|
|
|
|
params[variable] = param
|
|
|
|
return params
|
|
|
|
|
|
|
|
|
|
|
|
def _param_to_header(param):
|
|
|
|
param.pop("in", None)
|
|
|
|
param.pop("name", None)
|
|
|
|
return _clean_header(param)
|
|
|
|
|
|
|
|
|
|
|
|
def _clean_header(header):
|
|
|
|
if isinstance(header, str):
|
|
|
|
header = {"description": header}
|
|
|
|
typedef = header.get("type", "string")
|
|
|
|
if isinstance(typedef, Hashable) and typedef in PY_TYPES:
|
|
|
|
header["type"] = PY_TYPES[typedef]
|
|
|
|
elif (
|
|
|
|
isinstance(typedef, (list, tuple))
|
|
|
|
and len(typedef) == 1
|
|
|
|
and typedef[0] in PY_TYPES
|
|
|
|
):
|
|
|
|
header["type"] = "array"
|
|
|
|
header["items"] = {"type": PY_TYPES[typedef[0]]}
|
|
|
|
elif hasattr(typedef, "__schema__"):
|
|
|
|
header.update(typedef.__schema__)
|
|
|
|
else:
|
|
|
|
header["type"] = typedef
|
|
|
|
return not_none(header)
|
|
|
|
|
|
|
|
|
|
|
|
def parse_docstring(obj):
|
|
|
|
raw = getdoc(obj)
|
|
|
|
summary = raw.strip(" \n").split("\n")[0].split(".")[0] if raw else None
|
|
|
|
raises = {}
|
|
|
|
details = raw.replace(summary, "").lstrip(". \n").strip(" \n") if raw else None
|
|
|
|
for match in RE_RAISES.finditer(raw or ""):
|
|
|
|
raises[match.group("name")] = match.group("description")
|
|
|
|
if details:
|
|
|
|
details = details.replace(match.group(0), "")
|
|
|
|
parsed = {
|
|
|
|
"raw": raw,
|
|
|
|
"summary": summary or None,
|
|
|
|
"details": details or None,
|
|
|
|
"returns": None,
|
|
|
|
"params": [],
|
|
|
|
"raises": raises,
|
|
|
|
}
|
|
|
|
return parsed
|
|
|
|
|
|
|
|
|
|
|
|
def is_hidden(resource, route_doc=None):
|
|
|
|
"""
|
|
|
|
Determine whether a Resource has been hidden from Swagger documentation
|
|
|
|
i.e. by using Api.doc(False) decorator
|
|
|
|
"""
|
|
|
|
if route_doc is False:
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return hasattr(resource, "__apidoc__") and resource.__apidoc__ is False
|
|
|
|
|
|
|
|
|
|
|
|
def build_request_body_parameters_schema(body_params):
|
|
|
|
"""
|
|
|
|
:param body_params: List of JSON schema of body parameters.
|
|
|
|
:type body_params: list of dict, generated from the json body parameters of a request parser
|
|
|
|
:return dict: The Swagger schema representation of the request body
|
|
|
|
|
|
|
|
:Example:
|
|
|
|
{
|
|
|
|
'name': 'payload',
|
|
|
|
'required': True,
|
|
|
|
'in': 'body',
|
|
|
|
'schema': {
|
|
|
|
'type': 'object',
|
|
|
|
'properties': [
|
|
|
|
'parameter1': {
|
|
|
|
'type': 'integer'
|
|
|
|
},
|
|
|
|
'parameter2': {
|
|
|
|
'type': 'string'
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
|
|
|
|
properties = {}
|
|
|
|
for param in body_params:
|
|
|
|
properties[param["name"]] = {"type": param.get("type", "string")}
|
|
|
|
|
|
|
|
return {
|
|
|
|
"name": "payload",
|
|
|
|
"required": True,
|
|
|
|
"in": "body",
|
|
|
|
"schema": {"type": "object", "properties": properties},
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class Swagger(object):
|
|
|
|
"""
|
|
|
|
A Swagger documentation wrapper for an API instance.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, api):
|
|
|
|
self.api = api
|
|
|
|
self._registered_models = {}
|
|
|
|
|
|
|
|
def as_dict(self):
|
|
|
|
"""
|
|
|
|
Output the specification as a serializable ``dict``.
|
|
|
|
|
|
|
|
:returns: the full Swagger specification in a serializable format
|
|
|
|
:rtype: dict
|
|
|
|
"""
|
|
|
|
basepath = self.api.base_path
|
|
|
|
if len(basepath) > 1 and basepath.endswith("/"):
|
|
|
|
basepath = basepath[:-1]
|
|
|
|
infos = {
|
|
|
|
"title": _v(self.api.title),
|
|
|
|
"version": _v(self.api.version),
|
|
|
|
}
|
|
|
|
if self.api.description:
|
|
|
|
infos["description"] = _v(self.api.description)
|
|
|
|
if self.api.terms_url:
|
|
|
|
infos["termsOfService"] = _v(self.api.terms_url)
|
|
|
|
if self.api.contact and (self.api.contact_email or self.api.contact_url):
|
|
|
|
infos["contact"] = {
|
|
|
|
"name": _v(self.api.contact),
|
|
|
|
"email": _v(self.api.contact_email),
|
|
|
|
"url": _v(self.api.contact_url),
|
|
|
|
}
|
|
|
|
if self.api.license:
|
|
|
|
infos["license"] = {"name": _v(self.api.license)}
|
|
|
|
if self.api.license_url:
|
|
|
|
infos["license"]["url"] = _v(self.api.license_url)
|
|
|
|
|
|
|
|
paths = {}
|
|
|
|
tags = self.extract_tags(self.api)
|
|
|
|
|
|
|
|
# register errors
|
|
|
|
responses = self.register_errors()
|
|
|
|
|
|
|
|
for ns in self.api.namespaces:
|
|
|
|
for resource, urls, route_doc, kwargs in ns.resources:
|
|
|
|
for url in self.api.ns_urls(ns, urls):
|
|
|
|
path = extract_path(url)
|
|
|
|
serialized = self.serialize_resource(
|
|
|
|
ns, resource, url, route_doc=route_doc, **kwargs
|
|
|
|
)
|
|
|
|
paths[path] = serialized
|
|
|
|
|
|
|
|
# register all models if required
|
|
|
|
if current_app.config["RESTX_INCLUDE_ALL_MODELS"]:
|
|
|
|
for m in self.api.models:
|
|
|
|
self.register_model(m)
|
|
|
|
|
|
|
|
# merge in the top-level authorizations
|
|
|
|
for ns in self.api.namespaces:
|
|
|
|
if ns.authorizations:
|
|
|
|
if self.api.authorizations is None:
|
|
|
|
self.api.authorizations = {}
|
|
|
|
self.api.authorizations = merge(
|
|
|
|
self.api.authorizations, ns.authorizations
|
|
|
|
)
|
|
|
|
|
|
|
|
specs = {
|
|
|
|
"swagger": "2.0",
|
|
|
|
"basePath": basepath,
|
|
|
|
"paths": not_none_sorted(paths),
|
|
|
|
"info": infos,
|
|
|
|
"produces": list(self.api.representations.keys()),
|
|
|
|
"consumes": ["application/json"],
|
|
|
|
"securityDefinitions": self.api.authorizations or None,
|
|
|
|
"security": self.security_requirements(self.api.security) or None,
|
|
|
|
"tags": tags,
|
|
|
|
"definitions": self.serialize_definitions() or None,
|
|
|
|
"responses": responses or None,
|
|
|
|
"host": self.get_host(),
|
|
|
|
}
|
|
|
|
return not_none(specs)
|
|
|
|
|
|
|
|
def get_host(self):
|
|
|
|
hostname = current_app.config.get("SERVER_NAME", None) or None
|
|
|
|
if hostname and self.api.blueprint and self.api.blueprint.subdomain:
|
|
|
|
hostname = ".".join((self.api.blueprint.subdomain, hostname))
|
|
|
|
return hostname
|
|
|
|
|
|
|
|
def extract_tags(self, api):
|
|
|
|
tags = []
|
|
|
|
by_name = {}
|
|
|
|
for tag in api.tags:
|
|
|
|
if isinstance(tag, str):
|
|
|
|
tag = {"name": tag}
|
|
|
|
elif isinstance(tag, (list, tuple)):
|
|
|
|
tag = {"name": tag[0], "description": tag[1]}
|
|
|
|
elif isinstance(tag, dict) and "name" in tag:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
raise ValueError("Unsupported tag format for {0}".format(tag))
|
|
|
|
tags.append(tag)
|
|
|
|
by_name[tag["name"]] = tag
|
|
|
|
for ns in api.namespaces:
|
|
|
|
# hide namespaces without any Resources
|
|
|
|
if not ns.resources:
|
|
|
|
continue
|
|
|
|
# hide namespaces with all Resources hidden from Swagger documentation
|
|
|
|
if all(is_hidden(r.resource, route_doc=r.route_doc) for r in ns.resources):
|
|
|
|
continue
|
|
|
|
if ns.name not in by_name:
|
|
|
|
tags.append(
|
|
|
|
{"name": ns.name, "description": ns.description}
|
|
|
|
if ns.description
|
|
|
|
else {"name": ns.name}
|
|
|
|
)
|
|
|
|
elif ns.description:
|
|
|
|
by_name[ns.name]["description"] = ns.description
|
|
|
|
return tags
|
|
|
|
|
|
|
|
def extract_resource_doc(self, resource, url, route_doc=None):
|
|
|
|
route_doc = {} if route_doc is None else route_doc
|
|
|
|
if route_doc is False:
|
|
|
|
return False
|
|
|
|
doc = merge(getattr(resource, "__apidoc__", {}), route_doc)
|
|
|
|
if doc is False:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# ensure unique names for multiple routes to the same resource
|
|
|
|
# provides different Swagger operationId's
|
|
|
|
doc["name"] = (
|
|
|
|
"{}_{}".format(resource.__name__, url) if route_doc else resource.__name__
|
|
|
|
)
|
|
|
|
|
|
|
|
params = merge(self.expected_params(doc), doc.get("params", OrderedDict()))
|
|
|
|
params = merge(params, extract_path_params(url))
|
|
|
|
# Track parameters for late deduplication
|
|
|
|
up_params = {(n, p.get("in", "query")): p for n, p in params.items()}
|
|
|
|
need_to_go_down = set()
|
|
|
|
methods = [m.lower() for m in resource.methods or []]
|
|
|
|
for method in methods:
|
|
|
|
method_doc = doc.get(method, OrderedDict())
|
|
|
|
method_impl = getattr(resource, method)
|
|
|
|
if hasattr(method_impl, "im_func"):
|
|
|
|
method_impl = method_impl.im_func
|
|
|
|
elif hasattr(method_impl, "__func__"):
|
|
|
|
method_impl = method_impl.__func__
|
|
|
|
method_doc = merge(
|
|
|
|
method_doc, getattr(method_impl, "__apidoc__", OrderedDict())
|
|
|
|
)
|
|
|
|
if method_doc is not False:
|
|
|
|
method_doc["docstring"] = parse_docstring(method_impl)
|
|
|
|
method_params = self.expected_params(method_doc)
|
|
|
|
method_params = merge(method_params, method_doc.get("params", {}))
|
|
|
|
inherited_params = OrderedDict(
|
|
|
|
(k, v) for k, v in params.items() if k in method_params
|
|
|
|
)
|
|
|
|
method_doc["params"] = merge(inherited_params, method_params)
|
|
|
|
for name, param in method_doc["params"].items():
|
|
|
|
key = (name, param.get("in", "query"))
|
|
|
|
if key in up_params:
|
|
|
|
need_to_go_down.add(key)
|
|
|
|
doc[method] = method_doc
|
|
|
|
# Deduplicate parameters
|
|
|
|
# For each couple (name, in), if a method overrides it,
|
|
|
|
# we need to move the paramter down to each method
|
|
|
|
if need_to_go_down:
|
|
|
|
for method in methods:
|
|
|
|
method_doc = doc.get(method)
|
|
|
|
if not method_doc:
|
|
|
|
continue
|
|
|
|
params = {
|
|
|
|
(n, p.get("in", "query")): p
|
|
|
|
for n, p in (method_doc["params"] or {}).items()
|
|
|
|
}
|
|
|
|
for key in need_to_go_down:
|
|
|
|
if key not in params:
|
|
|
|
method_doc["params"][key[0]] = up_params[key]
|
|
|
|
doc["params"] = OrderedDict(
|
|
|
|
(k[0], p) for k, p in up_params.items() if k not in need_to_go_down
|
|
|
|
)
|
|
|
|
return doc
|
|
|
|
|
|
|
|
def expected_params(self, doc):
|
|
|
|
params = OrderedDict()
|
|
|
|
if "expect" not in doc:
|
|
|
|
return params
|
|
|
|
|
|
|
|
for expect in doc.get("expect", []):
|
|
|
|
if isinstance(expect, RequestParser):
|
|
|
|
parser_params = OrderedDict(
|
|
|
|
(p["name"], p) for p in expect.__schema__ if p["in"] != "body"
|
|
|
|
)
|
|
|
|
params.update(parser_params)
|
|
|
|
|
|
|
|
body_params = [p for p in expect.__schema__ if p["in"] == "body"]
|
|
|
|
if body_params:
|
|
|
|
params["payload"] = build_request_body_parameters_schema(
|
|
|
|
body_params
|
|
|
|
)
|
|
|
|
elif isinstance(expect, ModelBase):
|
|
|
|
params["payload"] = not_none(
|
|
|
|
{
|
|
|
|
"name": "payload",
|
|
|
|
"required": True,
|
|
|
|
"in": "body",
|
|
|
|
"schema": self.serialize_schema(expect),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
elif isinstance(expect, (list, tuple)):
|
|
|
|
if len(expect) == 2:
|
|
|
|
# this is (payload, description) shortcut
|
|
|
|
model, description = expect
|
|
|
|
params["payload"] = not_none(
|
|
|
|
{
|
|
|
|
"name": "payload",
|
|
|
|
"required": True,
|
|
|
|
"in": "body",
|
|
|
|
"schema": self.serialize_schema(model),
|
|
|
|
"description": description,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
params["payload"] = not_none(
|
|
|
|
{
|
|
|
|
"name": "payload",
|
|
|
|
"required": True,
|
|
|
|
"in": "body",
|
|
|
|
"schema": self.serialize_schema(expect),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
return params
|
|
|
|
|
|
|
|
def register_errors(self):
|
|
|
|
responses = {}
|
|
|
|
for exception, handler in self.api.error_handlers.items():
|
|
|
|
doc = parse_docstring(handler)
|
|
|
|
response = {"description": doc["summary"]}
|
|
|
|
apidoc = getattr(handler, "__apidoc__", {})
|
|
|
|
self.process_headers(response, apidoc)
|
|
|
|
if "responses" in apidoc:
|
|
|
|
_, model, _ = list(apidoc["responses"].values())[0]
|
|
|
|
response["schema"] = self.serialize_schema(model)
|
|
|
|
responses[exception.__name__] = not_none(response)
|
|
|
|
return responses
|
|
|
|
|
|
|
|
def serialize_resource(self, ns, resource, url, route_doc=None, **kwargs):
|
|
|
|
doc = self.extract_resource_doc(resource, url, route_doc=route_doc)
|
|
|
|
if doc is False:
|
|
|
|
return
|
|
|
|
path = {"parameters": self.parameters_for(doc) or None}
|
|
|
|
for method in [m.lower() for m in resource.methods or []]:
|
|
|
|
methods = [m.lower() for m in kwargs.get("methods", [])]
|
|
|
|
if doc[method] is False or methods and method not in methods:
|
|
|
|
continue
|
|
|
|
path[method] = self.serialize_operation(doc, method)
|
|
|
|
path[method]["tags"] = [ns.name]
|
|
|
|
return not_none(path)
|
|
|
|
|
|
|
|
def serialize_operation(self, doc, method):
|
|
|
|
operation = {
|
|
|
|
"responses": self.responses_for(doc, method) or None,
|
|
|
|
"summary": doc[method]["docstring"]["summary"],
|
|
|
|
"description": self.description_for(doc, method) or None,
|
|
|
|
"operationId": self.operation_id_for(doc, method),
|
|
|
|
"parameters": self.parameters_for(doc[method]) or None,
|
|
|
|
"security": self.security_for(doc, method),
|
|
|
|
}
|
|
|
|
# Handle 'produces' mimetypes documentation
|
|
|
|
if "produces" in doc[method]:
|
|
|
|
operation["produces"] = doc[method]["produces"]
|
|
|
|
# Handle deprecated annotation
|
|
|
|
if doc.get("deprecated") or doc[method].get("deprecated"):
|
|
|
|
operation["deprecated"] = True
|
|
|
|
# Handle form exceptions:
|
|
|
|
doc_params = list(doc.get("params", {}).values())
|
|
|
|
all_params = doc_params + (operation["parameters"] or [])
|
|
|
|
if all_params and any(p["in"] == "formData" for p in all_params):
|
|
|
|
if any(p["type"] == "file" for p in all_params):
|
|
|
|
operation["consumes"] = ["multipart/form-data"]
|
|
|
|
else:
|
|
|
|
operation["consumes"] = [
|
|
|
|
"application/x-www-form-urlencoded",
|
|
|
|
"multipart/form-data",
|
|
|
|
]
|
|
|
|
operation.update(self.vendor_fields(doc, method))
|
|
|
|
return not_none(operation)
|
|
|
|
|
|
|
|
def vendor_fields(self, doc, method):
|
|
|
|
"""
|
|
|
|
Extract custom 3rd party Vendor fields prefixed with ``x-``
|
|
|
|
|
|
|
|
See: https://swagger.io/specification/#specification-extensions
|
|
|
|
"""
|
|
|
|
return dict(
|
|
|
|
(k if k.startswith("x-") else "x-{0}".format(k), v)
|
|
|
|
for k, v in doc[method].get("vendor", {}).items()
|
|
|
|
)
|
|
|
|
|
|
|
|
def description_for(self, doc, method):
|
|
|
|
"""Extract the description metadata and fallback on the whole docstring"""
|
|
|
|
parts = []
|
|
|
|
if "description" in doc:
|
|
|
|
parts.append(doc["description"] or "")
|
|
|
|
if method in doc and "description" in doc[method]:
|
|
|
|
parts.append(doc[method]["description"])
|
|
|
|
if doc[method]["docstring"]["details"]:
|
|
|
|
parts.append(doc[method]["docstring"]["details"])
|
|
|
|
|
|
|
|
return "\n".join(parts).strip()
|
|
|
|
|
|
|
|
def operation_id_for(self, doc, method):
|
|
|
|
"""Extract the operation id"""
|
|
|
|
return (
|
|
|
|
doc[method]["id"]
|
|
|
|
if "id" in doc[method]
|
|
|
|
else self.api.default_id(doc["name"], method)
|
|
|
|
)
|
|
|
|
|
|
|
|
def parameters_for(self, doc):
|
|
|
|
params = []
|
|
|
|
for name, param in doc["params"].items():
|
|
|
|
param["name"] = name
|
|
|
|
if "type" not in param and "schema" not in param:
|
|
|
|
param["type"] = "string"
|
|
|
|
if "in" not in param:
|
|
|
|
param["in"] = "query"
|
|
|
|
|
|
|
|
if "type" in param and "schema" not in param:
|
|
|
|
ptype = param.get("type", None)
|
|
|
|
if isinstance(ptype, (list, tuple)):
|
|
|
|
typ = ptype[0]
|
|
|
|
param["type"] = "array"
|
|
|
|
param["items"] = {"type": PY_TYPES.get(typ, typ)}
|
|
|
|
|
|
|
|
elif isinstance(ptype, (type, type(None))) and ptype in PY_TYPES:
|
|
|
|
param["type"] = PY_TYPES[ptype]
|
|
|
|
|
|
|
|
params.append(param)
|
|
|
|
|
|
|
|
# Handle fields mask
|
|
|
|
mask = doc.get("__mask__")
|
|
|
|
if mask and current_app.config["RESTX_MASK_SWAGGER"]:
|
|
|
|
param = {
|
|
|
|
"name": current_app.config["RESTX_MASK_HEADER"],
|
|
|
|
"in": "header",
|
|
|
|
"type": "string",
|
|
|
|
"format": "mask",
|
|
|
|
"description": "An optional fields mask",
|
|
|
|
}
|
|
|
|
if isinstance(mask, str):
|
|
|
|
param["default"] = mask
|
|
|
|
params.append(param)
|
|
|
|
|
|
|
|
return params
|
|
|
|
|
|
|
|
def responses_for(self, doc, method):
|
|
|
|
# TODO: simplify/refactor responses/model handling
|
|
|
|
responses = {}
|
|
|
|
|
|
|
|
for d in doc, doc[method]:
|
|
|
|
if "responses" in d:
|
|
|
|
for code, response in d["responses"].items():
|
|
|
|
code = str(code)
|
|
|
|
if isinstance(response, str):
|
|
|
|
description = response
|
|
|
|
model = None
|
|
|
|
kwargs = {}
|
|
|
|
elif len(response) == 3:
|
|
|
|
description, model, kwargs = response
|
|
|
|
elif len(response) == 2:
|
|
|
|
description, model = response
|
|
|
|
kwargs = {}
|
|
|
|
else:
|
|
|
|
raise ValueError("Unsupported response specification")
|
|
|
|
description = description or DEFAULT_RESPONSE_DESCRIPTION
|
|
|
|
if code in responses:
|
|
|
|
responses[code].update(description=description)
|
|
|
|
else:
|
|
|
|
responses[code] = {"description": description}
|
|
|
|
if model:
|
|
|
|
schema = self.serialize_schema(model)
|
|
|
|
envelope = kwargs.get("envelope")
|
|
|
|
if envelope:
|
|
|
|
schema = {"properties": {envelope: schema}}
|
|
|
|
responses[code]["schema"] = schema
|
|
|
|
self.process_headers(
|
|
|
|
responses[code], doc, method, kwargs.get("headers")
|
|
|
|
)
|
|
|
|
if "model" in d:
|
|
|
|
code = str(d.get("default_code", HTTPStatus.OK))
|
|
|
|
if code not in responses:
|
|
|
|
responses[code] = self.process_headers(
|
|
|
|
DEFAULT_RESPONSE.copy(), doc, method
|
|
|
|
)
|
|
|
|
responses[code]["schema"] = self.serialize_schema(d["model"])
|
|
|
|
|
|
|
|
if "docstring" in d:
|
|
|
|
for name, description in d["docstring"]["raises"].items():
|
|
|
|
for exception, handler in self.api.error_handlers.items():
|
|
|
|
error_responses = getattr(handler, "__apidoc__", {}).get(
|
|
|
|
"responses", {}
|
|
|
|
)
|
|
|
|
code = (
|
|
|
|
str(list(error_responses.keys())[0])
|
|
|
|
if error_responses
|
|
|
|
else None
|
|
|
|
)
|
|
|
|
if code and exception.__name__ == name:
|
|
|
|
responses[code] = {"$ref": "#/responses/{0}".format(name)}
|
|
|
|
break
|
|
|
|
|
|
|
|
if not responses:
|
|
|
|
responses[str(HTTPStatus.OK.value)] = self.process_headers(
|
|
|
|
DEFAULT_RESPONSE.copy(), doc, method
|
|
|
|
)
|
|
|
|
return responses
|
|
|
|
|
|
|
|
def process_headers(self, response, doc, method=None, headers=None):
|
|
|
|
method_doc = doc.get(method, {})
|
|
|
|
if "headers" in doc or "headers" in method_doc or headers:
|
|
|
|
response["headers"] = dict(
|
|
|
|
(k, _clean_header(v))
|
|
|
|
for k, v in itertools.chain(
|
|
|
|
doc.get("headers", {}).items(),
|
|
|
|
method_doc.get("headers", {}).items(),
|
|
|
|
(headers or {}).items(),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return response
|
|
|
|
|
|
|
|
def serialize_definitions(self):
|
|
|
|
return dict(
|
|
|
|
(name, model.__schema__) for name, model in self._registered_models.items()
|
|
|
|
)
|
|
|
|
|
|
|
|
def serialize_schema(self, model):
|
|
|
|
if isinstance(model, (list, tuple)):
|
|
|
|
model = model[0]
|
|
|
|
return {
|
|
|
|
"type": "array",
|
|
|
|
"items": self.serialize_schema(model),
|
|
|
|
}
|
|
|
|
|
|
|
|
elif isinstance(model, ModelBase):
|
|
|
|
self.register_model(model)
|
|
|
|
return ref(model)
|
|
|
|
|
|
|
|
elif isinstance(model, str):
|
|
|
|
self.register_model(model)
|
|
|
|
return ref(model)
|
|
|
|
|
|
|
|
elif isclass(model) and issubclass(model, fields.Raw):
|
|
|
|
return self.serialize_schema(model())
|
|
|
|
|
|
|
|
elif isinstance(model, fields.Raw):
|
|
|
|
return model.__schema__
|
|
|
|
|
|
|
|
elif isinstance(model, (type, type(None))) and model in PY_TYPES:
|
|
|
|
return {"type": PY_TYPES[model]}
|
|
|
|
|
|
|
|
raise ValueError("Model {0} not registered".format(model))
|
|
|
|
|
|
|
|
def register_model(self, model):
|
|
|
|
name = model.name if isinstance(model, ModelBase) else model
|
|
|
|
if name not in self.api.models:
|
|
|
|
raise ValueError("Model {0} not registered".format(name))
|
|
|
|
specs = self.api.models[name]
|
|
|
|
if name in self._registered_models:
|
|
|
|
return ref(model)
|
|
|
|
self._registered_models[name] = specs
|
|
|
|
if isinstance(specs, ModelBase):
|
|
|
|
for parent in specs.__parents__:
|
|
|
|
self.register_model(parent)
|
|
|
|
if isinstance(specs, (Model, OrderedModel)):
|
|
|
|
for field in specs.values():
|
|
|
|
self.register_field(field)
|
|
|
|
return ref(model)
|
|
|
|
|
|
|
|
def register_field(self, field):
|
|
|
|
if isinstance(field, fields.Polymorph):
|
|
|
|
for model in field.mapping.values():
|
|
|
|
self.register_model(model)
|
|
|
|
elif isinstance(field, fields.Nested):
|
|
|
|
self.register_model(field.nested)
|
|
|
|
elif isinstance(field, (fields.List, fields.Wildcard)):
|
|
|
|
self.register_field(field.container)
|
|
|
|
|
|
|
|
def security_for(self, doc, method):
|
|
|
|
security = None
|
|
|
|
if "security" in doc:
|
|
|
|
auth = doc["security"]
|
|
|
|
security = self.security_requirements(auth)
|
|
|
|
|
|
|
|
if "security" in doc[method]:
|
|
|
|
auth = doc[method]["security"]
|
|
|
|
security = self.security_requirements(auth)
|
|
|
|
|
|
|
|
return security
|
|
|
|
|
|
|
|
def security_requirements(self, value):
|
|
|
|
if isinstance(value, (list, tuple)):
|
|
|
|
return [self.security_requirement(v) for v in value]
|
|
|
|
elif value:
|
|
|
|
requirement = self.security_requirement(value)
|
|
|
|
return [requirement] if requirement else None
|
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
|
|
|
def security_requirement(self, value):
|
|
|
|
if isinstance(value, (str)):
|
|
|
|
return {value: []}
|
|
|
|
elif isinstance(value, dict):
|
|
|
|
return dict(
|
|
|
|
(k, v if isinstance(v, (list, tuple)) else [v])
|
|
|
|
for k, v in value.items()
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
return None
|