1# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# http://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
13import os
14
15from botocore import xform_name
16from botocore.compat import OrderedDict
17from botocore.docs.bcdoc.restdoc import DocumentStructure
18from botocore.docs.example import ResponseExampleDocumenter
19from botocore.docs.method import (
20 document_custom_method,
21 document_model_driven_method,
22 get_instance_public_methods,
23)
24from botocore.docs.params import ResponseParamsDocumenter
25from botocore.docs.sharedexample import document_shared_examples
26from botocore.docs.utils import DocumentedShape, get_official_service_name
27
28
29def _allowlist_generate_presigned_url(method_name, service_name, **kwargs):
30 if method_name != 'generate_presigned_url':
31 return None
32 return service_name in ['s3']
33
34
35class ClientDocumenter:
36 _CLIENT_METHODS_FILTERS = [
37 _allowlist_generate_presigned_url,
38 ]
39
40 def __init__(self, client, root_docs_path, shared_examples=None):
41 self._client = client
42 self._client_class_name = self._client.__class__.__name__
43 self._root_docs_path = root_docs_path
44 self._shared_examples = shared_examples
45 if self._shared_examples is None:
46 self._shared_examples = {}
47 self._service_name = self._client.meta.service_model.service_name
48
49 def document_client(self, section):
50 """Documents a client and its methods
51
52 :param section: The section to write to.
53 """
54 self._add_title(section)
55 self._add_class_signature(section)
56 client_methods = self._get_client_methods()
57 self._add_client_intro(section, client_methods)
58 self._add_client_methods(client_methods)
59
60 def _get_client_methods(self):
61 client_methods = get_instance_public_methods(self._client)
62 return self._filter_client_methods(client_methods)
63
64 def _filter_client_methods(self, client_methods):
65 filtered_methods = {}
66 for method_name, method in client_methods.items():
67 include = self._filter_client_method(
68 method=method,
69 method_name=method_name,
70 service_name=self._service_name,
71 )
72 if include:
73 filtered_methods[method_name] = method
74 return filtered_methods
75
76 def _filter_client_method(self, **kwargs):
77 # Apply each filter to the method
78 for filter in self._CLIENT_METHODS_FILTERS:
79 filter_include = filter(**kwargs)
80 # Use the first non-None value returned by any of the filters
81 if filter_include is not None:
82 return filter_include
83 # Otherwise default to including it
84 return True
85
86 def _add_title(self, section):
87 section.style.h2('Client')
88
89 def _add_client_intro(self, section, client_methods):
90 section = section.add_new_section('intro')
91 # Write out the top level description for the client.
92 official_service_name = get_official_service_name(
93 self._client.meta.service_model
94 )
95 section.write(
96 f"A low-level client representing {official_service_name}"
97 )
98 section.style.new_line()
99 section.include_doc_string(
100 self._client.meta.service_model.documentation
101 )
102
103 # Write out the client example instantiation.
104 self._add_client_creation_example(section)
105
106 # List out all of the possible client methods.
107 section.style.dedent()
108 section.style.new_paragraph()
109 section.writeln('These are the available methods:')
110 section.style.toctree()
111 for method_name in sorted(client_methods):
112 section.style.tocitem(f'{self._service_name}/client/{method_name}')
113
114 def _add_class_signature(self, section):
115 section.style.start_sphinx_py_class(
116 class_name=f'{self._client_class_name}.Client'
117 )
118
119 def _add_client_creation_example(self, section):
120 section.style.start_codeblock()
121 section.style.new_line()
122 section.write(
123 f'client = session.create_client(\'{self._service_name}\')'
124 )
125 section.style.end_codeblock()
126
127 def _add_client_methods(self, client_methods):
128 for method_name in sorted(client_methods):
129 # Create a new DocumentStructure for each client method and add contents.
130 method_doc_structure = DocumentStructure(
131 method_name, target='html'
132 )
133 self._add_client_method(
134 method_doc_structure, method_name, client_methods[method_name]
135 )
136 # Write client methods in individual/nested files.
137 # Path: <root>/reference/services/<service>/client/<method_name>.rst
138 client_dir_path = os.path.join(
139 self._root_docs_path, self._service_name, 'client'
140 )
141 method_doc_structure.write_to_file(client_dir_path, method_name)
142
143 def _add_client_method(self, section, method_name, method):
144 breadcrumb_section = section.add_new_section('breadcrumb')
145 breadcrumb_section.style.ref(
146 self._client_class_name, f'../../{self._service_name}'
147 )
148 breadcrumb_section.write(f' / Client / {method_name}')
149 section.add_title_section(method_name)
150 method_section = section.add_new_section(
151 method_name,
152 context={'qualifier': f'{self._client_class_name}.Client.'},
153 )
154 if self._is_custom_method(method_name):
155 self._add_custom_method(
156 method_section,
157 method_name,
158 method,
159 )
160 else:
161 self._add_model_driven_method(method_section, method_name)
162
163 def _is_custom_method(self, method_name):
164 return method_name not in self._client.meta.method_to_api_mapping
165
166 def _add_custom_method(self, section, method_name, method):
167 document_custom_method(section, method_name, method)
168
169 def _add_method_exceptions_list(self, section, operation_model):
170 error_section = section.add_new_section('exceptions')
171 error_section.style.new_line()
172 error_section.style.bold('Exceptions')
173 error_section.style.new_line()
174 for error in operation_model.error_shapes:
175 class_name = (
176 f'{self._client_class_name}.Client.exceptions.{error.name}'
177 )
178 error_section.style.li(f':py:class:`{class_name}`')
179
180 def _add_model_driven_method(self, section, method_name):
181 service_model = self._client.meta.service_model
182 operation_name = self._client.meta.method_to_api_mapping[method_name]
183 operation_model = service_model.operation_model(operation_name)
184
185 example_prefix = f'response = client.{method_name}'
186 full_method_name = (
187 f"{section.context.get('qualifier', '')}{method_name}"
188 )
189 document_model_driven_method(
190 section,
191 full_method_name,
192 operation_model,
193 event_emitter=self._client.meta.events,
194 method_description=operation_model.documentation,
195 example_prefix=example_prefix,
196 )
197
198 # Add any modeled exceptions
199 if operation_model.error_shapes:
200 self._add_method_exceptions_list(section, operation_model)
201
202 # Add the shared examples
203 shared_examples = self._shared_examples.get(operation_name)
204 if shared_examples:
205 document_shared_examples(
206 section, operation_model, example_prefix, shared_examples
207 )
208
209
210class ClientExceptionsDocumenter:
211 _USER_GUIDE_LINK = (
212 'https://boto3.amazonaws.com/'
213 'v1/documentation/api/latest/guide/error-handling.html'
214 )
215 _GENERIC_ERROR_SHAPE = DocumentedShape(
216 name='Error',
217 type_name='structure',
218 documentation=('Normalized access to common exception attributes.'),
219 members=OrderedDict(
220 [
221 (
222 'Code',
223 DocumentedShape(
224 name='Code',
225 type_name='string',
226 documentation=(
227 'An identifier specifying the exception type.'
228 ),
229 ),
230 ),
231 (
232 'Message',
233 DocumentedShape(
234 name='Message',
235 type_name='string',
236 documentation=(
237 'A descriptive message explaining why the exception '
238 'occured.'
239 ),
240 ),
241 ),
242 ]
243 ),
244 )
245
246 def __init__(self, client, root_docs_path):
247 self._client = client
248 self._client_class_name = self._client.__class__.__name__
249 self._service_name = self._client.meta.service_model.service_name
250 self._root_docs_path = root_docs_path
251
252 def document_exceptions(self, section):
253 self._add_title(section)
254 self._add_overview(section)
255 self._add_exceptions_list(section)
256 self._add_exception_classes()
257
258 def _add_title(self, section):
259 section.style.h2('Client Exceptions')
260
261 def _add_overview(self, section):
262 section.style.new_line()
263 section.write(
264 'Client exceptions are available on a client instance '
265 'via the ``exceptions`` property. For more detailed instructions '
266 'and examples on the exact usage of client exceptions, see the '
267 'error handling '
268 )
269 section.style.external_link(
270 title='user guide',
271 link=self._USER_GUIDE_LINK,
272 )
273 section.write('.')
274 section.style.new_line()
275
276 def _exception_class_name(self, shape):
277 return f'{self._client_class_name}.Client.exceptions.{shape.name}'
278
279 def _add_exceptions_list(self, section):
280 error_shapes = self._client.meta.service_model.error_shapes
281 if not error_shapes:
282 section.style.new_line()
283 section.write('This client has no modeled exception classes.')
284 section.style.new_line()
285 return
286 section.style.new_line()
287 section.writeln('The available client exceptions are:')
288 section.style.toctree()
289 for shape in error_shapes:
290 section.style.tocitem(
291 f'{self._service_name}/client/exceptions/{shape.name}'
292 )
293
294 def _add_exception_classes(self):
295 for shape in self._client.meta.service_model.error_shapes:
296 # Create a new DocumentStructure for each exception method and add contents.
297 exception_doc_structure = DocumentStructure(
298 shape.name, target='html'
299 )
300 self._add_exception_class(exception_doc_structure, shape)
301 # Write exceptions in individual/nested files.
302 # Path: <root>/reference/services/<service>/client/exceptions/<exception_name>.rst
303 exception_dir_path = os.path.join(
304 self._root_docs_path,
305 self._service_name,
306 'client',
307 'exceptions',
308 )
309 exception_doc_structure.write_to_file(
310 exception_dir_path, shape.name
311 )
312
313 def _add_exception_class(self, section, shape):
314 breadcrumb_section = section.add_new_section('breadcrumb')
315 breadcrumb_section.style.ref(
316 self._client_class_name, f'../../../{self._service_name}'
317 )
318 breadcrumb_section.write(f' / Client / exceptions / {shape.name}')
319 section.add_title_section(shape.name)
320 class_section = section.add_new_section(shape.name)
321 class_name = self._exception_class_name(shape)
322 class_section.style.start_sphinx_py_class(class_name=class_name)
323 self._add_top_level_documentation(class_section, shape)
324 self._add_exception_catch_example(class_section, shape)
325 self._add_response_attr(class_section, shape)
326 class_section.style.end_sphinx_py_class()
327
328 def _add_top_level_documentation(self, section, shape):
329 if shape.documentation:
330 section.style.new_line()
331 section.include_doc_string(shape.documentation)
332 section.style.new_line()
333
334 def _add_exception_catch_example(self, section, shape):
335 section.style.new_line()
336 section.style.bold('Example')
337 section.style.new_paragraph()
338 section.style.start_codeblock()
339 section.write('try:')
340 section.style.indent()
341 section.style.new_line()
342 section.write('...')
343 section.style.dedent()
344 section.style.new_line()
345 section.write(f'except client.exceptions.{shape.name} as e:')
346 section.style.indent()
347 section.style.new_line()
348 section.write('print(e.response)')
349 section.style.dedent()
350 section.style.end_codeblock()
351
352 def _add_response_attr(self, section, shape):
353 response_section = section.add_new_section('response')
354 response_section.style.start_sphinx_py_attr('response')
355 self._add_response_attr_description(response_section)
356 self._add_response_example(response_section, shape)
357 self._add_response_params(response_section, shape)
358 response_section.style.end_sphinx_py_attr()
359
360 def _add_response_attr_description(self, section):
361 section.style.new_line()
362 section.include_doc_string(
363 'The parsed error response. All exceptions have a top level '
364 '``Error`` key that provides normalized access to common '
365 'exception atrributes. All other keys are specific to this '
366 'service or exception class.'
367 )
368 section.style.new_line()
369
370 def _add_response_example(self, section, shape):
371 example_section = section.add_new_section('syntax')
372 example_section.style.new_line()
373 example_section.style.bold('Syntax')
374 example_section.style.new_paragraph()
375 documenter = ResponseExampleDocumenter(
376 service_name=self._service_name,
377 operation_name=None,
378 event_emitter=self._client.meta.events,
379 )
380 documenter.document_example(
381 example_section,
382 shape,
383 include=[self._GENERIC_ERROR_SHAPE],
384 )
385
386 def _add_response_params(self, section, shape):
387 params_section = section.add_new_section('Structure')
388 params_section.style.new_line()
389 params_section.style.bold('Structure')
390 params_section.style.new_paragraph()
391 documenter = ResponseParamsDocumenter(
392 service_name=self._service_name,
393 operation_name=None,
394 event_emitter=self._client.meta.events,
395 )
396 documenter.document_params(
397 params_section,
398 shape,
399 include=[self._GENERIC_ERROR_SHAPE],
400 )
401
402
403class ClientContextParamsDocumenter:
404 _CONFIG_GUIDE_LINK = (
405 'https://boto3.amazonaws.com/'
406 'v1/documentation/api/latest/guide/configuration.html'
407 )
408
409 OMITTED_CONTEXT_PARAMS = {
410 's3': (
411 'Accelerate',
412 'DisableMultiRegionAccessPoints',
413 'ForcePathStyle',
414 'UseArnRegion',
415 ),
416 's3control': ('UseArnRegion',),
417 }
418
419 def __init__(self, service_name, context_params):
420 self._service_name = service_name
421 self._context_params = context_params
422
423 def document_context_params(self, section):
424 self._add_title(section)
425 self._add_overview(section)
426 self._add_context_params_list(section)
427
428 def _add_title(self, section):
429 section.style.h2('Client Context Parameters')
430
431 def _add_overview(self, section):
432 section.style.new_line()
433 section.write(
434 'Client context parameters are configurable on a client '
435 'instance via the ``client_context_params`` parameter in the '
436 '``Config`` object. For more detailed instructions and examples '
437 'on the exact usage of context params see the '
438 )
439 section.style.external_link(
440 title='configuration guide',
441 link=self._CONFIG_GUIDE_LINK,
442 )
443 section.write('.')
444 section.style.new_line()
445
446 def _add_context_params_list(self, section):
447 section.style.new_line()
448 sn = f'``{self._service_name}``'
449 section.writeln(f'The available {sn} client context params are:')
450 for param in self._context_params:
451 section.style.new_line()
452 name = f'``{xform_name(param.name)}``'
453 section.write(f'* {name} ({param.type}) - {param.documentation}')