blob: eb7a0f1c9364a85f2a056bbb9574960241acdf9b [file] [log] [blame]
Joe Gregorio79daca02013-03-29 16:25:52 -04001#!/usr/bin/python
Joe Gregorio20a5aa92011-04-01 17:44:25 -04002#
Craig Citro751b7fb2014-09-23 11:20:38 -07003# Copyright 2014 Google Inc. All Rights Reserved.
Joe Gregorio20a5aa92011-04-01 17:44:25 -04004#
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
Joe Gregorio81d92cc2012-07-09 16:46:02 -040017"""Create documentation for generate API surfaces.
18
19Command-line tool that creates documentation for all APIs listed in discovery.
20The documentation is generated from a combination of the discovery document and
21the generated API surface itself.
22"""
23
Joe Gregorio20a5aa92011-04-01 17:44:25 -040024__author__ = 'jcgregorio@google.com (Joe Gregorio)'
25
Bu Sun Kimc9773042019-07-17 14:03:17 -070026from collections import OrderedDict
Joe Gregorio79daca02013-03-29 16:25:52 -040027import argparse
Bu Sun Kimc9773042019-07-17 14:03:17 -070028import collections
Craig Citro6ae34d72014-08-18 23:10:09 -070029import json
Joe Gregorioafc45f22011-02-20 16:11:28 -050030import os
Joe Gregorioafc45f22011-02-20 16:11:28 -050031import re
Joe Gregorio79daca02013-03-29 16:25:52 -040032import string
Joe Gregorio20a5aa92011-04-01 17:44:25 -040033import sys
Joe Gregorioafc45f22011-02-20 16:11:28 -050034
John Asmuth864311d2014-04-24 15:46:08 -040035from googleapiclient.discovery import DISCOVERY_URI
36from googleapiclient.discovery import build
37from googleapiclient.discovery import build_from_document
Jon Wayne Parrottfd2f99c2016-02-19 16:02:04 -080038from googleapiclient.discovery import UnknownApiNameOrVersion
Igor Maravić22435292017-01-19 22:28:22 +010039from googleapiclient.http import build_http
Joe Gregorio81d92cc2012-07-09 16:46:02 -040040import uritemplate
41
Joe Gregorio81d92cc2012-07-09 16:46:02 -040042CSS = """<style>
43
44body, h1, h2, h3, div, span, p, pre, a {
45 margin: 0;
46 padding: 0;
47 border: 0;
48 font-weight: inherit;
49 font-style: inherit;
50 font-size: 100%;
51 font-family: inherit;
52 vertical-align: baseline;
53}
54
55body {
56 font-size: 13px;
57 padding: 1em;
58}
59
60h1 {
61 font-size: 26px;
62 margin-bottom: 1em;
63}
64
65h2 {
66 font-size: 24px;
67 margin-bottom: 1em;
68}
69
70h3 {
71 font-size: 20px;
72 margin-bottom: 1em;
73 margin-top: 1em;
74}
75
76pre, code {
77 line-height: 1.5;
78 font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
79}
80
81pre {
82 margin-top: 0.5em;
83}
84
85h1, h2, h3, p {
86 font-family: Arial, sans serif;
87}
88
89h1, h2, h3 {
90 border-bottom: solid #CCC 1px;
91}
92
93.toc_element {
94 margin-top: 0.5em;
95}
96
97.firstline {
98 margin-left: 2 em;
99}
100
101.method {
102 margin-top: 1em;
103 border: solid 1px #CCC;
104 padding: 1em;
105 background: #EEE;
106}
107
108.details {
109 font-weight: bold;
110 font-size: 14px;
111}
112
113</style>
114"""
115
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400116METHOD_TEMPLATE = """<div class="method">
117 <code class="details" id="$name">$name($params)</code>
118 <pre>$doc</pre>
119</div>
120"""
121
122COLLECTION_LINK = """<p class="toc_element">
123 <code><a href="$href">$name()</a></code>
124</p>
125<p class="firstline">Returns the $name Resource.</p>
126"""
127
128METHOD_LINK = """<p class="toc_element">
129 <code><a href="#$name">$name($params)</a></code></p>
130<p class="firstline">$firstline</p>"""
131
Joe Gregoriobb964352013-03-03 20:45:29 -0500132BASE = 'docs/dyn'
133
Sai Cheemalapatidf613972016-10-21 13:59:49 -0700134DIRECTORY_URI = 'https://www.googleapis.com/discovery/v1/apis'
Joe Gregoriobb964352013-03-03 20:45:29 -0500135
Joe Gregorio79daca02013-03-29 16:25:52 -0400136parser = argparse.ArgumentParser(description=__doc__)
Joe Gregoriobb964352013-03-03 20:45:29 -0500137
Joe Gregorio79daca02013-03-29 16:25:52 -0400138parser.add_argument('--discovery_uri_template', default=DISCOVERY_URI,
139 help='URI Template for discovery.')
Joe Gregoriobb964352013-03-03 20:45:29 -0500140
Joe Gregorio79daca02013-03-29 16:25:52 -0400141parser.add_argument('--discovery_uri', default='',
142 help=('URI of discovery document. If supplied then only '
143 'this API will be documented.'))
Joe Gregoriobb964352013-03-03 20:45:29 -0500144
Joe Gregorio79daca02013-03-29 16:25:52 -0400145parser.add_argument('--directory_uri', default=DIRECTORY_URI,
146 help=('URI of directory document. Unused if --discovery_uri'
147 ' is supplied.'))
Joe Gregoriobb964352013-03-03 20:45:29 -0500148
Joe Gregorio79daca02013-03-29 16:25:52 -0400149parser.add_argument('--dest', default=BASE,
150 help='Directory name to write documents into.')
151
Joe Gregoriobb964352013-03-03 20:45:29 -0500152
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400153
154def safe_version(version):
155 """Create a safe version of the verion string.
156
157 Needed so that we can distinguish between versions
158 and sub-collections in URIs. I.e. we don't want
159 adsense_v1.1 to refer to the '1' collection in the v1
160 version of the adsense api.
161
162 Args:
163 version: string, The version string.
164 Returns:
165 The string with '.' replaced with '_'.
166 """
167
168 return version.replace('.', '_')
169
170
171def unsafe_version(version):
172 """Undoes what safe_version() does.
173
174 See safe_version() for the details.
175
176
177 Args:
178 version: string, The safe version string.
179 Returns:
180 The string with '_' replaced with '.'.
181 """
182
183 return version.replace('_', '.')
184
185
186def method_params(doc):
187 """Document the parameters of a method.
188
189 Args:
190 doc: string, The method's docstring.
191
192 Returns:
193 The method signature as a string.
194 """
195 doclines = doc.splitlines()
196 if 'Args:' in doclines:
197 begin = doclines.index('Args:')
198 if 'Returns:' in doclines[begin+1:]:
199 end = doclines.index('Returns:', begin)
200 args = doclines[begin+1: end]
201 else:
202 args = doclines[begin+1:]
203
204 parameters = []
Thomas Coffee2f245372017-03-27 10:39:26 -0700205 pname = None
206 desc = ''
207 def add_param(pname, desc):
208 if pname is None:
209 return
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400210 if '(required)' not in desc:
211 pname = pname + '=None'
212 parameters.append(pname)
Thomas Coffee2f245372017-03-27 10:39:26 -0700213 for line in args:
214 m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line)
215 if m is None:
216 desc += line
217 continue
218 add_param(pname, desc)
219 pname = m.group(1)
220 desc = m.group(2)
221 add_param(pname, desc)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400222 parameters = ', '.join(parameters)
223 else:
224 parameters = ''
225 return parameters
226
227
228def method(name, doc):
229 """Documents an individual method.
230
231 Args:
232 name: string, Name of the method.
233 doc: string, The methods docstring.
234 """
235
236 params = method_params(doc)
Joe Gregorio79daca02013-03-29 16:25:52 -0400237 return string.Template(METHOD_TEMPLATE).substitute(
238 name=name, params=params, doc=doc)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400239
240
241def breadcrumbs(path, root_discovery):
242 """Create the breadcrumb trail to this page of documentation.
243
244 Args:
245 path: string, Dot separated name of the resource.
246 root_discovery: Deserialized discovery document.
247
248 Returns:
249 HTML with links to each of the parent resources of this resource.
250 """
251 parts = path.split('.')
252
253 crumbs = []
254 accumulated = []
255
256 for i, p in enumerate(parts):
257 prefix = '.'.join(accumulated)
258 # The first time through prefix will be [], so we avoid adding in a
259 # superfluous '.' to prefix.
260 if prefix:
261 prefix += '.'
262 display = p
263 if i == 0:
264 display = root_discovery.get('title', display)
265 crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display))
266 accumulated.append(p)
267
268 return ' . '.join(crumbs)
269
270
271def document_collection(resource, path, root_discovery, discovery, css=CSS):
272 """Document a single collection in an API.
273
274 Args:
275 resource: Collection or service being documented.
276 path: string, Dot separated name of the resource.
277 root_discovery: Deserialized discovery document.
278 discovery: Deserialized discovery document, but just the portion that
279 describes the resource.
280 css: string, The CSS to include in the generated file.
281 """
Joe Gregorioafc45f22011-02-20 16:11:28 -0500282 collections = []
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400283 methods = []
284 resource_name = path.split('.')[-2]
285 html = [
286 '<html><body>',
287 css,
288 '<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery),
289 '<h2>Instance Methods</h2>'
290 ]
291
292 # Which methods are for collections.
Joe Gregorioafc45f22011-02-20 16:11:28 -0500293 for name in dir(resource):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400294 if not name.startswith('_') and callable(getattr(resource, name)):
295 if hasattr(getattr(resource, name), '__is_resource__'):
296 collections.append(name)
297 else:
298 methods.append(name)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500299
Joe Gregorioafc45f22011-02-20 16:11:28 -0500300
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400301 # TOC
302 if collections:
303 for name in collections:
304 if not name.startswith('_') and callable(getattr(resource, name)):
305 href = path + name + '.html'
Joe Gregorio79daca02013-03-29 16:25:52 -0400306 html.append(string.Template(COLLECTION_LINK).substitute(
307 href=href, name=name))
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400308
309 if methods:
310 for name in methods:
311 if not name.startswith('_') and callable(getattr(resource, name)):
312 doc = getattr(resource, name).__doc__
313 params = method_params(doc)
314 firstline = doc.splitlines()[0]
Joe Gregorio79daca02013-03-29 16:25:52 -0400315 html.append(string.Template(METHOD_LINK).substitute(
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400316 name=name, params=params, firstline=firstline))
317
318 if methods:
319 html.append('<h3>Method Details</h3>')
320 for name in methods:
321 dname = name.rsplit('_')[0]
322 html.append(method(name, getattr(resource, name).__doc__))
323
324 html.append('</body></html>')
325
326 return '\n'.join(html)
327
328
329def document_collection_recursive(resource, path, root_discovery, discovery):
330
331 html = document_collection(resource, path, root_discovery, discovery)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500332
Joe Gregoriobb964352013-03-03 20:45:29 -0500333 f = open(os.path.join(FLAGS.dest, path + 'html'), 'w')
Joe Gregoriod67010d2012-11-05 08:57:06 -0500334 f.write(html.encode('utf-8'))
Joe Gregorioafc45f22011-02-20 16:11:28 -0500335 f.close()
336
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400337 for name in dir(resource):
338 if (not name.startswith('_')
339 and callable(getattr(resource, name))
Bu Sun Kim715bd7f2019-06-14 16:50:42 -0700340 and hasattr(getattr(resource, name), '__is_resource__')
341 and discovery != {}):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400342 dname = name.rsplit('_')[0]
343 collection = getattr(resource, name)()
344 document_collection_recursive(collection, path + name + '.', root_discovery,
345 discovery['resources'].get(dname, {}))
346
Joe Gregorioafc45f22011-02-20 16:11:28 -0500347def document_api(name, version):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400348 """Document the given API.
349
350 Args:
351 name: string, Name of the API.
352 version: string, Version of the API.
353 """
Jon Wayne Parrottfd2f99c2016-02-19 16:02:04 -0800354 try:
355 service = build(name, version)
356 except UnknownApiNameOrVersion as e:
357 print 'Warning: {} {} found but could not be built.'.format(name, version)
358 return
359
Igor Maravić22435292017-01-19 22:28:22 +0100360 http = build_http()
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400361 response, content = http.request(
362 uritemplate.expand(
Joe Gregoriobb964352013-03-03 20:45:29 -0500363 FLAGS.discovery_uri_template, {
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400364 'api': name,
365 'apiVersion': version})
366 )
Craig Citro6ae34d72014-08-18 23:10:09 -0700367 discovery = json.loads(content)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400368
369 version = safe_version(version)
370
371 document_collection_recursive(
372 service, '%s_%s.' % (name, version), discovery, discovery)
373
Joe Gregorioafc45f22011-02-20 16:11:28 -0500374
Joe Gregoriobb964352013-03-03 20:45:29 -0500375def document_api_from_discovery_document(uri):
376 """Document the given API.
377
378 Args:
379 uri: string, URI of discovery document.
380 """
Igor Maravić22435292017-01-19 22:28:22 +0100381 http = build_http()
Joe Gregoriobb964352013-03-03 20:45:29 -0500382 response, content = http.request(FLAGS.discovery_uri)
Craig Citro6ae34d72014-08-18 23:10:09 -0700383 discovery = json.loads(content)
Joe Gregoriobb964352013-03-03 20:45:29 -0500384
385 service = build_from_document(discovery)
386
387 name = discovery['version']
388 version = safe_version(discovery['version'])
389
390 document_collection_recursive(
391 service, '%s_%s.' % (name, version), discovery, discovery)
392
393
394if __name__ == '__main__':
Joe Gregorio79daca02013-03-29 16:25:52 -0400395 FLAGS = parser.parse_args(sys.argv[1:])
Joe Gregoriobb964352013-03-03 20:45:29 -0500396 if FLAGS.discovery_uri:
397 document_api_from_discovery_document(FLAGS.discovery_uri)
Joe Gregorio20a5aa92011-04-01 17:44:25 -0400398 else:
Bu Sun Kimc9773042019-07-17 14:03:17 -0700399 api_directory = collections.defaultdict(list)
Igor Maravić22435292017-01-19 22:28:22 +0100400 http = build_http()
Joe Gregoriobb964352013-03-03 20:45:29 -0500401 resp, content = http.request(
402 FLAGS.directory_uri,
403 headers={'X-User-IP': '0.0.0.0'})
404 if resp.status == 200:
Craig Citro6ae34d72014-08-18 23:10:09 -0700405 directory = json.loads(content)['items']
Joe Gregoriobb964352013-03-03 20:45:29 -0500406 for api in directory:
407 document_api(api['name'], api['version'])
Bu Sun Kimc9773042019-07-17 14:03:17 -0700408 api_directory[api['name']].append(api['version'])
409
410 # sort by api name and version number
411 for api in api_directory:
412 api_directory[api] = sorted(api_directory[api])
413 api_directory = OrderedDict(sorted(api_directory.items(), key = lambda x: x[0]))
414
415 markdown = []
416 for api, versions in api_directory.items():
417 markdown.append('## %s' % api)
418 for version in versions:
419 markdown.append('* [%s](http://googleapis.github.io/google-api-python-client/docs/dyn/%s_%s.html)' % (version, api, version))
420 markdown.append('\n')
421
422 with open('docs/dyn/index.md', 'w') as f:
423 f.write('\n'.join(markdown).encode('utf-8'))
424
Joe Gregoriobb964352013-03-03 20:45:29 -0500425 else:
426 sys.exit("Failed to load the discovery document.")