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