Merge ~lloydwaltersj/maas:fix_oapi_get_config_description into maas:master

Proposed by Jack Lloyd-Walters
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)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
MAAS Maintainers Pending
Review via email: mp+451215@code.launchpad.net

Commit message

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b fix_oapi_get_config_description lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: b69a16ef1a7636406227a22973de28048c919be1

review: Approve
Revision history for this message
Alexsander de Souza (alexsander-souza) :
dd40ddd... by Jack Lloyd-Walters

cleanup and respond to feedback

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b fix_oapi_get_config_description lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/3536/console
COMMIT: dd40ddd71042cb5b118c255163dc7fc6cfa24fab

review: Needs Fixing
eb20fea... by Jack Lloyd-Walters

update openapi template

Revision history for this message
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

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b fix_oapi_get_config_description lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/3539/console
COMMIT: eb20fea54c621b94eb1ffb679448ebae8a4beb55

review: Needs Fixing
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

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b fix_oapi_get_config_description lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 485e6b28a8a7843d071466e4e50ff192fb85bb05

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b fix_oapi_get_config_description lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: bb749919ff5dec223e523a068475d23b1954fe2d

review: Approve
Revision history for this message
Adam Collard (adam-collard) :
2e78d34... by Jack Lloyd-Walters

fix makefile name

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b fix_oapi_get_config_description lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 2e78d34bd2eee96abe4ceff798b675b7448f29bd

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/Makefile b/Makefile
index a22a714..4635d90 100644
--- a/Makefile
+++ b/Makefile
@@ -259,7 +259,10 @@ api-docs.rst: bin/maas-region src/maasserver/api/doc_handler.py syncdb
259openapi.yaml: bin/maas-region src/maasserver/api/doc_handler.py syncdb259openapi.yaml: bin/maas-region src/maasserver/api/doc_handler.py syncdb
260 bin/maas-region generate_oapi_spec > $@260 bin/maas-region generate_oapi_spec > $@
261261
262doc: api-docs.rst openapi.yaml swagger-css swagger-js262openapi-doc: openapi.yaml swagger-css swagger-js
263.PHONY: openapi-doc
264
265doc: api-docs.rst openapi-doc
263.PHONY: doc266.PHONY: doc
264267
265clean-ui:268clean-ui:
diff --git a/src/maasserver/api/doc_oapi.py b/src/maasserver/api/doc_oapi.py
index 5ee4d55..e8e3f1b 100644
--- a/src/maasserver/api/doc_oapi.py
+++ b/src/maasserver/api/doc_oapi.py
@@ -11,8 +11,10 @@ from inspect import getdoc, signature
11import json11import json
12import re12import re
13from textwrap import dedent13from textwrap import dedent
14from typing import Any
1415
15from django.http import HttpResponse16from django.http import HttpResponse
17from piston3.resource import Resource
16import yaml18import yaml
1719
18from maasserver.api import support20from maasserver.api import support
@@ -29,8 +31,19 @@ PARAM_RE = re.compile(
29 r"^{(?P<param>\S+)}$",31 r"^{(?P<param>\S+)}$",
30)32)
3133
34# https://github.com/canonical/maas.io/issues/806
35# Match variables enclosed by :, ', ` within a docstring, variables cannot contain spaces or the
36# enclosing character within their definition
37MARKERS = [":", "'", "`"]
38MATCHES = [re.compile(rf" {k}[^ ^{k}]*{k} ") for k in MARKERS]
3239
33def landing_page(request):40NEWLINES = re.compile(r"(?<![:.])[\n]+")
41WHITESPACE = re.compile(r"\n\s+")
42PUNCTUATION = re.compile(r"([^\w\s])\1+")
43POINTS = ["*", "+", "-"]
44
45
46def landing_page(request: str) -> HttpResponse:
34 """Render a landing page with pointers for the MAAS API.47 """Render a landing page with pointers for the MAAS API.
3548
36 :return: An `HttpResponse` containing a JSON page with pointers to both49 :return: An `HttpResponse` containing a JSON page with pointers to both
@@ -46,7 +59,7 @@ def landing_page(request):
46 )59 )
4760
4861
49def endpoint(request):62def endpoint(request: str) -> HttpResponse:
50 """Render the OpenApi endpoint.63 """Render the OpenApi endpoint.
5164
52 :return: An `HttpResponse` containing a YAML document that complies65 :return: An `HttpResponse` containing a YAML document that complies
@@ -61,7 +74,7 @@ def endpoint(request):
61 )74 )
6275
6376
64def get_api_landing_page():77def get_api_landing_page() -> dict[str, str | Any]:
65 """Return the API landing page"""78 """Return the API landing page"""
66 description = {79 description = {
67 "title": "MAAS API",80 "title": "MAAS API",
@@ -74,23 +87,29 @@ def get_api_landing_page():
74 "title": "this document",87 "title": "this document",
75 },88 },
76 {89 {
90 "path": "/MAAS/docs",
91 "rel": "service-doc",
92 "type": "text/html",
93 "title": "offline MAAS documentation",
94 },
95 {
77 "path": f"{settings.API_URL_PREFIX}openapi.yaml",96 "path": f"{settings.API_URL_PREFIX}openapi.yaml",
78 "rel": "service-desc",97 "rel": "service-desc",
79 "type": "application/openapi+yaml",98 "type": "application/openapi+yaml",
80 "title": "the API definition",99 "title": "the OpenAPI definition",
81 },100 },
82 {101 {
83 "path": "/MAAS/api/docs/",102 "path": "/MAAS/api/docs/",
84 "rel": "service-doc",103 "rel": "service-doc",
85 "type": "text/html",104 "type": "text/html",
86 "title": "the API documentation",105 "title": "OpenAPI documentation",
87 },106 },
88 ],107 ],
89 }108 }
90 return description109 return description
91110
92111
93def get_api_endpoint():112def get_api_endpoint() -> dict[str, str | Any]:
94 """Return the API endpoint"""113 """Return the API endpoint"""
95 description = {114 description = {
96 "openapi": "3.0.0",115 "openapi": "3.0.0",
@@ -112,7 +131,7 @@ def get_api_endpoint():
112 return description131 return description
113132
114133
115def _get_maas_servers():134def _get_maas_servers() -> list[dict[str, str]]:
116 """Return a servers defintion of the public-facing MAAS address.135 """Return a servers defintion of the public-facing MAAS address.
117136
118 :return: An object describing the MAAS public-facing server.137 :return: An object describing the MAAS public-facing server.
@@ -129,7 +148,7 @@ def _get_maas_servers():
129 ]148 ]
130149
131150
132def _new_path_item(params):151def _new_path_item(params: list[Any]) -> dict[str, dict[str, Any]]:
133 path_item = {}152 path_item = {}
134 for p in params:153 for p in params:
135 path_item.setdefault("parameters", []).append(154 path_item.setdefault("parameters", []).append(
@@ -143,17 +162,65 @@ def _new_path_item(params):
143 return path_item162 return path_item
144163
145164
146def _prettify(doc):165def _prettify(doc: str) -> str:
147 """Cleans up text by replacing newlines with spaces, so that sentences are166 """Cleans up text by:
148 not broken apart prematurely.167 - Dedenting text to the same depth.
149 Respects paragraphing by not replacing newlines that occur after periods.168 - Respecting paragraphing by not replacing newlines that occur after periods or colons
169 - Removeing duplicate punctuation groups and replaces with singular
150 """170 """
151 return re.sub("(?<![.\n])[\n]+", " ", dedent(doc)).strip()171 doc = dedent(doc)
172 doc = NEWLINES.sub(" ", doc)
173 doc = WHITESPACE.sub("\n", doc)
174 doc = PUNCTUATION.sub(r"\1", doc)
175 for idx, point in enumerate(POINTS):
176 doc = re.sub(rf"\n\s*\{point}", f"\n{' '*2*idx}{point}", doc)
177 return doc.strip()
178
179
180def _contains_variables(doc: str) -> list[str] | None:
181 """Search for any instances of :variable:, ''variable'', or `variable`."""
182 for m in MATCHES:
183 if (v := m.findall(doc[doc.find(":") :])) and len(v) > 1:
184 return v
185
186
187def _parse_enumerable(doc: str) -> tuple[str, list[str]]:
188 """Parse docstring for multiple variables. Clean up and represent as the correct
189 form. (https://github.com/canonical/maas.io/issues/806)"""
190 enumerable = {}
191 if variables := _contains_variables(doc):
192 for idx, var in enumerate(variables):
193 start_pos = doc.find(var) + len(var)
194 end_pos = (
195 len(doc)
196 if idx >= len(variables) - 1
197 else doc.find(variables[idx + 1])
198 )
199 var_string = doc[start_pos:end_pos]
200 doc = doc.replace(var + var_string, "")
201 depth = MARKERS.index(var[1])
202 enumerable[var.strip(var[:2]) or " "] = {
203 "description": _parse_enumerable(var_string)[0],
204 "point": POINTS[depth],
205 }
206 doc = "\n".join(
207 [f"{doc}"]
208 + [
209 f"{v['point']} `{k}` {v['description'].strip('.,')}."
210 for k, v in enumerable.items()
211 ]
212 )
213 return doc, list(enumerable.keys())
152214
153215
154def _render_oapi_oper_item(216def _render_oapi_oper_item(
155 http_method, op, doc, uri_params, function, resources217 http_method: str,
156):218 op: str,
219 doc: str,
220 uri_params: Any,
221 function: object,
222 resources: set[Resource],
223) -> dict[str, str | Any]:
157 oper_id = op or support.OperationsResource.crudmap.get(http_method)224 oper_id = op or support.OperationsResource.crudmap.get(http_method)
158 oper_obj = {225 oper_obj = {
159 "operationId": f"{doc.name}_{oper_id}",226 "operationId": f"{doc.name}_{oper_id}",
@@ -171,9 +238,13 @@ def _render_oapi_oper_item(
171238
172239
173def _oapi_item_from_docstring(240def _oapi_item_from_docstring(
174 function, http_method, uri_params, doc, resources241 function: object,
175):242 http_method: str,
176 def _type_to_string(schema):243 uri_params: Any,
244 doc: str,
245 resources: set[Resource],
246) -> dict[str, str | Any]:
247 def _type_to_string(schema: str) -> str:
177 match schema:248 match schema:
178 case "Boolean":249 case "Boolean":
179 return "boolean"250 return "boolean"
@@ -186,7 +257,7 @@ def _oapi_item_from_docstring(
186 case _:257 case _:
187 return "object"258 return "object"
188259
189 def _response_pair(ap_dict):260 def _response_pair(ap_dict: dict[str, str | Any]) -> list[str]:
190 status_code = "HTTP Status Code"261 status_code = "HTTP Status Code"
191 status = content = {}262 status = content = {}
192 paired = []263 paired = []
@@ -215,11 +286,15 @@ def _oapi_item_from_docstring(
215 ap.parse(docstring)286 ap.parse(docstring)
216 ap_dict = ap.get_dict()287 ap_dict = ap.get_dict()
217 oper_obj["summary"] = ap_dict["description_title"].strip()288 oper_obj["summary"] = ap_dict["description_title"].strip()
218 oper_obj["description"] = _prettify(ap_dict["description"])289
290 oper_obj["description"] = _prettify(
291 _parse_enumerable(ap_dict["description"])[0]
292 )
293
219 if "deprecated" in oper_obj["description"].lower():294 if "deprecated" in oper_obj["description"].lower():
220 oper_obj["deprecated"] = True295 oper_obj["deprecated"] = True
221 for param in ap_dict["params"]:296 for param in ap_dict["params"]:
222 description = _prettify(param["description_stripped"])297 description = _parse_enumerable(param["description_stripped"])[0]
223 # LP 2009140298 # LP 2009140
224 stripped_name = PARAM_RE.match(param["name"])299 stripped_name = PARAM_RE.match(param["name"])
225 name = (300 name = (
@@ -236,7 +311,7 @@ def _oapi_item_from_docstring(
236 param_dict = {311 param_dict = {
237 "name": name,312 "name": name,
238 "in": "path" if name in uri_params else "query",313 "in": "path" if name in uri_params else "query",
239 "description": description,314 "description": _prettify(description),
240 "schema": {315 "schema": {
241 "type": _type_to_string(param["type"]),316 "type": _type_to_string(param["type"]),
242 },317 },
@@ -244,10 +319,11 @@ def _oapi_item_from_docstring(
244 }319 }
245 oper_obj.setdefault("parameters", []).append(param_dict)320 oper_obj.setdefault("parameters", []).append(param_dict)
246 else:321 else:
247 body.setdefault("properties", {})[name] = {322 params_dict = {
248 "description": description,323 "description": _prettify(description),
249 "type": _type_to_string(param["type"]),324 "type": _type_to_string(param["type"]),
250 }325 }
326 body.setdefault("properties", {})[name] = params_dict
251 if required:327 if required:
252 body.setdefault("required", []).append(name)328 body.setdefault("required", []).append(name)
253329
@@ -336,13 +412,15 @@ def _oapi_item_from_docstring(
336 return oper_obj412 return oper_obj
337413
338414
339def _render_oapi_paths():415def _render_oapi_paths() -> dict[str, str | Any]:
340 from maasserver import urls_api as urlconf416 from maasserver import urls_api as urlconf
341417
342 def _resource_key(resource):418 def _resource_key(resource: Resource) -> str:
343 return resource.handler.__class__.__name__419 return resource.handler.__class__.__name__
344420
345 def _export_key(export):421 def _export_key(
422 export: tuple[tuple[str, str], object]
423 ) -> tuple[str, object]:
346 (http_method, op), function = export424 (http_method, op), function = export
347 return http_method, op or "", function425 return http_method, op or "", function
348426
diff --git a/src/maasserver/api/tests/test_oapi.py b/src/maasserver/api/tests/test_oapi.py
index d22c2cc..c28e18d 100644
--- a/src/maasserver/api/tests/test_oapi.py
+++ b/src/maasserver/api/tests/test_oapi.py
@@ -138,12 +138,8 @@ represented in ASCII using ``bsondump example.bson`` and is for
138demonstrative purposes."""138demonstrative purposes."""
139139
140 after = """\140 after = """\
141Returns system details -- for example, LLDP and ``lshw`` XML dumps.141Returns system details - for example, LLDP and `lshw` XML dumps.
142142Returns a `{detail_type: xml, .}` map, where `detail_type` is something like "lldp" or "lshw".
143143Note 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."""
144Returns a ``{detail_type: xml, ...}`` map, where ``detail_type`` is something like "lldp" or "lshw".
145
146
147Note 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."""
148144
149 self.assertEqual(_prettify(before), after)145 self.assertEqual(_prettify(before), after)
diff --git a/src/maasserver/templates/openapi.html b/src/maasserver/templates/openapi.html
index 4b75523..23186c8 100644
--- a/src/maasserver/templates/openapi.html
+++ b/src/maasserver/templates/openapi.html
@@ -4,7 +4,8 @@
4 <head>4 <head>
5 <meta charset="UTF-8">5 <meta charset="UTF-8">
6 <title>MAAS API</title>6 <title>MAAS API</title>
7 <link href="/MAAS/r/static/css/main.a4c6517c.css" rel="stylesheet">7 <!-- TODO: Figure out how to update this programatically -->
8 <link href="/MAAS/r/static/css/main.d1c59af9.css" rel="stylesheet">
8 <style>9 <style>
9 {% include "dist/swagger-ui.css" %}10 {% include "dist/swagger-ui.css" %}
10 </style>11 </style>
@@ -21,6 +22,9 @@
21 margin: 0 !important;22 margin: 0 !important;
22 max-width: 100% !important;23 max-width: 100% !important;
23 }24 }
25 .swagger-ui .parameters-col_description {
26 width: 85% !important
27 }
24 </style>28 </style>
25 </head>29 </head>
2630

Subscribers

People subscribed via source and target branches