1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
|
"""Deal with OpenAPI v3."""
import json
import re
from jinja2 import Environment, FileSystemLoader
class OpenAPI: # pylint: disable=too-few-public-methods
"""Create OpenAPI skeleton."""
def __init__(self, napp_path, tpl_path):
"""Instantiate an OpenAPI object.
Args:
napp_path (string): Napp directory
tlp_path (string): File name from template
"""
self._napp_path = napp_path
self._template = tpl_path / 'openapi.yml.template'
self._api_file = napp_path / 'openapi.yml'
self._napp_dict = self._parse_napp_metadata()
# Data for a path
self._summary = None
self._description = None
# Part of template context
self._paths = {}
def render_template(self):
"""Render and save API doc in openapi.yml."""
self._parse_paths()
context = dict(napp=self._napp_dict, paths=self._paths)
self._save(context)
def _parse_napp_metadata(self):
"""Return a NApp metadata file."""
filename = self._napp_path / 'kytos.json'
with open(filename, encoding='utf-8') as data_file:
data = json.loads(data_file.read())
return data
def _parse_paths(self):
main_file = self._napp_path / 'main.py'
code = main_file.open().read()
return self._parse_decorated_functions(code)
def _parse_decorated_functions(self, code):
"""Return URL rule, HTTP methods and docstring."""
matches = re.finditer(r"""
# @rest decorators
(?P<decorators>
(?:@rest\(.+?\)\n)+ # one or more @rest decorators inside
)
# docstring delimited by 3 double quotes
.+?"{3}(?P<docstring>.+?)"{3}
""", code, re.VERBOSE | re.DOTALL)
for function_match in matches:
m_dict = function_match.groupdict()
self._parse_docstring(m_dict['docstring'])
self._add_function_paths(m_dict['decorators'])
def _get_absolute_rule(self, rule):
napp_prefix = "/api/{username}/{name}/"
relative_rule = rule[1:] if rule.startswith('/') else rule
return napp_prefix.format_map(self._napp_dict) + relative_rule
def _add_function_paths(self, decorators_str):
for rule, parsed_methods in self._parse_decorators(decorators_str):
absolute_rule = self._get_absolute_rule(rule)
path_url = self._rule2path(absolute_rule)
path_methods = self._paths.setdefault(path_url, {})
self._add_methods(parsed_methods, path_methods)
def _parse_docstring(self, docstring):
"""Parse the method docstring."""
match = re.match(r"""
# Following PEP 257
\s* (?P<summary>[^\n]+?) \s* # First line
( # Description and YAML are optional
(\n \s*){2} # Blank line
# Description (optional)
(
(?!-{3,}) # Don't use YAML as description
\s* (?P<description>.+?) \s* # Third line and maybe others
(?=-{3,})? # Stop if "---" is found
)?
# YAML spec (optional) **currently not used**
(
-{3,}\n # "---" begins yaml spec
(?P<open_api>.+)
)?
)?
$""", docstring, re.VERBOSE | re.DOTALL)
summary = 'TODO write the summary.'
description = 'TODO write/remove the description'
if match:
m_dict = match.groupdict()
summary = m_dict['summary']
if m_dict['description']:
description = re.sub(r'(\s|\n){2,}', ' ',
m_dict['description'])
self._summary = summary
self._description = description
def _parse_decorators(self, decorators_str):
matches = re.finditer(r"""
@rest\(
## Endpoint rule
(?P<quote>['"]) # inside single or double quotes
(?P<rule>.+?)
(?P=quote)
## HTTP methods (optional)
(\s*,\s*
methods=(?P<methods>\[.+?\])
)?
.*?\)\s*$
""", decorators_str, re.VERBOSE)
for match in matches:
rule = match.group('rule')
methods = self._parse_methods(match.group('methods'))
yield rule, methods
@classmethod
def _parse_methods(cls, list_string):
"""Return HTTP method list. Use json for security reasons."""
if list_string is None:
return ('GET',)
# json requires double quotes
json_list = list_string.replace("'", '"')
return json.loads(json_list)
def _add_methods(self, methods, path_methods):
for method in methods:
path_method = dict(summary=self._summary,
description=self._description)
path_methods[method.lower()] = path_method
@classmethod
def _rule2path(cls, rule):
"""Convert relative Flask rule to absolute OpenAPI path."""
typeless = re.sub(r'<\w+?:', '<', rule) # remove Flask types
return typeless.replace('<', '{').replace('>', '}') # <> -> {}
def _read_napp_info(self):
filename = self._napp_path / 'kytos.json'
return json.load(filename.open())
def _save(self, context):
tpl_env = Environment(
loader=FileSystemLoader(str(self._template.parent)),
trim_blocks=True)
content = tpl_env.get_template(
'openapi.yml.template').render(context)
with self._api_file.open('w') as openapi:
openapi.write(content)
|