Merge lp:~jtv/maas/extract-doc-handler into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 2744
Proposed branch: lp:~jtv/maas/extract-doc-handler
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 499 lines (+210/-194)
5 files modified
src/maasserver/api/api.py (+2/-190)
src/maasserver/api/doc_handler.py (+202/-0)
src/maasserver/api/tests/test_describe.py (+1/-1)
src/maasserver/management/commands/generate_api_doc.py (+1/-1)
src/maasserver/urls_api.py (+4/-2)
To merge this branch: bzr merge lp:~jtv/maas/extract-doc-handler
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+231179@code.launchpad.net

Commit message

Extract API handler: documentation.

Description of the change

This isn't quite like the other handlers. But extracting this is still worth the bother because in the end, we'll be able to "extract" the last big chunk (nodes, presumably) by renaming api.py to match its last remaining handlers. And at least for that last big chunk, revision history and pending changes will be unaffected.

We can do better with the doc and doc_handler modules: the docstring that is used as the top matter for the generated API documentation is now in doc_handler.py, which is a bit weird. Good ideas and motivation for cleaning things up further are welcome. I'm doing this not just to make the codebase nicer but to make it easier for others to improve it further!

Jeroen

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Looks OK. Self-approving.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/api.py'
2--- src/maasserver/api/api.py 2014-08-18 11:43:18 +0000
3+++ src/maasserver/api/api.py 2014-08-18 12:27:51 +0000
4@@ -1,51 +1,6 @@
5 # Copyright 2012-2014 Canonical Ltd. This software is licensed under the
6 # GNU Affero General Public License version 3 (see the file LICENSE).
7
8-"""Restful MAAS API.
9-
10-This is the documentation for the API that lets you control and query MAAS.
11-The API is "Restful", which means that you access it through normal HTTP
12-requests.
13-
14-
15-API versions
16-------------
17-
18-At any given time, MAAS may support multiple versions of its API. The version
19-number is included in the API's URL, e.g. /api/1.0/
20-
21-For now, 1.0 is the only supported version.
22-
23-
24-HTTP methods and parameter-passing
25-----------------------------------
26-
27-The following HTTP methods are available for accessing the API:
28- * GET (for information retrieval and queries),
29- * POST (for asking the system to do things),
30- * PUT (for updating objects), and
31- * DELETE (for deleting objects).
32-
33-All methods except DELETE may take parameters, but they are not all passed in
34-the same way. GET parameters are passed in the URL, as is normal with a GET:
35-"/item/?foo=bar" passes parameter "foo" with value "bar".
36-
37-POST and PUT are different. Your request should have MIME type
38-"multipart/form-data"; each part represents one parameter (for POST) or
39-attribute (for PUT). Each part is named after the parameter or attribute it
40-contains, and its contents are the conveyed value.
41-
42-All parameters are in text form. If you need to submit binary data to the
43-API, don't send it as any MIME binary format; instead, send it as a plain text
44-part containing base64-encoded data.
45-
46-Most resources offer a choice of GET or POST operations. In those cases these
47-methods will take one special parameter, called `op`, to indicate what it is
48-you want to do.
49-
50-For example, to list all nodes, you might GET "/api/1.0/nodes/?op=list".
51-"""
52-
53 from __future__ import (
54 absolute_import,
55 print_function,
56@@ -57,25 +12,17 @@
57 __metaclass__ = type
58 __all__ = [
59 "AnonNodesHandler",
60- "api_doc",
61- "api_doc_title",
62 "get_oauth_token",
63 "MaasHandler",
64 "NodeHandler",
65 "NodeMacHandler",
66 "NodeMacsHandler",
67 "NodesHandler",
68- "render_api_docs",
69 "store_node_power_parameters",
70 ]
71
72 from base64 import b64decode
73-from cStringIO import StringIO
74-from functools import partial
75 import httplib
76-from inspect import getdoc
77-import sys
78-from textwrap import dedent
79
80 import bson
81 from django.conf import settings
82@@ -84,19 +31,8 @@
83 ValidationError,
84 )
85 from django.http import HttpResponse
86-from django.shortcuts import (
87- get_object_or_404,
88- render_to_response,
89- )
90-from django.template import RequestContext
91-from docutils import core
92+from django.shortcuts import get_object_or_404
93 from formencode import validators
94-from maasserver.api.doc import (
95- describe_resource,
96- find_api_resources,
97- generate_api_docs,
98- generate_power_types_doc,
99- )
100 from maasserver.api.support import (
101 admin_method,
102 AnonymousOperationsHandler,
103@@ -149,10 +85,7 @@
104 from maasserver.models.nodeprobeddetails import get_single_probed_details
105 from maasserver.node_action import Commission
106 from maasserver.node_constraint_filter_forms import AcquireNodeForm
107-from maasserver.utils import (
108- build_absolute_uri,
109- find_nodegroup,
110- )
111+from maasserver.utils import find_nodegroup
112 from maasserver.utils.orm import get_first
113 import netaddr
114 from piston.utils import rc
115@@ -1198,127 +1131,6 @@
116 status=httplib.OK)
117
118
119-# Title section for the API documentation. Matches in style, format,
120-# etc. whatever render_api_docs() produces, so that you can concatenate
121-# the two.
122-api_doc_title = dedent("""
123- .. _region-controller-api:
124-
125- ========
126- MAAS API
127- ========
128- """.lstrip('\n'))
129-
130-
131-def render_api_docs():
132- """Render ReST documentation for the REST API.
133-
134- This module's docstring forms the head of the documentation; details of
135- the API methods follow.
136-
137- :return: Documentation, in ReST, for the API.
138- :rtype: :class:`unicode`
139- """
140- from maasserver import urls_api as urlconf
141-
142- module = sys.modules[__name__]
143- output = StringIO()
144- line = partial(print, file=output)
145-
146- line(getdoc(module))
147- line()
148- line()
149- line('Operations')
150- line('----------')
151- line()
152-
153- resources = find_api_resources(urlconf)
154- for doc in generate_api_docs(resources):
155- uri_template = doc.resource_uri_template
156- exports = doc.handler.exports.items()
157- # Derive a section title from the name of the handler class.
158- section_name = doc.handler.api_doc_section_name
159- line(section_name)
160- line('=' * len(section_name))
161- line(doc.handler.__doc__)
162- line()
163- line()
164- for (http_method, op), function in sorted(exports):
165- line("``%s %s``" % (http_method, uri_template), end="")
166- if op is not None:
167- line(" ``op=%s``" % op)
168- line()
169- docstring = getdoc(function)
170- if docstring is not None:
171- for docline in docstring.splitlines():
172- line(" ", docline, sep="")
173- line()
174-
175- line()
176- line()
177- line(generate_power_types_doc())
178-
179- return output.getvalue()
180-
181-
182-def reST_to_html_fragment(a_str):
183- parts = core.publish_parts(source=a_str, writer_name='html')
184- return parts['body_pre_docinfo'] + parts['fragment']
185-
186-
187-def api_doc(request):
188- """Get ReST documentation for the REST API."""
189- # Generate the documentation and keep it cached. Note that we can't do
190- # that at the module level because the API doc generation needs Django
191- # fully initialized.
192- return render_to_response(
193- 'maasserver/api_doc.html',
194- {'doc': reST_to_html_fragment(render_api_docs())},
195- context_instance=RequestContext(request))
196-
197-
198-def describe(request):
199- """Return a description of the whole MAAS API.
200-
201- :param request: The http request for this document. This is used to
202- derive the URL where the client expects to see the MAAS API.
203- :return: A JSON object describing the whole MAAS API. Links to the API
204- will use the same scheme and hostname that the client used in
205- `request`.
206- """
207- from maasserver import urls_api as urlconf
208- resources = [
209- describe_resource(resource)
210- for resource in find_api_resources(urlconf)
211- ]
212- # Make all URIs absolute. Clients - and the command-line client in
213- # particular - expect that all handler URIs are absolute, not just paths.
214- # The handler URIs returned by describe_resource() are relative paths.
215- absolute = partial(build_absolute_uri, request)
216- for resource in resources:
217- for handler_type in "anon", "auth":
218- handler = resource[handler_type]
219- if handler is not None:
220- handler["uri"] = absolute(handler["path"])
221- # Package it all up.
222- description = {
223- "doc": "MAAS API",
224- "resources": resources,
225- }
226- # For backward compatibility, add "handlers" as an alias for all not-None
227- # anon and auth handlers in "resources".
228- description["handlers"] = []
229- description["handlers"].extend(
230- resource["anon"] for resource in description["resources"]
231- if resource["anon"] is not None)
232- description["handlers"].extend(
233- resource["auth"] for resource in description["resources"]
234- if resource["auth"] is not None)
235- return HttpResponse(
236- json.dumps(description),
237- content_type="application/json")
238-
239-
240 class IPAddressesHandler(OperationsHandler):
241 """Manage IP addresses allocated by MAAS."""
242 api_doc_section_name = "IP Addresses"
243
244=== added file 'src/maasserver/api/doc_handler.py'
245--- src/maasserver/api/doc_handler.py 1970-01-01 00:00:00 +0000
246+++ src/maasserver/api/doc_handler.py 2014-08-18 12:27:51 +0000
247@@ -0,0 +1,202 @@
248+# Copyright 2014 Canonical Ltd. This software is licensed under the
249+# GNU Affero General Public License version 3 (see the file LICENSE).
250+
251+"""Restful MAAS API.
252+
253+This is the documentation for the API that lets you control and query MAAS.
254+The API is "Restful", which means that you access it through normal HTTP
255+requests.
256+
257+
258+API versions
259+------------
260+
261+At any given time, MAAS may support multiple versions of its API. The version
262+number is included in the API's URL, e.g. /api/1.0/
263+
264+For now, 1.0 is the only supported version.
265+
266+
267+HTTP methods and parameter-passing
268+----------------------------------
269+
270+The following HTTP methods are available for accessing the API:
271+ * GET (for information retrieval and queries),
272+ * POST (for asking the system to do things),
273+ * PUT (for updating objects), and
274+ * DELETE (for deleting objects).
275+
276+All methods except DELETE may take parameters, but they are not all passed in
277+the same way. GET parameters are passed in the URL, as is normal with a GET:
278+"/item/?foo=bar" passes parameter "foo" with value "bar".
279+
280+POST and PUT are different. Your request should have MIME type
281+"multipart/form-data"; each part represents one parameter (for POST) or
282+attribute (for PUT). Each part is named after the parameter or attribute it
283+contains, and its contents are the conveyed value.
284+
285+All parameters are in text form. If you need to submit binary data to the
286+API, don't send it as any MIME binary format; instead, send it as a plain text
287+part containing base64-encoded data.
288+
289+Most resources offer a choice of GET or POST operations. In those cases these
290+methods will take one special parameter, called `op`, to indicate what it is
291+you want to do.
292+
293+For example, to list all nodes, you might GET "/api/1.0/nodes/?op=list".
294+"""
295+
296+from __future__ import (
297+ absolute_import,
298+ print_function,
299+ unicode_literals,
300+ )
301+
302+str = None
303+
304+__metaclass__ = type
305+__all__ = [
306+ 'api_doc',
307+ 'api_doc_title',
308+ 'describe',
309+ 'render_api_docs',
310+ ]
311+
312+from cStringIO import StringIO
313+from functools import partial
314+from inspect import getdoc
315+import sys
316+from textwrap import dedent
317+
318+from django.http import HttpResponse
319+from django.shortcuts import render_to_response
320+from django.template import RequestContext
321+from docutils import core
322+from maasserver.api.doc import (
323+ describe_resource,
324+ find_api_resources,
325+ generate_api_docs,
326+ generate_power_types_doc,
327+ )
328+from maasserver.utils import build_absolute_uri
329+import simplejson as json
330+
331+# Title section for the API documentation. Matches in style, format,
332+# etc. whatever render_api_docs() produces, so that you can concatenate
333+# the two.
334+api_doc_title = dedent("""
335+ .. _region-controller-api:
336+
337+ ========
338+ MAAS API
339+ ========
340+ """.lstrip('\n'))
341+
342+
343+def render_api_docs():
344+ """Render ReST documentation for the REST API.
345+
346+ This module's docstring forms the head of the documentation; details of
347+ the API methods follow.
348+
349+ :return: Documentation, in ReST, for the API.
350+ :rtype: :class:`unicode`
351+ """
352+ from maasserver import urls_api as urlconf
353+
354+ module = sys.modules[__name__]
355+ output = StringIO()
356+ line = partial(print, file=output)
357+
358+ line(getdoc(module))
359+ line()
360+ line()
361+ line('Operations')
362+ line('----------')
363+ line()
364+
365+ resources = find_api_resources(urlconf)
366+ for doc in generate_api_docs(resources):
367+ uri_template = doc.resource_uri_template
368+ exports = doc.handler.exports.items()
369+ # Derive a section title from the name of the handler class.
370+ section_name = doc.handler.api_doc_section_name
371+ line(section_name)
372+ line('=' * len(section_name))
373+ line(doc.handler.__doc__)
374+ line()
375+ line()
376+ for (http_method, op), function in sorted(exports):
377+ line("``%s %s``" % (http_method, uri_template), end="")
378+ if op is not None:
379+ line(" ``op=%s``" % op)
380+ line()
381+ docstring = getdoc(function)
382+ if docstring is not None:
383+ for docline in docstring.splitlines():
384+ line(" ", docline, sep="")
385+ line()
386+
387+ line()
388+ line()
389+ line(generate_power_types_doc())
390+
391+ return output.getvalue()
392+
393+
394+def reST_to_html_fragment(a_str):
395+ parts = core.publish_parts(source=a_str, writer_name='html')
396+ return parts['body_pre_docinfo'] + parts['fragment']
397+
398+
399+def api_doc(request):
400+ """Get ReST documentation for the REST API."""
401+ # Generate the documentation and keep it cached. Note that we can't do
402+ # that at the module level because the API doc generation needs Django
403+ # fully initialized.
404+ return render_to_response(
405+ 'maasserver/api_doc.html',
406+ {'doc': reST_to_html_fragment(render_api_docs())},
407+ context_instance=RequestContext(request))
408+
409+
410+def describe(request):
411+ """Return a description of the whole MAAS API.
412+
413+ :param request: The http request for this document. This is used to
414+ derive the URL where the client expects to see the MAAS API.
415+ :return: A JSON object describing the whole MAAS API. Links to the API
416+ will use the same scheme and hostname that the client used in
417+ `request`.
418+ """
419+ from maasserver import urls_api as urlconf
420+ resources = [
421+ describe_resource(resource)
422+ for resource in find_api_resources(urlconf)
423+ ]
424+ # Make all URIs absolute. Clients - and the command-line client in
425+ # particular - expect that all handler URIs are absolute, not just paths.
426+ # The handler URIs returned by describe_resource() are relative paths.
427+ absolute = partial(build_absolute_uri, request)
428+ for resource in resources:
429+ for handler_type in "anon", "auth":
430+ handler = resource[handler_type]
431+ if handler is not None:
432+ handler["uri"] = absolute(handler["path"])
433+ # Package it all up.
434+ description = {
435+ "doc": "MAAS API",
436+ "resources": resources,
437+ }
438+ # For backward compatibility, add "handlers" as an alias for all not-None
439+ # anon and auth handlers in "resources".
440+ description["handlers"] = []
441+ description["handlers"].extend(
442+ resource["anon"] for resource in description["resources"]
443+ if resource["anon"] is not None)
444+ description["handlers"].extend(
445+ resource["auth"] for resource in description["resources"]
446+ if resource["auth"] is not None)
447+ return HttpResponse(
448+ json.dumps(description),
449+ content_type="application/json")
450
451=== modified file 'src/maasserver/api/tests/test_describe.py'
452--- src/maasserver/api/tests/test_describe.py 2014-08-16 05:43:33 +0000
453+++ src/maasserver/api/tests/test_describe.py 2014-08-18 12:27:51 +0000
454@@ -25,7 +25,7 @@
455 get_script_prefix,
456 )
457 from django.test.client import RequestFactory
458-from maasserver.api.api import describe
459+from maasserver.api.doc_handler import describe
460 from maasserver.testing.factory import factory
461 from maasserver.testing.testcase import MAASServerTestCase
462 from testscenarios import multiply_scenarios
463
464=== modified file 'src/maasserver/management/commands/generate_api_doc.py'
465--- src/maasserver/management/commands/generate_api_doc.py 2014-08-16 05:43:33 +0000
466+++ src/maasserver/management/commands/generate_api_doc.py 2014-08-18 12:27:51 +0000
467@@ -17,7 +17,7 @@
468 ]
469
470 from django.core.management.base import BaseCommand
471-from maasserver.api.api import (
472+from maasserver.api.doc_handler import (
473 api_doc_title,
474 render_api_docs,
475 )
476
477=== modified file 'src/maasserver/urls_api.py'
478--- src/maasserver/urls_api.py 2014-08-18 11:43:18 +0000
479+++ src/maasserver/urls_api.py 2014-08-18 12:27:51 +0000
480@@ -20,8 +20,6 @@
481 )
482 from maasserver.api.account import AccountHandler
483 from maasserver.api.api import (
484- api_doc,
485- describe,
486 IPAddressesHandler,
487 MaasHandler,
488 NodeHandler,
489@@ -48,6 +46,10 @@
490 CommissioningScriptHandler,
491 CommissioningScriptsHandler,
492 )
493+from maasserver.api.doc_handler import (
494+ api_doc,
495+ describe,
496+ )
497 from maasserver.api.files import (
498 FileHandler,
499 FilesHandler,