blob: 636eb5dee1990f63198907a004f22885e5802458 [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"""
Christian Clauss9fdc2b22019-07-22 19:43:21 +020023from __future__ import print_function
Joe Gregorio81d92cc2012-07-09 16:46:02 -040024
Joe Gregorio20a5aa92011-04-01 17:44:25 -040025__author__ = 'jcgregorio@google.com (Joe Gregorio)'
26
Bu Sun Kimc9773042019-07-17 14:03:17 -070027from collections import OrderedDict
Joe Gregorio79daca02013-03-29 16:25:52 -040028import argparse
Bu Sun Kimc9773042019-07-17 14:03:17 -070029import collections
Craig Citro6ae34d72014-08-18 23:10:09 -070030import json
Joe Gregorioafc45f22011-02-20 16:11:28 -050031import os
Joe Gregorioafc45f22011-02-20 16:11:28 -050032import re
Joe Gregorio79daca02013-03-29 16:25:52 -040033import string
Joe Gregorio20a5aa92011-04-01 17:44:25 -040034import sys
Joe Gregorioafc45f22011-02-20 16:11:28 -050035
John Asmuth864311d2014-04-24 15:46:08 -040036from googleapiclient.discovery import DISCOVERY_URI
37from googleapiclient.discovery import build
38from googleapiclient.discovery import build_from_document
Jon Wayne Parrottfd2f99c2016-02-19 16:02:04 -080039from googleapiclient.discovery import UnknownApiNameOrVersion
Igor Maravić22435292017-01-19 22:28:22 +010040from googleapiclient.http import build_http
Joe Gregorio81d92cc2012-07-09 16:46:02 -040041import uritemplate
42
Joe Gregorio81d92cc2012-07-09 16:46:02 -040043CSS = """<style>
44
45body, h1, h2, h3, div, span, p, pre, a {
46 margin: 0;
47 padding: 0;
48 border: 0;
49 font-weight: inherit;
50 font-style: inherit;
51 font-size: 100%;
52 font-family: inherit;
53 vertical-align: baseline;
54}
55
56body {
57 font-size: 13px;
58 padding: 1em;
59}
60
61h1 {
62 font-size: 26px;
63 margin-bottom: 1em;
64}
65
66h2 {
67 font-size: 24px;
68 margin-bottom: 1em;
69}
70
71h3 {
72 font-size: 20px;
73 margin-bottom: 1em;
74 margin-top: 1em;
75}
76
77pre, code {
78 line-height: 1.5;
79 font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
80}
81
82pre {
83 margin-top: 0.5em;
84}
85
86h1, h2, h3, p {
87 font-family: Arial, sans serif;
88}
89
90h1, h2, h3 {
91 border-bottom: solid #CCC 1px;
92}
93
94.toc_element {
95 margin-top: 0.5em;
96}
97
98.firstline {
99 margin-left: 2 em;
100}
101
102.method {
103 margin-top: 1em;
104 border: solid 1px #CCC;
105 padding: 1em;
106 background: #EEE;
107}
108
109.details {
110 font-weight: bold;
111 font-size: 14px;
112}
113
114</style>
115"""
116
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400117METHOD_TEMPLATE = """<div class="method">
118 <code class="details" id="$name">$name($params)</code>
119 <pre>$doc</pre>
120</div>
121"""
122
123COLLECTION_LINK = """<p class="toc_element">
124 <code><a href="$href">$name()</a></code>
125</p>
126<p class="firstline">Returns the $name Resource.</p>
127"""
128
129METHOD_LINK = """<p class="toc_element">
130 <code><a href="#$name">$name($params)</a></code></p>
131<p class="firstline">$firstline</p>"""
132
Joe Gregoriobb964352013-03-03 20:45:29 -0500133BASE = 'docs/dyn'
134
Sai Cheemalapatidf613972016-10-21 13:59:49 -0700135DIRECTORY_URI = 'https://www.googleapis.com/discovery/v1/apis'
Joe Gregoriobb964352013-03-03 20:45:29 -0500136
Joe Gregorio79daca02013-03-29 16:25:52 -0400137parser = argparse.ArgumentParser(description=__doc__)
Joe Gregoriobb964352013-03-03 20:45:29 -0500138
Joe Gregorio79daca02013-03-29 16:25:52 -0400139parser.add_argument('--discovery_uri_template', default=DISCOVERY_URI,
140 help='URI Template for discovery.')
Joe Gregoriobb964352013-03-03 20:45:29 -0500141
Joe Gregorio79daca02013-03-29 16:25:52 -0400142parser.add_argument('--discovery_uri', default='',
143 help=('URI of discovery document. If supplied then only '
144 'this API will be documented.'))
Joe Gregoriobb964352013-03-03 20:45:29 -0500145
Joe Gregorio79daca02013-03-29 16:25:52 -0400146parser.add_argument('--directory_uri', default=DIRECTORY_URI,
147 help=('URI of directory document. Unused if --discovery_uri'
148 ' is supplied.'))
Joe Gregoriobb964352013-03-03 20:45:29 -0500149
Joe Gregorio79daca02013-03-29 16:25:52 -0400150parser.add_argument('--dest', default=BASE,
151 help='Directory name to write documents into.')
152
Joe Gregoriobb964352013-03-03 20:45:29 -0500153
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400154
155def safe_version(version):
156 """Create a safe version of the verion string.
157
158 Needed so that we can distinguish between versions
159 and sub-collections in URIs. I.e. we don't want
160 adsense_v1.1 to refer to the '1' collection in the v1
161 version of the adsense api.
162
163 Args:
164 version: string, The version string.
165 Returns:
166 The string with '.' replaced with '_'.
167 """
168
169 return version.replace('.', '_')
170
171
172def unsafe_version(version):
173 """Undoes what safe_version() does.
174
175 See safe_version() for the details.
176
177
178 Args:
179 version: string, The safe version string.
180 Returns:
181 The string with '_' replaced with '.'.
182 """
183
184 return version.replace('_', '.')
185
186
187def method_params(doc):
188 """Document the parameters of a method.
189
190 Args:
191 doc: string, The method's docstring.
192
193 Returns:
194 The method signature as a string.
195 """
196 doclines = doc.splitlines()
197 if 'Args:' in doclines:
198 begin = doclines.index('Args:')
199 if 'Returns:' in doclines[begin+1:]:
200 end = doclines.index('Returns:', begin)
201 args = doclines[begin+1: end]
202 else:
203 args = doclines[begin+1:]
204
205 parameters = []
Thomas Coffee2f245372017-03-27 10:39:26 -0700206 pname = None
207 desc = ''
208 def add_param(pname, desc):
209 if pname is None:
210 return
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400211 if '(required)' not in desc:
212 pname = pname + '=None'
213 parameters.append(pname)
Thomas Coffee2f245372017-03-27 10:39:26 -0700214 for line in args:
215 m = re.search('^\s+([a-zA-Z0-9_]+): (.*)', line)
216 if m is None:
217 desc += line
218 continue
219 add_param(pname, desc)
220 pname = m.group(1)
221 desc = m.group(2)
222 add_param(pname, desc)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400223 parameters = ', '.join(parameters)
224 else:
225 parameters = ''
226 return parameters
227
228
229def method(name, doc):
230 """Documents an individual method.
231
232 Args:
233 name: string, Name of the method.
234 doc: string, The methods docstring.
235 """
236
237 params = method_params(doc)
Joe Gregorio79daca02013-03-29 16:25:52 -0400238 return string.Template(METHOD_TEMPLATE).substitute(
239 name=name, params=params, doc=doc)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400240
241
242def breadcrumbs(path, root_discovery):
243 """Create the breadcrumb trail to this page of documentation.
244
245 Args:
246 path: string, Dot separated name of the resource.
247 root_discovery: Deserialized discovery document.
248
249 Returns:
250 HTML with links to each of the parent resources of this resource.
251 """
252 parts = path.split('.')
253
254 crumbs = []
255 accumulated = []
256
257 for i, p in enumerate(parts):
258 prefix = '.'.join(accumulated)
259 # The first time through prefix will be [], so we avoid adding in a
260 # superfluous '.' to prefix.
261 if prefix:
262 prefix += '.'
263 display = p
264 if i == 0:
265 display = root_discovery.get('title', display)
266 crumbs.append('<a href="%s.html">%s</a>' % (prefix + p, display))
267 accumulated.append(p)
268
269 return ' . '.join(crumbs)
270
271
272def document_collection(resource, path, root_discovery, discovery, css=CSS):
273 """Document a single collection in an API.
274
275 Args:
276 resource: Collection or service being documented.
277 path: string, Dot separated name of the resource.
278 root_discovery: Deserialized discovery document.
279 discovery: Deserialized discovery document, but just the portion that
280 describes the resource.
281 css: string, The CSS to include in the generated file.
282 """
Joe Gregorioafc45f22011-02-20 16:11:28 -0500283 collections = []
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400284 methods = []
285 resource_name = path.split('.')[-2]
286 html = [
287 '<html><body>',
288 css,
289 '<h1>%s</h1>' % breadcrumbs(path[:-1], root_discovery),
290 '<h2>Instance Methods</h2>'
291 ]
292
293 # Which methods are for collections.
Joe Gregorioafc45f22011-02-20 16:11:28 -0500294 for name in dir(resource):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400295 if not name.startswith('_') and callable(getattr(resource, name)):
296 if hasattr(getattr(resource, name), '__is_resource__'):
297 collections.append(name)
298 else:
299 methods.append(name)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500300
Joe Gregorioafc45f22011-02-20 16:11:28 -0500301
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400302 # TOC
303 if collections:
304 for name in collections:
305 if not name.startswith('_') and callable(getattr(resource, name)):
306 href = path + name + '.html'
Joe Gregorio79daca02013-03-29 16:25:52 -0400307 html.append(string.Template(COLLECTION_LINK).substitute(
308 href=href, name=name))
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400309
310 if methods:
311 for name in methods:
312 if not name.startswith('_') and callable(getattr(resource, name)):
313 doc = getattr(resource, name).__doc__
314 params = method_params(doc)
315 firstline = doc.splitlines()[0]
Joe Gregorio79daca02013-03-29 16:25:52 -0400316 html.append(string.Template(METHOD_LINK).substitute(
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400317 name=name, params=params, firstline=firstline))
318
319 if methods:
320 html.append('<h3>Method Details</h3>')
321 for name in methods:
322 dname = name.rsplit('_')[0]
323 html.append(method(name, getattr(resource, name).__doc__))
324
325 html.append('</body></html>')
326
327 return '\n'.join(html)
328
329
330def document_collection_recursive(resource, path, root_discovery, discovery):
331
332 html = document_collection(resource, path, root_discovery, discovery)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500333
Joe Gregoriobb964352013-03-03 20:45:29 -0500334 f = open(os.path.join(FLAGS.dest, path + 'html'), 'w')
Joe Gregoriod67010d2012-11-05 08:57:06 -0500335 f.write(html.encode('utf-8'))
Joe Gregorioafc45f22011-02-20 16:11:28 -0500336 f.close()
337
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400338 for name in dir(resource):
339 if (not name.startswith('_')
340 and callable(getattr(resource, name))
Bu Sun Kim715bd7f2019-06-14 16:50:42 -0700341 and hasattr(getattr(resource, name), '__is_resource__')
342 and discovery != {}):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400343 dname = name.rsplit('_')[0]
344 collection = getattr(resource, name)()
345 document_collection_recursive(collection, path + name + '.', root_discovery,
346 discovery['resources'].get(dname, {}))
347
Joe Gregorioafc45f22011-02-20 16:11:28 -0500348def document_api(name, version):
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400349 """Document the given API.
350
351 Args:
352 name: string, Name of the API.
353 version: string, Version of the API.
354 """
Jon Wayne Parrottfd2f99c2016-02-19 16:02:04 -0800355 try:
356 service = build(name, version)
357 except UnknownApiNameOrVersion as e:
Christian Clauss9fdc2b22019-07-22 19:43:21 +0200358 print('Warning: {} {} found but could not be built.'.format(name, version))
Jon Wayne Parrottfd2f99c2016-02-19 16:02:04 -0800359 return
360
Igor Maravić22435292017-01-19 22:28:22 +0100361 http = build_http()
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400362 response, content = http.request(
363 uritemplate.expand(
Joe Gregoriobb964352013-03-03 20:45:29 -0500364 FLAGS.discovery_uri_template, {
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400365 'api': name,
366 'apiVersion': version})
367 )
Craig Citro6ae34d72014-08-18 23:10:09 -0700368 discovery = json.loads(content)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400369
370 version = safe_version(version)
371
372 document_collection_recursive(
373 service, '%s_%s.' % (name, version), discovery, discovery)
374
Joe Gregorioafc45f22011-02-20 16:11:28 -0500375
Joe Gregoriobb964352013-03-03 20:45:29 -0500376def document_api_from_discovery_document(uri):
377 """Document the given API.
378
379 Args:
380 uri: string, URI of discovery document.
381 """
Igor Maravić22435292017-01-19 22:28:22 +0100382 http = build_http()
Joe Gregoriobb964352013-03-03 20:45:29 -0500383 response, content = http.request(FLAGS.discovery_uri)
Craig Citro6ae34d72014-08-18 23:10:09 -0700384 discovery = json.loads(content)
Joe Gregoriobb964352013-03-03 20:45:29 -0500385
386 service = build_from_document(discovery)
387
388 name = discovery['version']
389 version = safe_version(discovery['version'])
390
391 document_collection_recursive(
392 service, '%s_%s.' % (name, version), discovery, discovery)
393
394
395if __name__ == '__main__':
Joe Gregorio79daca02013-03-29 16:25:52 -0400396 FLAGS = parser.parse_args(sys.argv[1:])
Joe Gregoriobb964352013-03-03 20:45:29 -0500397 if FLAGS.discovery_uri:
398 document_api_from_discovery_document(FLAGS.discovery_uri)
Joe Gregorio20a5aa92011-04-01 17:44:25 -0400399 else:
Bu Sun Kimc9773042019-07-17 14:03:17 -0700400 api_directory = collections.defaultdict(list)
Igor Maravić22435292017-01-19 22:28:22 +0100401 http = build_http()
Joe Gregoriobb964352013-03-03 20:45:29 -0500402 resp, content = http.request(
403 FLAGS.directory_uri,
404 headers={'X-User-IP': '0.0.0.0'})
405 if resp.status == 200:
Craig Citro6ae34d72014-08-18 23:10:09 -0700406 directory = json.loads(content)['items']
Joe Gregoriobb964352013-03-03 20:45:29 -0500407 for api in directory:
408 document_api(api['name'], api['version'])
Bu Sun Kimc9773042019-07-17 14:03:17 -0700409 api_directory[api['name']].append(api['version'])
410
411 # sort by api name and version number
412 for api in api_directory:
413 api_directory[api] = sorted(api_directory[api])
414 api_directory = OrderedDict(sorted(api_directory.items(), key = lambda x: x[0]))
415
416 markdown = []
417 for api, versions in api_directory.items():
418 markdown.append('## %s' % api)
419 for version in versions:
420 markdown.append('* [%s](http://googleapis.github.io/google-api-python-client/docs/dyn/%s_%s.html)' % (version, api, version))
421 markdown.append('\n')
422
423 with open('docs/dyn/index.md', 'w') as f:
424 f.write('\n'.join(markdown).encode('utf-8'))
425
Joe Gregoriobb964352013-03-03 20:45:29 -0500426 else:
427 sys.exit("Failed to load the discovery document.")