# -*- coding: utf-8 -*- """Various utilities for parsing OpenAPI operations from docstrings and validating against the OpenAPI spec. """ import re import json import tempfile import subprocess import yaml from apispec.compat import iteritems from apispec import exceptions # from django.contrib.admindocs.utils 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 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() def load_yaml_from_docstring(docstring): """Loads YAML from docstring.""" split_lines = trim_docstring(docstring).split('\n') # Cut YAML from rest of docstring for index, line in enumerate(split_lines): line = line.strip() if line.startswith('---'): cut_from = index break else: return None yaml_string = "\n".join(split_lines[cut_from:]) yaml_string = dedent(yaml_string) return yaml.load(yaml_string) PATH_KEYS = set([ 'get', 'put', 'post', 'delete', 'options', 'head', 'patch', ]) def load_operations_from_docstring(docstring): """Return a dictionary of Swagger operations parsed from a a docstring. """ doc_data = load_yaml_from_docstring(docstring) if doc_data: return {key: val for key, val in iteritems(doc_data) if key in PATH_KEYS or key.startswith('x-')} else: return None def validate_swagger(spec): """Validate the output of an :class:`APISpec` object. Note: Requires installing the node package `swagger-tools`. :raise: SwaggerError if validation fails. """ with tempfile.NamedTemporaryFile(mode='w') as fp: json.dump(spec.to_dict(), fp) fp.seek(0) try: subprocess.check_output( ['swagger-tools', 'validate', fp.name], stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as error: raise exceptions.SwaggerError(error.output.decode('utf-8'))