Merge ~lloydwaltersj/maas:fix_oapi_get_config_description into maas:master
- Git
- lp:~lloydwaltersj/maas
- fix_oapi_get_config_description
- Merge into master
Status: | Merged |
---|---|
Approved by: | Jack Lloyd-Walters |
Approved revision: | 2e78d34bd2eee96abe4ceff798b675b7448f29bd |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~lloydwaltersj/maas:fix_oapi_get_config_description |
Merge into: | maas:master |
Diff against target: |
323 lines (+117/-36) 4 files modified
Makefile (+4/-1) src/maasserver/api/doc_oapi.py (+105/-27) src/maasserver/api/tests/test_oapi.py (+3/-7) src/maasserver/templates/openapi.html (+5/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
MAAS Lander | Approve | ||
MAAS Maintainers | Pending | ||
Review via email:
|
Commit message
Add formatting fix for https:/
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Alexsander de Souza (alexsander-souza) : | # |
- dd40ddd... by Jack Lloyd-Walters
-
cleanup and respond to feedback
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_oapi_
STATUS: FAILED
LOG: http://
COMMIT: dd40ddd71042cb5
- eb20fea... by Jack Lloyd-Walters
-
update openapi template
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Nick De Villiers (nickdv99) wrote : | # |
Once this has landed, I have a branch ready for maas.io that should fix the formatting on the frontend as well. Let me know once the YAML file we use for production has been updated
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_oapi_
STATUS: FAILED
LOG: http://
COMMIT: eb20fea54c621b9
- ada201d... by Jack Lloyd-Walters
-
linting
- af06abf... by Jack Lloyd-Walters
-
add offline docs to api page
- 485e6b2... by Jack Lloyd-Walters
-
add todo
- bb74991... by Jack Lloyd-Walters
-
add makefile targets
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_oapi_
STATUS: SUCCESS
COMMIT: 485e6b28a8a7843
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_oapi_
STATUS: SUCCESS
COMMIT: bb749919ff5dec2
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Adam Collard (adam-collard) : | # |
- 2e78d34... by Jack Lloyd-Walters
-
fix makefile name
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b fix_oapi_
STATUS: SUCCESS
COMMIT: 2e78d34bd2eee96
Preview Diff
1 | diff --git a/Makefile b/Makefile | |||
2 | index a22a714..4635d90 100644 | |||
3 | --- a/Makefile | |||
4 | +++ b/Makefile | |||
5 | @@ -259,7 +259,10 @@ api-docs.rst: bin/maas-region src/maasserver/api/doc_handler.py syncdb | |||
6 | 259 | openapi.yaml: bin/maas-region src/maasserver/api/doc_handler.py syncdb | 259 | openapi.yaml: bin/maas-region src/maasserver/api/doc_handler.py syncdb |
7 | 260 | bin/maas-region generate_oapi_spec > $@ | 260 | bin/maas-region generate_oapi_spec > $@ |
8 | 261 | 261 | ||
10 | 262 | doc: api-docs.rst openapi.yaml swagger-css swagger-js | 262 | openapi-doc: openapi.yaml swagger-css swagger-js |
11 | 263 | .PHONY: openapi-doc | ||
12 | 264 | |||
13 | 265 | doc: api-docs.rst openapi-doc | ||
14 | 263 | .PHONY: doc | 266 | .PHONY: doc |
15 | 264 | 267 | ||
16 | 265 | clean-ui: | 268 | clean-ui: |
17 | diff --git a/src/maasserver/api/doc_oapi.py b/src/maasserver/api/doc_oapi.py | |||
18 | index 5ee4d55..e8e3f1b 100644 | |||
19 | --- a/src/maasserver/api/doc_oapi.py | |||
20 | +++ b/src/maasserver/api/doc_oapi.py | |||
21 | @@ -11,8 +11,10 @@ from inspect import getdoc, signature | |||
22 | 11 | import json | 11 | import json |
23 | 12 | import re | 12 | import re |
24 | 13 | from textwrap import dedent | 13 | from textwrap import dedent |
25 | 14 | from typing import Any | ||
26 | 14 | 15 | ||
27 | 15 | from django.http import HttpResponse | 16 | from django.http import HttpResponse |
28 | 17 | from piston3.resource import Resource | ||
29 | 16 | import yaml | 18 | import yaml |
30 | 17 | 19 | ||
31 | 18 | from maasserver.api import support | 20 | from maasserver.api import support |
32 | @@ -29,8 +31,19 @@ PARAM_RE = re.compile( | |||
33 | 29 | r"^{(?P<param>\S+)}$", | 31 | r"^{(?P<param>\S+)}$", |
34 | 30 | ) | 32 | ) |
35 | 31 | 33 | ||
36 | 34 | # https://github.com/canonical/maas.io/issues/806 | ||
37 | 35 | # Match variables enclosed by :, ', ` within a docstring, variables cannot contain spaces or the | ||
38 | 36 | # enclosing character within their definition | ||
39 | 37 | MARKERS = [":", "'", "`"] | ||
40 | 38 | MATCHES = [re.compile(rf" {k}[^ ^{k}]*{k} ") for k in MARKERS] | ||
41 | 32 | 39 | ||
43 | 33 | def landing_page(request): | 40 | NEWLINES = re.compile(r"(?<![:.])[\n]+") |
44 | 41 | WHITESPACE = re.compile(r"\n\s+") | ||
45 | 42 | PUNCTUATION = re.compile(r"([^\w\s])\1+") | ||
46 | 43 | POINTS = ["*", "+", "-"] | ||
47 | 44 | |||
48 | 45 | |||
49 | 46 | def landing_page(request: str) -> HttpResponse: | ||
50 | 34 | """Render a landing page with pointers for the MAAS API. | 47 | """Render a landing page with pointers for the MAAS API. |
51 | 35 | 48 | ||
52 | 36 | :return: An `HttpResponse` containing a JSON page with pointers to both | 49 | :return: An `HttpResponse` containing a JSON page with pointers to both |
53 | @@ -46,7 +59,7 @@ def landing_page(request): | |||
54 | 46 | ) | 59 | ) |
55 | 47 | 60 | ||
56 | 48 | 61 | ||
58 | 49 | def endpoint(request): | 62 | def endpoint(request: str) -> HttpResponse: |
59 | 50 | """Render the OpenApi endpoint. | 63 | """Render the OpenApi endpoint. |
60 | 51 | 64 | ||
61 | 52 | :return: An `HttpResponse` containing a YAML document that complies | 65 | :return: An `HttpResponse` containing a YAML document that complies |
62 | @@ -61,7 +74,7 @@ def endpoint(request): | |||
63 | 61 | ) | 74 | ) |
64 | 62 | 75 | ||
65 | 63 | 76 | ||
67 | 64 | def get_api_landing_page(): | 77 | def get_api_landing_page() -> dict[str, str | Any]: |
68 | 65 | """Return the API landing page""" | 78 | """Return the API landing page""" |
69 | 66 | description = { | 79 | description = { |
70 | 67 | "title": "MAAS API", | 80 | "title": "MAAS API", |
71 | @@ -74,23 +87,29 @@ def get_api_landing_page(): | |||
72 | 74 | "title": "this document", | 87 | "title": "this document", |
73 | 75 | }, | 88 | }, |
74 | 76 | { | 89 | { |
75 | 90 | "path": "/MAAS/docs", | ||
76 | 91 | "rel": "service-doc", | ||
77 | 92 | "type": "text/html", | ||
78 | 93 | "title": "offline MAAS documentation", | ||
79 | 94 | }, | ||
80 | 95 | { | ||
81 | 77 | "path": f"{settings.API_URL_PREFIX}openapi.yaml", | 96 | "path": f"{settings.API_URL_PREFIX}openapi.yaml", |
82 | 78 | "rel": "service-desc", | 97 | "rel": "service-desc", |
83 | 79 | "type": "application/openapi+yaml", | 98 | "type": "application/openapi+yaml", |
85 | 80 | "title": "the API definition", | 99 | "title": "the OpenAPI definition", |
86 | 81 | }, | 100 | }, |
87 | 82 | { | 101 | { |
88 | 83 | "path": "/MAAS/api/docs/", | 102 | "path": "/MAAS/api/docs/", |
89 | 84 | "rel": "service-doc", | 103 | "rel": "service-doc", |
90 | 85 | "type": "text/html", | 104 | "type": "text/html", |
92 | 86 | "title": "the API documentation", | 105 | "title": "OpenAPI documentation", |
93 | 87 | }, | 106 | }, |
94 | 88 | ], | 107 | ], |
95 | 89 | } | 108 | } |
96 | 90 | return description | 109 | return description |
97 | 91 | 110 | ||
98 | 92 | 111 | ||
100 | 93 | def get_api_endpoint(): | 112 | def get_api_endpoint() -> dict[str, str | Any]: |
101 | 94 | """Return the API endpoint""" | 113 | """Return the API endpoint""" |
102 | 95 | description = { | 114 | description = { |
103 | 96 | "openapi": "3.0.0", | 115 | "openapi": "3.0.0", |
104 | @@ -112,7 +131,7 @@ def get_api_endpoint(): | |||
105 | 112 | return description | 131 | return description |
106 | 113 | 132 | ||
107 | 114 | 133 | ||
109 | 115 | def _get_maas_servers(): | 134 | def _get_maas_servers() -> list[dict[str, str]]: |
110 | 116 | """Return a servers defintion of the public-facing MAAS address. | 135 | """Return a servers defintion of the public-facing MAAS address. |
111 | 117 | 136 | ||
112 | 118 | :return: An object describing the MAAS public-facing server. | 137 | :return: An object describing the MAAS public-facing server. |
113 | @@ -129,7 +148,7 @@ def _get_maas_servers(): | |||
114 | 129 | ] | 148 | ] |
115 | 130 | 149 | ||
116 | 131 | 150 | ||
118 | 132 | def _new_path_item(params): | 151 | def _new_path_item(params: list[Any]) -> dict[str, dict[str, Any]]: |
119 | 133 | path_item = {} | 152 | path_item = {} |
120 | 134 | for p in params: | 153 | for p in params: |
121 | 135 | path_item.setdefault("parameters", []).append( | 154 | path_item.setdefault("parameters", []).append( |
122 | @@ -143,17 +162,65 @@ def _new_path_item(params): | |||
123 | 143 | return path_item | 162 | return path_item |
124 | 144 | 163 | ||
125 | 145 | 164 | ||
130 | 146 | def _prettify(doc): | 165 | def _prettify(doc: str) -> str: |
131 | 147 | """Cleans up text by replacing newlines with spaces, so that sentences are | 166 | """Cleans up text by: |
132 | 148 | not broken apart prematurely. | 167 | - Dedenting text to the same depth. |
133 | 149 | Respects paragraphing by not replacing newlines that occur after periods. | 168 | - Respecting paragraphing by not replacing newlines that occur after periods or colons |
134 | 169 | - Removeing duplicate punctuation groups and replaces with singular | ||
135 | 150 | """ | 170 | """ |
137 | 151 | return re.sub("(?<![.\n])[\n]+", " ", dedent(doc)).strip() | 171 | doc = dedent(doc) |
138 | 172 | doc = NEWLINES.sub(" ", doc) | ||
139 | 173 | doc = WHITESPACE.sub("\n", doc) | ||
140 | 174 | doc = PUNCTUATION.sub(r"\1", doc) | ||
141 | 175 | for idx, point in enumerate(POINTS): | ||
142 | 176 | doc = re.sub(rf"\n\s*\{point}", f"\n{' '*2*idx}{point}", doc) | ||
143 | 177 | return doc.strip() | ||
144 | 178 | |||
145 | 179 | |||
146 | 180 | def _contains_variables(doc: str) -> list[str] | None: | ||
147 | 181 | """Search for any instances of :variable:, ''variable'', or `variable`.""" | ||
148 | 182 | for m in MATCHES: | ||
149 | 183 | if (v := m.findall(doc[doc.find(":") :])) and len(v) > 1: | ||
150 | 184 | return v | ||
151 | 185 | |||
152 | 186 | |||
153 | 187 | def _parse_enumerable(doc: str) -> tuple[str, list[str]]: | ||
154 | 188 | """Parse docstring for multiple variables. Clean up and represent as the correct | ||
155 | 189 | form. (https://github.com/canonical/maas.io/issues/806)""" | ||
156 | 190 | enumerable = {} | ||
157 | 191 | if variables := _contains_variables(doc): | ||
158 | 192 | for idx, var in enumerate(variables): | ||
159 | 193 | start_pos = doc.find(var) + len(var) | ||
160 | 194 | end_pos = ( | ||
161 | 195 | len(doc) | ||
162 | 196 | if idx >= len(variables) - 1 | ||
163 | 197 | else doc.find(variables[idx + 1]) | ||
164 | 198 | ) | ||
165 | 199 | var_string = doc[start_pos:end_pos] | ||
166 | 200 | doc = doc.replace(var + var_string, "") | ||
167 | 201 | depth = MARKERS.index(var[1]) | ||
168 | 202 | enumerable[var.strip(var[:2]) or " "] = { | ||
169 | 203 | "description": _parse_enumerable(var_string)[0], | ||
170 | 204 | "point": POINTS[depth], | ||
171 | 205 | } | ||
172 | 206 | doc = "\n".join( | ||
173 | 207 | [f"{doc}"] | ||
174 | 208 | + [ | ||
175 | 209 | f"{v['point']} `{k}` {v['description'].strip('.,')}." | ||
176 | 210 | for k, v in enumerable.items() | ||
177 | 211 | ] | ||
178 | 212 | ) | ||
179 | 213 | return doc, list(enumerable.keys()) | ||
180 | 152 | 214 | ||
181 | 153 | 215 | ||
182 | 154 | def _render_oapi_oper_item( | 216 | def _render_oapi_oper_item( |
185 | 155 | http_method, op, doc, uri_params, function, resources | 217 | http_method: str, |
186 | 156 | ): | 218 | op: str, |
187 | 219 | doc: str, | ||
188 | 220 | uri_params: Any, | ||
189 | 221 | function: object, | ||
190 | 222 | resources: set[Resource], | ||
191 | 223 | ) -> dict[str, str | Any]: | ||
192 | 157 | oper_id = op or support.OperationsResource.crudmap.get(http_method) | 224 | oper_id = op or support.OperationsResource.crudmap.get(http_method) |
193 | 158 | oper_obj = { | 225 | oper_obj = { |
194 | 159 | "operationId": f"{doc.name}_{oper_id}", | 226 | "operationId": f"{doc.name}_{oper_id}", |
195 | @@ -171,9 +238,13 @@ def _render_oapi_oper_item( | |||
196 | 171 | 238 | ||
197 | 172 | 239 | ||
198 | 173 | def _oapi_item_from_docstring( | 240 | def _oapi_item_from_docstring( |
202 | 174 | function, http_method, uri_params, doc, resources | 241 | function: object, |
203 | 175 | ): | 242 | http_method: str, |
204 | 176 | def _type_to_string(schema): | 243 | uri_params: Any, |
205 | 244 | doc: str, | ||
206 | 245 | resources: set[Resource], | ||
207 | 246 | ) -> dict[str, str | Any]: | ||
208 | 247 | def _type_to_string(schema: str) -> str: | ||
209 | 177 | match schema: | 248 | match schema: |
210 | 178 | case "Boolean": | 249 | case "Boolean": |
211 | 179 | return "boolean" | 250 | return "boolean" |
212 | @@ -186,7 +257,7 @@ def _oapi_item_from_docstring( | |||
213 | 186 | case _: | 257 | case _: |
214 | 187 | return "object" | 258 | return "object" |
215 | 188 | 259 | ||
217 | 189 | def _response_pair(ap_dict): | 260 | def _response_pair(ap_dict: dict[str, str | Any]) -> list[str]: |
218 | 190 | status_code = "HTTP Status Code" | 261 | status_code = "HTTP Status Code" |
219 | 191 | status = content = {} | 262 | status = content = {} |
220 | 192 | paired = [] | 263 | paired = [] |
221 | @@ -215,11 +286,15 @@ def _oapi_item_from_docstring( | |||
222 | 215 | ap.parse(docstring) | 286 | ap.parse(docstring) |
223 | 216 | ap_dict = ap.get_dict() | 287 | ap_dict = ap.get_dict() |
224 | 217 | oper_obj["summary"] = ap_dict["description_title"].strip() | 288 | oper_obj["summary"] = ap_dict["description_title"].strip() |
226 | 218 | oper_obj["description"] = _prettify(ap_dict["description"]) | 289 | |
227 | 290 | oper_obj["description"] = _prettify( | ||
228 | 291 | _parse_enumerable(ap_dict["description"])[0] | ||
229 | 292 | ) | ||
230 | 293 | |||
231 | 219 | if "deprecated" in oper_obj["description"].lower(): | 294 | if "deprecated" in oper_obj["description"].lower(): |
232 | 220 | oper_obj["deprecated"] = True | 295 | oper_obj["deprecated"] = True |
233 | 221 | for param in ap_dict["params"]: | 296 | for param in ap_dict["params"]: |
235 | 222 | description = _prettify(param["description_stripped"]) | 297 | description = _parse_enumerable(param["description_stripped"])[0] |
236 | 223 | # LP 2009140 | 298 | # LP 2009140 |
237 | 224 | stripped_name = PARAM_RE.match(param["name"]) | 299 | stripped_name = PARAM_RE.match(param["name"]) |
238 | 225 | name = ( | 300 | name = ( |
239 | @@ -236,7 +311,7 @@ def _oapi_item_from_docstring( | |||
240 | 236 | param_dict = { | 311 | param_dict = { |
241 | 237 | "name": name, | 312 | "name": name, |
242 | 238 | "in": "path" if name in uri_params else "query", | 313 | "in": "path" if name in uri_params else "query", |
244 | 239 | "description": description, | 314 | "description": _prettify(description), |
245 | 240 | "schema": { | 315 | "schema": { |
246 | 241 | "type": _type_to_string(param["type"]), | 316 | "type": _type_to_string(param["type"]), |
247 | 242 | }, | 317 | }, |
248 | @@ -244,10 +319,11 @@ def _oapi_item_from_docstring( | |||
249 | 244 | } | 319 | } |
250 | 245 | oper_obj.setdefault("parameters", []).append(param_dict) | 320 | oper_obj.setdefault("parameters", []).append(param_dict) |
251 | 246 | else: | 321 | else: |
254 | 247 | body.setdefault("properties", {})[name] = { | 322 | params_dict = { |
255 | 248 | "description": description, | 323 | "description": _prettify(description), |
256 | 249 | "type": _type_to_string(param["type"]), | 324 | "type": _type_to_string(param["type"]), |
257 | 250 | } | 325 | } |
258 | 326 | body.setdefault("properties", {})[name] = params_dict | ||
259 | 251 | if required: | 327 | if required: |
260 | 252 | body.setdefault("required", []).append(name) | 328 | body.setdefault("required", []).append(name) |
261 | 253 | 329 | ||
262 | @@ -336,13 +412,15 @@ def _oapi_item_from_docstring( | |||
263 | 336 | return oper_obj | 412 | return oper_obj |
264 | 337 | 413 | ||
265 | 338 | 414 | ||
267 | 339 | def _render_oapi_paths(): | 415 | def _render_oapi_paths() -> dict[str, str | Any]: |
268 | 340 | from maasserver import urls_api as urlconf | 416 | from maasserver import urls_api as urlconf |
269 | 341 | 417 | ||
271 | 342 | def _resource_key(resource): | 418 | def _resource_key(resource: Resource) -> str: |
272 | 343 | return resource.handler.__class__.__name__ | 419 | return resource.handler.__class__.__name__ |
273 | 344 | 420 | ||
275 | 345 | def _export_key(export): | 421 | def _export_key( |
276 | 422 | export: tuple[tuple[str, str], object] | ||
277 | 423 | ) -> tuple[str, object]: | ||
278 | 346 | (http_method, op), function = export | 424 | (http_method, op), function = export |
279 | 347 | return http_method, op or "", function | 425 | return http_method, op or "", function |
280 | 348 | 426 | ||
281 | diff --git a/src/maasserver/api/tests/test_oapi.py b/src/maasserver/api/tests/test_oapi.py | |||
282 | index d22c2cc..c28e18d 100644 | |||
283 | --- a/src/maasserver/api/tests/test_oapi.py | |||
284 | +++ b/src/maasserver/api/tests/test_oapi.py | |||
285 | @@ -138,12 +138,8 @@ represented in ASCII using ``bsondump example.bson`` and is for | |||
286 | 138 | demonstrative purposes.""" | 138 | demonstrative purposes.""" |
287 | 139 | 139 | ||
288 | 140 | after = """\ | 140 | after = """\ |
296 | 141 | Returns system details -- for example, LLDP and ``lshw`` XML dumps. | 141 | Returns system details - for example, LLDP and `lshw` XML dumps. |
297 | 142 | 142 | Returns a `{detail_type: xml, .}` map, where `detail_type` is something like "lldp" or "lshw". | |
298 | 143 | 143 | Note that this is returned as BSON and not JSON. This is for efficiency, but mainly because JSON can't do binary content without applying additional encoding like base-64. The example output below is represented in ASCII using `bsondump example.bson` and is for demonstrative purposes.""" | |
292 | 144 | Returns a ``{detail_type: xml, ...}`` map, where ``detail_type`` is something like "lldp" or "lshw". | ||
293 | 145 | |||
294 | 146 | |||
295 | 147 | Note that this is returned as BSON and not JSON. This is for efficiency, but mainly because JSON can''t do binary content without applying additional encoding like base-64. The example output below is represented in ASCII using ``bsondump example.bson`` and is for demonstrative purposes.""" | ||
299 | 148 | 144 | ||
300 | 149 | self.assertEqual(_prettify(before), after) | 145 | self.assertEqual(_prettify(before), after) |
301 | diff --git a/src/maasserver/templates/openapi.html b/src/maasserver/templates/openapi.html | |||
302 | index 4b75523..23186c8 100644 | |||
303 | --- a/src/maasserver/templates/openapi.html | |||
304 | +++ b/src/maasserver/templates/openapi.html | |||
305 | @@ -4,7 +4,8 @@ | |||
306 | 4 | <head> | 4 | <head> |
307 | 5 | <meta charset="UTF-8"> | 5 | <meta charset="UTF-8"> |
308 | 6 | <title>MAAS API</title> | 6 | <title>MAAS API</title> |
310 | 7 | <link href="/MAAS/r/static/css/main.a4c6517c.css" rel="stylesheet"> | 7 | <!-- TODO: Figure out how to update this programatically --> |
311 | 8 | <link href="/MAAS/r/static/css/main.d1c59af9.css" rel="stylesheet"> | ||
312 | 8 | <style> | 9 | <style> |
313 | 9 | {% include "dist/swagger-ui.css" %} | 10 | {% include "dist/swagger-ui.css" %} |
314 | 10 | </style> | 11 | </style> |
315 | @@ -21,6 +22,9 @@ | |||
316 | 21 | margin: 0 !important; | 22 | margin: 0 !important; |
317 | 22 | max-width: 100% !important; | 23 | max-width: 100% !important; |
318 | 23 | } | 24 | } |
319 | 25 | .swagger-ui .parameters-col_description { | ||
320 | 26 | width: 85% !important | ||
321 | 27 | } | ||
322 | 24 | </style> | 28 | </style> |
323 | 25 | </head> | 29 | </head> |
324 | 26 | 30 |
UNIT TESTS get_config_ description lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas
-b fix_oapi_
STATUS: SUCCESS 06227a22973de28 048c919be1
COMMIT: b69a16ef1a76364