OLD | NEW |
| (Empty) |
1 #!/usr/bin/python | |
2 # | |
3 # Copyright 2014 Google Inc. All Rights Reserved. | |
4 # | |
5 # Licensed under the Apache License, Version 2.0 (the "License"); | |
6 # you may not use this file except in compliance with the License. | |
7 # You may obtain a copy of the License at | |
8 # | |
9 # http://www.apache.org/licenses/LICENSE-2.0 | |
10 # | |
11 # Unless required by applicable law or agreed to in writing, software | |
12 # distributed under the License is distributed on an "AS IS" BASIS, | |
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 # See the License for the specific language governing permissions and | |
15 # limitations under the License. | |
16 | |
17 """Create documentation for generate API surfaces. | |
18 | |
19 Command-line tool that creates documentation for all APIs listed in discovery. | |
20 The documentation is generated from a combination of the discovery document and | |
21 the generated API surface itself. | |
22 """ | |
23 | |
24 __author__ = 'jcgregorio@google.com (Joe Gregorio)' | |
25 | |
26 import argparse | |
27 import json | |
28 import os | |
29 import re | |
30 import string | |
31 import sys | |
32 | |
33 from googleapiclient.discovery import DISCOVERY_URI | |
34 from googleapiclient.discovery import build | |
35 from googleapiclient.discovery import build_from_document | |
36 import httplib2 | |
37 import uritemplate | |
38 | |
39 CSS = """<style> | |
40 | |
41 body, h1, h2, h3, div, span, p, pre, a { | |
42 margin: 0; | |
43 padding: 0; | |
44 border: 0; | |
45 font-weight: inherit; | |
46 font-style: inherit; | |
47 font-size: 100%; | |
48 font-family: inherit; | |
49 vertical-align: baseline; | |
50 } | |
51 | |
52 body { | |
53 font-size: 13px; | |
54 padding: 1em; | |
55 } | |
56 | |
57 h1 { | |
58 font-size: 26px; | |
59 margin-bottom: 1em; | |
60 } | |
61 | |
62 h2 { | |
63 font-size: 24px; | |
64 margin-bottom: 1em; | |
65 } | |
66 | |
67 h3 { | |
68 font-size: 20px; | |
69 margin-bottom: 1em; | |
70 margin-top: 1em; | |
71 } | |
72 | |
73 pre, code { | |
74 line-height: 1.5; | |
75 font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida C
onsole', monospace; | |
76 } | |
77 | |
78 pre { | |
79 margin-top: 0.5em; | |
80 } | |
81 | |
82 h1, h2, h3, p { | |
83 font-family: Arial, sans serif; | |
84 } | |
85 | |
86 h1, h2, h3 { | |
87 border-bottom: solid #CCC 1px; | |
88 } | |
89 | |
90 .toc_element { | |
91 margin-top: 0.5em; | |
92 } | |
93 | |
94 .firstline { | |
95 margin-left: 2 em; | |
96 } | |
97 | |
98 .method { | |
99 margin-top: 1em; | |
100 border: solid 1px #CCC; | |
101 padding: 1em; | |
102 background: #EEE; | |
103 } | |
104 | |
105 .details { | |
106 font-weight: bold; | |
107 font-size: 14px; | |
108 } | |
109 | |
110 </style> | |
111 """ | |
112 | |
113 METHOD_TEMPLATE = """<div class="method"> | |
114 <code class="details" id="$name">$name($params)</code> | |
115 <pre>$doc</pre> | |
116 </div> | |
117 """ | |
118 | |
119 COLLECTION_LINK = """<p class="toc_element"> | |
120 <code><a href="$href">$name()</a></code> | |
121 </p> | |
122 <p class="firstline">Returns the $name Resource.</p> | |
123 """ | |
124 | |
125 METHOD_LINK = """<p class="toc_element"> | |
126 <code><a href="#$name">$name($params)</a></code></p> | |
127 <p class="firstline">$firstline</p>""" | |
128 | |
129 BASE = 'docs/dyn' | |
130 | |
131 DIRECTORY_URI = 'https://www.googleapis.com/discovery/v1/apis?preferred=true' | |
132 | |
133 parser = argparse.ArgumentParser(description=__doc__) | |
134 | |
135 parser.add_argument('--discovery_uri_template', default=DISCOVERY_URI, | |
136 help='URI Template for discovery.') | |
137 | |
138 parser.add_argument('--discovery_uri', default='', | |
139 help=('URI of discovery document. If supplied then only ' | |
140 'this API will be documented.')) | |
141 | |
142 parser.add_argument('--directory_uri', default=DIRECTORY_URI, | |
143 help=('URI of directory document. Unused if --discovery_uri' | |
144 ' is supplied.')) | |
145 | |
146 parser.add_argument('--dest', default=BASE, | |
147 help='Directory name to write documents into.') | |
148 | |
149 | |
150 | |
151 def safe_version(version): | |
152 """Create a safe version of the verion string. | |
153 | |
154 Needed so that we can distinguish between versions | |
155 and sub-collections in URIs. I.e. we don't want | |
156 adsense_v1.1 to refer to the '1' collection in the v1 | |
157 version of the adsense api. | |
158 | |
159 Args: | |
160 version: string, The version string. | |
161 Returns: | |
162 The string with '.' replaced with '_'. | |
163 """ | |
164 | |
165 return version.replace('.', '_') | |
166 | |
167 | |
168 def unsafe_version(version): | |
169 """Undoes what safe_version() does. | |
170 | |
171 See safe_version() for the details. | |
172 | |
173 | |
174 Args: | |
175 version: string, The safe version string. | |
176 Returns: | |
177 The string with '_' replaced with '.'. | |
178 """ | |
179 | |
180 return version.replace('_', '.') | |
181 | |
182 | |
183 def method_params(doc): | |
184 """Document the parameters of a method. | |
185 | |
186 Args: | |
187 doc: string, The method's docstring. | |
188 | |
189 Returns: | |
190 The method signature as a string. | |
191 """ | |
192 doclines = doc.splitlines() | |
193 if 'Args:' in doclines: | |
194 begin = doclines.index('Args:') | |
195 if 'Returns:' in doclines[begin+1:]: | |
196 end = doclines.index('Returns:', begin) | |
197 args = doclines[begin+1: end] | |
198 else: | |
199 args = doclines[begin+1:] | |
200 | |
201 parameters = [] | |
202 for line in args: | |
203 m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line) | |
204 if m is None: | |
205 continue | |
206 pname = m.group(1) | |
207 desc = m.group(2) | |
208 if '(required)' not in desc: | |
209 pname = pname + '=None' | |
210 parameters.append(pname) | |
211 parameters = ', '.join(parameters) | |
212 else: | |
213 parameters = '' | |
214 return parameters | |
215 | |
216 | |
217 def method(name, doc): | |
218 """Documents an individual method. | |
219 | |
220 Args: | |
221 name: string, Name of the method. | |
222 doc: string, The methods docstring. | |
223 """ | |
224 | |
225 params = method_params(doc) | |
226 return string.Template(METHOD_TEMPLATE).substitute( | |
227 name=name, params=params, doc=doc) | |
228 | |
229 | |
230 def breadcrumbs(path, root_discovery): | |
231 """Create the breadcrumb trail to this page of documentation. | |
232 | |
233 Args: | |
234 path: string, Dot separated name of the resource. | |
235 root_discovery: Deserialized discovery document. | |
236 | |
237 Returns: | |
238 HTML with links to each of the parent resources of this resource. | |
239 """ | |
240 parts = path.split('.') | |
241 | |
242 crumbs = [] | |
243 accumulated = [] | |
244 | |
245 for i, p in enumerate(parts): | |
246 prefix = '.'.join(accumulated) | |
247 # The first time through prefix will be [], so we avoid adding in a | |
248 # superfluous '.' to prefix. | |
249 if prefix: | |
250 prefix += '.' | |
251 display = p | |
252 if i == 0: | |
253 display = root_discovery.get('title', display) | |
254 crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display)) | |
255 accumulated.append(p) | |
256 | |
257 return ' . '.join(crumbs) | |
258 | |
259 | |
260 def document_collection(resource, path, root_discovery, discovery, css=CSS): | |
261 """Document a single collection in an API. | |
262 | |
263 Args: | |
264 resource: Collection or service being documented. | |
265 path: string, Dot separated name of the resource. | |
266 root_discovery: Deserialized discovery document. | |
267 discovery: Deserialized discovery document, but just the portion that | |
268 describes the resource. | |
269 css: string, The CSS to include in the generated file. | |
270 """ | |
271 collections = [] | |
272 methods = [] | |
273 resource_name = path.split('.')[-2] | |
274 html = [ | |
275 '<html><body>', | |
276 css, | |
277 '<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery), | |
278 '<h2>Instance Methods</h2>' | |
279 ] | |
280 | |
281 # Which methods are for collections. | |
282 for name in dir(resource): | |
283 if not name.startswith('_') and callable(getattr(resource, name)): | |
284 if hasattr(getattr(resource, name), '__is_resource__'): | |
285 collections.append(name) | |
286 else: | |
287 methods.append(name) | |
288 | |
289 | |
290 # TOC | |
291 if collections: | |
292 for name in collections: | |
293 if not name.startswith('_') and callable(getattr(resource, name)): | |
294 href = path + name + '.html' | |
295 html.append(string.Template(COLLECTION_LINK).substitute( | |
296 href=href, name=name)) | |
297 | |
298 if methods: | |
299 for name in methods: | |
300 if not name.startswith('_') and callable(getattr(resource, name)): | |
301 doc = getattr(resource, name).__doc__ | |
302 params = method_params(doc) | |
303 firstline = doc.splitlines()[0] | |
304 html.append(string.Template(METHOD_LINK).substitute( | |
305 name=name, params=params, firstline=firstline)) | |
306 | |
307 if methods: | |
308 html.append('<h3>Method Details</h3>') | |
309 for name in methods: | |
310 dname = name.rsplit('_')[0] | |
311 html.append(method(name, getattr(resource, name).__doc__)) | |
312 | |
313 html.append('</body></html>') | |
314 | |
315 return '\n'.join(html) | |
316 | |
317 | |
318 def document_collection_recursive(resource, path, root_discovery, discovery): | |
319 | |
320 html = document_collection(resource, path, root_discovery, discovery) | |
321 | |
322 f = open(os.path.join(FLAGS.dest, path + 'html'), 'w') | |
323 f.write(html.encode('utf-8')) | |
324 f.close() | |
325 | |
326 for name in dir(resource): | |
327 if (not name.startswith('_') | |
328 and callable(getattr(resource, name)) | |
329 and hasattr(getattr(resource, name), '__is_resource__')): | |
330 dname = name.rsplit('_')[0] | |
331 collection = getattr(resource, name)() | |
332 document_collection_recursive(collection, path + name + '.', root_discover
y, | |
333 discovery['resources'].get(dname, {})) | |
334 | |
335 def document_api(name, version): | |
336 """Document the given API. | |
337 | |
338 Args: | |
339 name: string, Name of the API. | |
340 version: string, Version of the API. | |
341 """ | |
342 service = build(name, version) | |
343 response, content = http.request( | |
344 uritemplate.expand( | |
345 FLAGS.discovery_uri_template, { | |
346 'api': name, | |
347 'apiVersion': version}) | |
348 ) | |
349 discovery = json.loads(content) | |
350 | |
351 version = safe_version(version) | |
352 | |
353 document_collection_recursive( | |
354 service, '%s_%s.' % (name, version), discovery, discovery) | |
355 | |
356 | |
357 def document_api_from_discovery_document(uri): | |
358 """Document the given API. | |
359 | |
360 Args: | |
361 uri: string, URI of discovery document. | |
362 """ | |
363 http = httplib2.Http() | |
364 response, content = http.request(FLAGS.discovery_uri) | |
365 discovery = json.loads(content) | |
366 | |
367 service = build_from_document(discovery) | |
368 | |
369 name = discovery['version'] | |
370 version = safe_version(discovery['version']) | |
371 | |
372 document_collection_recursive( | |
373 service, '%s_%s.' % (name, version), discovery, discovery) | |
374 | |
375 | |
376 if __name__ == '__main__': | |
377 FLAGS = parser.parse_args(sys.argv[1:]) | |
378 if FLAGS.discovery_uri: | |
379 document_api_from_discovery_document(FLAGS.discovery_uri) | |
380 else: | |
381 http = httplib2.Http() | |
382 resp, content = http.request( | |
383 FLAGS.directory_uri, | |
384 headers={'X-User-IP': '0.0.0.0'}) | |
385 if resp.status == 200: | |
386 directory = json.loads(content)['items'] | |
387 for api in directory: | |
388 document_api(api['name'], api['version']) | |
389 else: | |
390 sys.exit("Failed to load the discovery document.") | |
OLD | NEW |