Source code for flask_swag.extractor.base
"""
extractor.base
==============
Base & default implmentation class of extractor.
"""
import io
import inspect
import collections
from flask import Flask
from werkzeug.routing import parse_rule, parse_converter_args
from ..core import PathItem, Operation, Parameter, Response
from ..utils import get_type_base, TYPE_MAP, parse_endpoint, \
merge, normalize_indent
_MISSING = object()
CONVERTER_TYPES = {
'float': float,
'path': str,
'any': str,
'default': str,
'uuid': str,
'int': int,
'string': str,
}
WerkzeugConverter = collections.namedtuple(
'WerkzeugConverter', ['converter', 'args', 'kwargs'])
PathAndParams = collections.namedtuple('PathAndParams', ['path', 'params'])
PathAndPathItem = collections.namedtuple('PathAndPathItem', ['path', 'item'])
[docs]class Extractor(object):
"""
Base class that extract swagger spec from flask application.
You can extract path items from app by using :meth:`extract_paths`
and customize converting method by overriding them.
"""
[docs] def convert_werkzeug_converter(self, name: str,
converter: WerkzeugConverter, ctx: dict):
"""Convert werkzeug converter to swagger parameter object."""
python_type = CONVERTER_TYPES.get(converter.converter, None)
type_base = get_type_base(python_type)
if type_base is None:
return None
return Parameter(name=name, in_="path", required=True, **type_base)
[docs] def convert_annotation(self, name, annotation, ctx: dict):
"""Convert function annotation to swagger parameter object."""
if annotation is None:
return None
if not isinstance(annotation, type):
return None
type_base = None
for available_type in TYPE_MAP:
if issubclass(annotation, available_type):
type_base = get_type_base(available_type)
break
if type_base is None:
return None
return Parameter(name=name, in_='path', **type_base)
[docs] def parse_werkzeug_rule(self, rule: str, ctx: dict) -> PathAndParams:
"""
Convert werkzeug rule to swagger path format and
extract parameter info.
"""
params = {}
with io.StringIO() as buf:
for converter, arguments, variable in parse_rule(rule):
if converter:
if arguments is not None:
args, kwargs = parse_converter_args(arguments)
else:
args = ()
kwargs = {}
params[variable] = WerkzeugConverter(
converter=converter,
args=args,
kwargs=kwargs,
)
buf.write('{')
buf.write(variable)
buf.write('}')
else:
buf.write(variable)
return PathAndParams(buf.getvalue(), params)
[docs] def extract_description(self, view, ctx: dict) -> str:
"""Extract description info from view function."""
doc = getattr(view, '__doc__', None) or None
if not doc:
return None
return normalize_indent(doc)
[docs] def extract_summary(self, view, ctx) -> str:
"""Extract brief description from view function."""
description = self.extract_description(view, ctx)
if not description:
return None
return description.strip().split('\n', 1)[0][:120].strip()
[docs] def extract_param(self, view, name, ctx: dict):
"""Extract path parameters info from view function."""
signature = inspect.signature(view)
if name not in signature.parameters:
return None
parameter = signature.parameters.get(name)
annotation = parameter.annotation
return self.convert_annotation(name, annotation, ctx)
[docs] def default_path_param(self, name, ctx: dict):
return Parameter(
name=name,
in_='path',
required=True,
**get_type_base(str)
)
[docs] def extract_responses(self, view, ctx: dict):
return {
'default': Response(
description=''
),
}
[docs] def build_parameters(self, view, param_info, ctx: dict) -> list:
"""
Build parameters from path params and view params.
path params have higher order.
"""
parameters = []
for name, converter in param_info.items():
parameter = self.convert_werkzeug_converter(name, converter, ctx)
if parameter is None:
parameter = self.extract_param(view, name, ctx)
if parameter is None:
parameter = self.default_path_param(name, ctx)
parameters.append(parameter)
return parameters
[docs] def extract_others(self, view, ctx: dict):
"""Extract other fields from view & context."""
return {}
[docs] def make_operation(self, view, params: dict, ctx: dict):
"""Convert view to swagger opration object."""
description = self.extract_description(view, ctx)
summary = self.extract_summary(view, ctx)
parameters = self.build_parameters(view, params, ctx)
responses = self.extract_responses(view, ctx)
others = self.extract_others(view, merge(ctx, {
'params': params,
}))
kwargs = dict(
description=description,
summary=summary,
parameters=parameters,
responses=responses,
)
kwargs.update(others)
return Operation(**kwargs)
[docs] def make_path_item(self, app: Flask, rule: str, endpoints: dict,
ctx: dict) -> PathAndPathItem:
"""Make path item from rule and endpoints collected by HTTP methods."""
path, params = self.parse_werkzeug_rule(rule, ctx)
operations = {}
for method, endpoint in endpoints.items():
view = app.view_functions[endpoint]
operations[method.lower()] = self.make_operation(
view, params, merge(ctx, {
'endpoint': endpoint,
'path': path,
'method': method,
}))
return PathAndPathItem(
path=path,
item=PathItem(**operations),
)
[docs] def collect_endpoints(self, app: Flask, blueprint=_MISSING, endpoint=None,
exclude_blueprint=_MISSING, exclude_endpoint=None) \
-> dict:
"""Collect endpoints in rules.
:param blueprint: name of blueprints to be collected. :const:`None`
means non-blueprint endpoints. It cat either be list
or string.
:param endpoint: endpoints to be collected. It cat either be list or
string.
:param exclude_blueprint: blueprints not to be collected.
:param exclude_endpoint: endpoint not to be collected.
"""
if blueprint is not _MISSING:
if blueprint is None or isinstance(blueprint, str):
blueprint = (blueprint,)
if isinstance(endpoint, str):
endpoint = (endpoint,)
if exclude_blueprint is not _MISSING:
if exclude_blueprint is None or isinstance(exclude_blueprint, str):
exclude_blueprint = (exclude_blueprint,)
if isinstance(exclude_endpoint, str):
exclude_endpoint = (exclude_endpoint,)
endpoints = {}
for rule in app.url_map.iter_rules():
if blueprint is not _MISSING:
rule_blueprint, rule_endpoint = parse_endpoint(rule.endpoint)
if rule_blueprint not in blueprint:
continue
if endpoint and rule_endpoint not in endpoint:
continue
elif endpoint and rule.endpoint not in endpoint:
continue
if exclude_blueprint is not _MISSING:
rule_blueprint, rule_endpoint = parse_endpoint(rule.endpoint)
if rule_blueprint in exclude_blueprint:
continue
if exclude_endpoint and rule_endpoint in exclude_endpoint:
continue
elif exclude_endpoint and rule.endpoint in exclude_endpoint:
continue
methods = rule.methods.difference({'HEAD', 'OPTIONS'})
method_collection = endpoints.setdefault(rule.rule, {})
for method in methods:
method_collection[method] = rule.endpoint
return endpoints
[docs] def extract_paths(self, app: Flask, blueprint=_MISSING, endpoint=None,
exclude_blueprint=_MISSING, exclude_endpoint=None):
"""Extract path items from flask app.
:param blueprint: name of blueprints to be collected. :const:`None`
means non-blueprint endpoints. It cat either be list
or string.
:param endpoint: endpoints to be collected. It cat either be list or
string.
:param exclude_blueprint: blueprints not to be collected.
:param exclude_endpoint: endpoint not to be collected.
"""
endpoints = self.collect_endpoints(app, blueprint, endpoint,
exclude_blueprint, exclude_endpoint)
paths = {}
for rule, methods in endpoints.items():
ctx = {
'rule': rule,
'methods': methods,
'app': app,
}
path, path_item = self.make_path_item(app, rule, methods, ctx)
paths[path] = path_item
return paths