From 5df871847b6a4dd6d43722725b007bfd217ba8f1 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:16:33 -0600 Subject: [PATCH 01/10] Adding a test code examples section. --- botocore/docs/bcdoc/restdoc.py | 3 +- botocore/docs/codeexamples.py | 71 +++++++++++++++++ botocore/docs/service.py | 26 +++++++ botocore/loaders.py | 137 +++++++++++++++++++++++++++++++++ 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 botocore/docs/codeexamples.py diff --git a/botocore/docs/bcdoc/restdoc.py b/botocore/docs/bcdoc/restdoc.py index 3868126cc9..5311b8c0aa 100644 --- a/botocore/docs/bcdoc/restdoc.py +++ b/botocore/docs/bcdoc/restdoc.py @@ -44,6 +44,7 @@ 'client-api': 4, 'paginator-api': 3, 'waiter-api': 3, + 'examples-api': 3, } @@ -203,7 +204,7 @@ def add_new_section(self, name, context=None): name=name, target=self.target, context=context ) section.path = self.path + [name] - # Indent the section apporpriately as well + # Indent the section appropriately as well section.style.indentation = self.style.indentation section.translation_map = self.translation_map section.hrefs = self.hrefs diff --git a/botocore/docs/codeexamples.py b/botocore/docs/codeexamples.py new file mode 100644 index 0000000000..ac3a22d394 --- /dev/null +++ b/botocore/docs/codeexamples.py @@ -0,0 +1,71 @@ +# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +import os + +from botocore import xform_name +from botocore.compat import OrderedDict +from botocore.docs.bcdoc.restdoc import DocumentStructure +from botocore.docs.method import document_model_driven_method +from botocore.docs.utils import DocumentedShape +from botocore.utils import get_service_module_name + + +class CodeExamplesDocumenter: + def __init__(self, client, root_docs_path): + self._client = client + self._client_class_name = self._client.__class__.__name__ + self._service_name = self._client.meta.service_model.service_name + self._root_docs_path = root_docs_path + self._USER_GUIDE_LINK = ( + 'https://boto3.amazonaws.com/' + 'v1/documentation/api/latest/guide/clients.html#waiters' + ) + self._CODE_EXAMPLE_LINK = ( + 'https://docs.aws.amazon.com/code-library/latest/ug/python_3_' + ) + + def document_code_examples(self, section, examples): + """Documents the code library code examples for a service. + + :param section: The section to write to. + :param examples: The list of examples. + """ + section.style.h2('AWS Code Examples') + self._add_overview(section) + + # List the available Code Library examples with a link. + # TODO: fix service name + for example in examples: + section.style.new_paragraph() + title_text = examples[example]['title'] + plain_title = title_text.replace('', '').replace('', '') + section.style.external_link( + title=plain_title, + link=examples[example]['doc_filenames']['service_pages']['lookoutvision'], + # link=self._CODE_EXAMPLE_LINK + self._service_name + '_code_examples.html', + ) + section.style.new_line() + + def _add_overview(self, section): + section.style.new_line() + section.write( + 'Explore more examples for this service in the ' + ) + section.style.external_link( + title='AWS Examples Code Library', + link=self._CODE_EXAMPLE_LINK + self._service_name + '_code_examples.html', + ) + section.write('.') + section.style.new_line() + + diff --git a/botocore/docs/service.py b/botocore/docs/service.py index d20a889dc9..77b98e1e54 100644 --- a/botocore/docs/service.py +++ b/botocore/docs/service.py @@ -18,6 +18,7 @@ ) from botocore.docs.paginator import PaginatorDocumenter from botocore.docs.waiter import WaiterDocumenter +from botocore.docs.codeexamples import CodeExamplesDocumenter from botocore.exceptions import DataNotFoundError @@ -42,6 +43,7 @@ def __init__(self, service_name, session, root_docs_path): 'paginator-api', 'waiter-api', 'client-context-params', + 'examples-api', ] def document_service(self): @@ -61,6 +63,7 @@ def document_service(self): 'client-context-params' ) self.client_context_params(context_params_section) + self.examples_api(doc_structure.get_section('examples-api')) return doc_structure.flush_structure() def title(self, section): @@ -111,6 +114,29 @@ def waiter_api(self, section): ) waiter_documenter.document_waiters(section) + def examples_api(self, section): + library_examples_list = None + try: + library_examples_list = self.get_library_examples() + except DataNotFoundError: + pass + + if library_examples_list: + examples_documenter = CodeExamplesDocumenter( + self._client, self._root_docs_path + ) + examples_documenter.document_code_examples(section, library_examples_list) + + def get_library_examples(self): + loader = self._session.get_component('data_loader') + examples = loader.file_loader.load_examples_file('sample_file') + print(examples['examples']) + + for example in examples['examples']: + print(example) + print(examples['examples'][example]['id']) + return examples['examples'] + def get_examples(self, service_name, api_version=None): loader = self._session.get_component('data_loader') examples = loader.load_service_model( diff --git a/botocore/loaders.py b/botocore/loaders.py index f5072a3e5f..2b0578c9f6 100644 --- a/botocore/loaders.py +++ b/botocore/loaders.py @@ -197,6 +197,143 @@ def load_file(self, file_path): return data return None + def load_examples_file(self, file_path): + """Load the examples data if available + + :return: The loaded data if it exists, otherwise None. + + """ + examples_json = '''{ + "examples": { + "lookoutvision_CreateDataset": { + "id": "lookoutvision_CreateDataset", + "file": "lookoutvision_metadata.yaml", + "languages": { + "Python": { + "name": "Python", + "property": "", + "versions": [ + { + "sdk_version": 3, + "block_content": null, + "excerpts": [ + { + "description": null, + "snippet_tags": [ + "python.example_code.lookoutvision.Datasets", + "python.example_code.lookoutvision.CreateDataset" + ], + "snippet_files": [], + "genai": "none" + } + ], + "github": "python/example_code/lookoutvision", + "sdkguide": null, + "more_info": [] + } + ] + } + }, + "title": "Use CreateDataset", + "title_abbrev": "CreateDataset", + "synopsis": "use CreateDataset.", + "category": "Api", + "guide_topic": { + "title": "Creating your dataset", + "url": "lookout-for-vision/latest/developer-guide/model-create-dataset.html" + }, + "service_main": null, + "services": { + "lookoutvision": { + "__set__": [ + "CreateDataset" + ] + } + }, + "doc_filenames": { + "service_pages": { + "lookoutvision": "https://docs.aws.amazon.com/code-library/latest/ug/lookoutvision_example_lookoutvision_CreateDataset_section.html" + }, + "sdk_pages": { + "": { + "3": { + "actions_scenarios": { + "lookoutvision": "https://docs.aws.amazon.com/code-library/latest/ug/_3_lookoutvision_code_examples.html#actions" + }, + "cross_service": null + } + } + } + }, + "synopsis_list": [], + "source_key": null + }, + "lookoutvision_Scenario_CreateManifestFile": { + "id": "lookoutvision_Scenario_CreateManifestFile", + "file": "lookoutvision_metadata.yaml", + "languages": { + "Python": { + "name": "Python", + "property": "", + "versions": [ + { + "sdk_version": 3, + "block_content": null, + "excerpts": [ + { + "description": null, + "snippet_tags": [ + "python.example_code.lookoutvision.Datasets", + "python.example_code.lookoutvision.Scenario_CreateManifestFile" + ], + "snippet_files": [], + "genai": "none" + } + ], + "github": "python/example_code/lookoutvision", + "sdkguide": null, + "more_info": [] + } + ] + } + }, + "title": "Create a Lookout for Vision manifest file using an AWS SDK", + "title_abbrev": "Create a manifest file", + "synopsis": "create a Lookout for Vision manifest file and upload it to Amazon S3.", + "category": "Scenarios", + "guide_topic": { + "title": "Creating a manifest file", + "url": "lookout-for-vision/latest/developer-guide/manifest-files.html" + }, + "service_main": null, + "services": { + "lookoutvision": { + "__set__": [] + } + }, + "doc_filenames": { + "service_pages": { + "lookoutvision": "https://docs.aws.amazon.com/code-library/latest/ug/lookoutvision_example_lookoutvision_Scenario_CreateManifestFile_section.html" + }, + "sdk_pages": { + "": { + "3": { + "actions_scenarios": { + "lookoutvision": "https://docs.aws.amazon.com/code-library/latest/ug/_3_lookoutvision_code_examples.html#scenarios" + }, + "cross_service": null + } + } + } + }, + "synopsis_list": [], + "source_key": null + } + } + }''' + # return json.loads(examples_json, object_pairs_hook=OrderedDict) + return json.loads(examples_json) + def create_loader(search_path_string=None): """Create a Loader class. From 4dc960f220bf4b95f57a94538d8ee1e0edec7d00 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:32:48 -0600 Subject: [PATCH 02/10] Update code examples section --- botocore/docs/codeexamples.py | 8 ++++---- botocore/docs/service.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/botocore/docs/codeexamples.py b/botocore/docs/codeexamples.py index ac3a22d394..558eace5e3 100644 --- a/botocore/docs/codeexamples.py +++ b/botocore/docs/codeexamples.py @@ -40,13 +40,13 @@ def document_code_examples(self, section, examples): :param section: The section to write to. :param examples: The list of examples. """ - section.style.h2('AWS Code Examples') + section.style.h2('AWS SDK Code Examples') self._add_overview(section) # List the available Code Library examples with a link. # TODO: fix service name for example in examples: - section.style.new_paragraph() + section.style.start_li() title_text = examples[example]['title'] plain_title = title_text.replace('', '').replace('', '') section.style.external_link( @@ -54,7 +54,7 @@ def document_code_examples(self, section, examples): link=examples[example]['doc_filenames']['service_pages']['lookoutvision'], # link=self._CODE_EXAMPLE_LINK + self._service_name + '_code_examples.html', ) - section.style.new_line() + section.style.end_li() def _add_overview(self, section): section.style.new_line() @@ -62,7 +62,7 @@ def _add_overview(self, section): 'Explore more examples for this service in the ' ) section.style.external_link( - title='AWS Examples Code Library', + title='AWS SDK Code Examples Code Library', link=self._CODE_EXAMPLE_LINK + self._service_name + '_code_examples.html', ) section.write('.') diff --git a/botocore/docs/service.py b/botocore/docs/service.py index 77b98e1e54..ce0a9f6ffe 100644 --- a/botocore/docs/service.py +++ b/botocore/docs/service.py @@ -130,11 +130,11 @@ def examples_api(self, section): def get_library_examples(self): loader = self._session.get_component('data_loader') examples = loader.file_loader.load_examples_file('sample_file') - print(examples['examples']) + # print(examples['examples']) - for example in examples['examples']: - print(example) - print(examples['examples'][example]['id']) + # for example in examples['examples']: + # print(example) + # print(examples['examples'][example]['id']) return examples['examples'] def get_examples(self, service_name, api_version=None): From 51f61c9a920cbe032453a65d95ed9adc4b520c39 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:54:42 -0500 Subject: [PATCH 03/10] Add categories and service map. --- botocore/docs/codeexamples.py | 36 +++++--- botocore/docs/service.py | 25 ++++-- botocore/loaders.py | 151 +++++----------------------------- botocore/utils.py | 4 + 4 files changed, 64 insertions(+), 152 deletions(-) diff --git a/botocore/docs/codeexamples.py b/botocore/docs/codeexamples.py index 558eace5e3..7494a4044d 100644 --- a/botocore/docs/codeexamples.py +++ b/botocore/docs/codeexamples.py @@ -34,32 +34,42 @@ def __init__(self, client, root_docs_path): 'https://docs.aws.amazon.com/code-library/latest/ug/python_3_' ) - def document_code_examples(self, section, examples): + def document_code_examples(self, section, examples, service_id): """Documents the code library code examples for a service. :param section: The section to write to. :param examples: The list of examples. + :param service_id: The code examples service id. """ section.style.h2('AWS SDK Code Examples') self._add_overview(section) # List the available Code Library examples with a link. - # TODO: fix service name - for example in examples: - section.style.start_li() - title_text = examples[example]['title'] - plain_title = title_text.replace('', '').replace('', '') - section.style.external_link( - title=plain_title, - link=examples[example]['doc_filenames']['service_pages']['lookoutvision'], - # link=self._CODE_EXAMPLE_LINK + self._service_name + '_code_examples.html', - ) - section.style.end_li() + + # Group the examples by category. Do not show a category if there are no examples. + example_categories = {} + for i in range(len(examples)): + example_categories.setdefault(examples[i]['category'], []).append(examples[i]) + + # for example in examples: + for category in example_categories: + section.style.new_line() + section.style.h3(category) + for i in range(len(example_categories[category])): + section.style.start_li() + title_text = example_categories[category][i]['title'] + if not title_text: + title_text = example_categories[category][i]['id'].rsplit('_', 1)[-1] + section.style.external_link( + title=title_text, + link=example_categories[category][i]['doc_filenames']['service_pages'][service_id], + ) + section.style.end_li() def _add_overview(self, section): section.style.new_line() section.write( - 'Explore more examples for this service in the ' + 'Explore code examples in the ' ) section.style.external_link( title='AWS SDK Code Examples Code Library', diff --git a/botocore/docs/service.py b/botocore/docs/service.py index ce0a9f6ffe..61afe68581 100644 --- a/botocore/docs/service.py +++ b/botocore/docs/service.py @@ -20,6 +20,9 @@ from botocore.docs.waiter import WaiterDocumenter from botocore.docs.codeexamples import CodeExamplesDocumenter from botocore.exceptions import DataNotFoundError +from botocore.utils import ( + SDK_CODE_EXAMPLE_ALIASES +) class ServiceDocumenter: @@ -117,7 +120,8 @@ def waiter_api(self, section): def examples_api(self, section): library_examples_list = None try: - library_examples_list = self.get_library_examples() + code_example_service_name = SDK_CODE_EXAMPLE_ALIASES.get(self._service_name, self._service_name) + library_examples_list = self.get_library_examples(code_example_service_name) except DataNotFoundError: pass @@ -125,17 +129,20 @@ def examples_api(self, section): examples_documenter = CodeExamplesDocumenter( self._client, self._root_docs_path ) - examples_documenter.document_code_examples(section, library_examples_list) + examples_documenter.document_code_examples(section, library_examples_list, code_example_service_name) - def get_library_examples(self): + def get_library_examples(self, service_name): loader = self._session.get_component('data_loader') - examples = loader.file_loader.load_examples_file('sample_file') - # print(examples['examples']) + #TODO: move these constants to config somewhere + git_url = 'https://raw.githubusercontent.com/rlhagerm/aws-doc-sdk-examples/fe9f3a68c94ba888f96e23e799aa8b43cb3f457d/python/example_code/' + git_service_url = f"{git_url}/{service_name}/examples_catalog.json" - # for example in examples['examples']: - # print(example) - # print(examples['examples'][example]['id']) - return examples['examples'] + examples = loader.file_loader.load_examples_url(git_service_url) + + if len(examples) > 0: + return examples['examples'] + + return [] def get_examples(self, service_name, api_version=None): loader = self._session.get_component('data_loader') diff --git a/botocore/loaders.py b/botocore/loaders.py index 2b0578c9f6..36167f5391 100644 --- a/botocore/loaders.py +++ b/botocore/loaders.py @@ -104,6 +104,7 @@ import logging import os +import requests from botocore import BOTOCORE_ROOT from botocore.compat import HAS_GZIP, OrderedDict, json @@ -203,136 +204,26 @@ def load_examples_file(self, file_path): :return: The loaded data if it exists, otherwise None. """ - examples_json = '''{ - "examples": { - "lookoutvision_CreateDataset": { - "id": "lookoutvision_CreateDataset", - "file": "lookoutvision_metadata.yaml", - "languages": { - "Python": { - "name": "Python", - "property": "", - "versions": [ - { - "sdk_version": 3, - "block_content": null, - "excerpts": [ - { - "description": null, - "snippet_tags": [ - "python.example_code.lookoutvision.Datasets", - "python.example_code.lookoutvision.CreateDataset" - ], - "snippet_files": [], - "genai": "none" - } - ], - "github": "python/example_code/lookoutvision", - "sdkguide": null, - "more_info": [] - } - ] - } - }, - "title": "Use CreateDataset", - "title_abbrev": "CreateDataset", - "synopsis": "use CreateDataset.", - "category": "Api", - "guide_topic": { - "title": "Creating your dataset", - "url": "lookout-for-vision/latest/developer-guide/model-create-dataset.html" - }, - "service_main": null, - "services": { - "lookoutvision": { - "__set__": [ - "CreateDataset" - ] - } - }, - "doc_filenames": { - "service_pages": { - "lookoutvision": "https://docs.aws.amazon.com/code-library/latest/ug/lookoutvision_example_lookoutvision_CreateDataset_section.html" - }, - "sdk_pages": { - "": { - "3": { - "actions_scenarios": { - "lookoutvision": "https://docs.aws.amazon.com/code-library/latest/ug/_3_lookoutvision_code_examples.html#actions" - }, - "cross_service": null - } - } - } - }, - "synopsis_list": [], - "source_key": null - }, - "lookoutvision_Scenario_CreateManifestFile": { - "id": "lookoutvision_Scenario_CreateManifestFile", - "file": "lookoutvision_metadata.yaml", - "languages": { - "Python": { - "name": "Python", - "property": "", - "versions": [ - { - "sdk_version": 3, - "block_content": null, - "excerpts": [ - { - "description": null, - "snippet_tags": [ - "python.example_code.lookoutvision.Datasets", - "python.example_code.lookoutvision.Scenario_CreateManifestFile" - ], - "snippet_files": [], - "genai": "none" - } - ], - "github": "python/example_code/lookoutvision", - "sdkguide": null, - "more_info": [] - } - ] - } - }, - "title": "Create a Lookout for Vision manifest file using an AWS SDK", - "title_abbrev": "Create a manifest file", - "synopsis": "create a Lookout for Vision manifest file and upload it to Amazon S3.", - "category": "Scenarios", - "guide_topic": { - "title": "Creating a manifest file", - "url": "lookout-for-vision/latest/developer-guide/manifest-files.html" - }, - "service_main": null, - "services": { - "lookoutvision": { - "__set__": [] - } - }, - "doc_filenames": { - "service_pages": { - "lookoutvision": "https://docs.aws.amazon.com/code-library/latest/ug/lookoutvision_example_lookoutvision_Scenario_CreateManifestFile_section.html" - }, - "sdk_pages": { - "": { - "3": { - "actions_scenarios": { - "lookoutvision": "https://docs.aws.amazon.com/code-library/latest/ug/_3_lookoutvision_code_examples.html#scenarios" - }, - "cross_service": null - } - } - } - }, - "synopsis_list": [], - "source_key": null - } - } - }''' - # return json.loads(examples_json, object_pairs_hook=OrderedDict) - return json.loads(examples_json) + + examples_json = self.load_file(file_path) + if examples_json: + return examples_json + return [] + + def load_examples_url(self, file_url): + """Load the examples data from a url if available + + :return: The loaded data if it exists, otherwise None. + + """ + response = requests.get(file_url) + if response.status_code == 200: + example_json = response.text + if example_json: + examples = json.loads(example_json) + return examples + + return [] def create_loader(search_path_string=None): diff --git a/botocore/utils.py b/botocore/utils.py index 30a513ea90..e2fd92cea7 100644 --- a/botocore/utils.py +++ b/botocore/utils.py @@ -178,6 +178,10 @@ "tagging": "resource-groups-tagging-api", } +# The SDK code examples library service names mapped to the service id, if they do not match. +SDK_CODE_EXAMPLE_ALIASES = { + "apigateway": "api-gateway", +} # This pattern can be used to detect if a header is a flexible checksum header CHECKSUM_HEADER_PATTERN = re.compile( From 469bc653aed22050dda41a979a030e1b8aa58a99 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:11:34 -0500 Subject: [PATCH 04/10] Updates to service page logic. --- botocore/docs/codeexamples.py | 4 +++- botocore/loaders.py | 1 + botocore/utils.py | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/botocore/docs/codeexamples.py b/botocore/docs/codeexamples.py index 7494a4044d..41f1958d8c 100644 --- a/botocore/docs/codeexamples.py +++ b/botocore/docs/codeexamples.py @@ -60,9 +60,11 @@ def document_code_examples(self, section, examples, service_id): title_text = example_categories[category][i]['title'] if not title_text: title_text = example_categories[category][i]['id'].rsplit('_', 1)[-1] + service_page = example_categories[category][i]['doc_filenames']['service_pages'].get( + service_id,list(example_categories[category][i]['doc_filenames']['service_pages'].values())[0]) section.style.external_link( title=title_text, - link=example_categories[category][i]['doc_filenames']['service_pages'][service_id], + link=service_page, ) section.style.end_li() diff --git a/botocore/loaders.py b/botocore/loaders.py index 36167f5391..fedcbb4d97 100644 --- a/botocore/loaders.py +++ b/botocore/loaders.py @@ -345,6 +345,7 @@ def list_available_services(self, type_name): if self.file_loader.exists(full_load_path): services.add(service_name) break + print(sorted(services)) return sorted(services) @instance_cache diff --git a/botocore/utils.py b/botocore/utils.py index e2fd92cea7..2d69c24d11 100644 --- a/botocore/utils.py +++ b/botocore/utils.py @@ -181,6 +181,9 @@ # The SDK code examples library service names mapped to the service id, if they do not match. SDK_CODE_EXAMPLE_ALIASES = { "apigateway": "api-gateway", + "autoscaling": "auto-scaling", + "cognito-identity": "cognito", + "kinesis-analytics-v2": "kinesisanalyticsv2", } # This pattern can be used to detect if a header is a flexible checksum header From b970473520a516c4afe20e528a0b746e1b064877 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:40:37 -0500 Subject: [PATCH 05/10] Updates to naming and tests. --- botocore/docs/bcdoc/restdoc.py | 2 +- botocore/docs/codeexamples.py | 48 +++++++----- botocore/docs/service.py | 32 +++----- botocore/loaders.py | 1 - tests/unit/docs/test_sdkcodeexamples.py | 99 +++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 44 deletions(-) create mode 100644 tests/unit/docs/test_sdkcodeexamples.py diff --git a/botocore/docs/bcdoc/restdoc.py b/botocore/docs/bcdoc/restdoc.py index 5311b8c0aa..c24f1d698a 100644 --- a/botocore/docs/bcdoc/restdoc.py +++ b/botocore/docs/bcdoc/restdoc.py @@ -44,7 +44,7 @@ 'client-api': 4, 'paginator-api': 3, 'waiter-api': 3, - 'examples-api': 3, + 'sdk-examples-api': 3, } diff --git a/botocore/docs/codeexamples.py b/botocore/docs/codeexamples.py index 41f1958d8c..ea4bdf7666 100644 --- a/botocore/docs/codeexamples.py +++ b/botocore/docs/codeexamples.py @@ -10,14 +10,11 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -import os -from botocore import xform_name -from botocore.compat import OrderedDict -from botocore.docs.bcdoc.restdoc import DocumentStructure -from botocore.docs.method import document_model_driven_method -from botocore.docs.utils import DocumentedShape -from botocore.utils import get_service_module_name + +CODE_EXAMPLE_LINK = 'https://docs.aws.amazon.com/code-library/latest/ug/python_3_' +# Change this to the main repo and branch when PR is merged. +CODE_EXAMPLE_CATALOG_BASE = 'https://raw.githubusercontent.com/rlhagerm/aws-doc-sdk-examples/fe9f3a68c94ba888f96e23e799aa8b43cb3f457d' class CodeExamplesDocumenter: @@ -26,13 +23,21 @@ def __init__(self, client, root_docs_path): self._client_class_name = self._client.__class__.__name__ self._service_name = self._client.meta.service_model.service_name self._root_docs_path = root_docs_path - self._USER_GUIDE_LINK = ( - 'https://boto3.amazonaws.com/' - 'v1/documentation/api/latest/guide/clients.html#waiters' - ) - self._CODE_EXAMPLE_LINK = ( - 'https://docs.aws.amazon.com/code-library/latest/ug/python_3_' - ) + + def load_code_examples_catalog(self, loader, service_id): + """Loads the code library example catalog listing for the service. + + :param loader: The loader for the examples catalog. + :param service_id: The code examples service id. + + :return: The list of examples. + """ + git_service_url = f"{CODE_EXAMPLE_CATALOG_BASE}/python/example_code/{service_id}/examples_catalog.json" + examples = loader.file_loader.load_examples_url(git_service_url) + if len(examples) > 0: + return examples['examples'] + + return [] def document_code_examples(self, section, examples, service_id): """Documents the code library code examples for a service. @@ -41,17 +46,16 @@ def document_code_examples(self, section, examples, service_id): :param examples: The list of examples. :param service_id: The code examples service id. """ - section.style.h2('AWS SDK Code Examples') - self._add_overview(section) - - # List the available Code Library examples with a link. + if examples: + section.style.h2('AWS SDK Code Examples') + self._add_overview(section) # Group the examples by category. Do not show a category if there are no examples. example_categories = {} for i in range(len(examples)): example_categories.setdefault(examples[i]['category'], []).append(examples[i]) - # for example in examples: + # Write a link item for each example in the category. for category in example_categories: section.style.new_line() section.style.h3(category) @@ -69,13 +73,17 @@ def document_code_examples(self, section, examples, service_id): section.style.end_li() def _add_overview(self, section): + """Write the overview section for code examples. + + :param section: The section to write to. + """ section.style.new_line() section.write( 'Explore code examples in the ' ) section.style.external_link( title='AWS SDK Code Examples Code Library', - link=self._CODE_EXAMPLE_LINK + self._service_name + '_code_examples.html', + link=CODE_EXAMPLE_LINK + self._service_name + '_code_examples.html', ) section.write('.') section.style.new_line() diff --git a/botocore/docs/service.py b/botocore/docs/service.py index 61afe68581..856659b5b9 100644 --- a/botocore/docs/service.py +++ b/botocore/docs/service.py @@ -46,7 +46,7 @@ def __init__(self, service_name, session, root_docs_path): 'paginator-api', 'waiter-api', 'client-context-params', - 'examples-api', + 'sdk-examples-api', ] def document_service(self): @@ -66,7 +66,7 @@ def document_service(self): 'client-context-params' ) self.client_context_params(context_params_section) - self.examples_api(doc_structure.get_section('examples-api')) + self.sdk_examples_api(doc_structure.get_section('sdk-examples-api')) return doc_structure.flush_structure() def title(self, section): @@ -117,32 +117,20 @@ def waiter_api(self, section): ) waiter_documenter.document_waiters(section) - def examples_api(self, section): + def sdk_examples_api(self, section): library_examples_list = None try: code_example_service_name = SDK_CODE_EXAMPLE_ALIASES.get(self._service_name, self._service_name) - library_examples_list = self.get_library_examples(code_example_service_name) - except DataNotFoundError: - pass - - if library_examples_list: examples_documenter = CodeExamplesDocumenter( self._client, self._root_docs_path ) - examples_documenter.document_code_examples(section, library_examples_list, code_example_service_name) - - def get_library_examples(self, service_name): - loader = self._session.get_component('data_loader') - #TODO: move these constants to config somewhere - git_url = 'https://raw.githubusercontent.com/rlhagerm/aws-doc-sdk-examples/fe9f3a68c94ba888f96e23e799aa8b43cb3f457d/python/example_code/' - git_service_url = f"{git_url}/{service_name}/examples_catalog.json" - - examples = loader.file_loader.load_examples_url(git_service_url) - - if len(examples) > 0: - return examples['examples'] - - return [] + loader = self._session.get_component('data_loader') + library_examples_list = examples_documenter.load_code_examples_catalog(loader, code_example_service_name) + # Only document the examples if there are any in the list. + if library_examples_list: + examples_documenter.document_code_examples(section, library_examples_list, code_example_service_name) + except DataNotFoundError: + pass def get_examples(self, service_name, api_version=None): loader = self._session.get_component('data_loader') diff --git a/botocore/loaders.py b/botocore/loaders.py index fedcbb4d97..36167f5391 100644 --- a/botocore/loaders.py +++ b/botocore/loaders.py @@ -345,7 +345,6 @@ def list_available_services(self, type_name): if self.file_loader.exists(full_load_path): services.add(service_name) break - print(sorted(services)) return sorted(services) @instance_cache diff --git a/tests/unit/docs/test_sdkcodeexamples.py b/tests/unit/docs/test_sdkcodeexamples.py new file mode 100644 index 0000000000..4ed83a1cd7 --- /dev/null +++ b/tests/unit/docs/test_sdkcodeexamples.py @@ -0,0 +1,99 @@ +# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +from botocore.docs.codeexamples import CodeExamplesDocumenter +from botocore.compat import json +from tests.unit.docs import BaseDocsTest + + +class TestCodeExamplesDocumenter(BaseDocsTest): + def setUp(self): + super().setUp() + self.add_shape_to_params('Biz', 'String') + self.extra_setup() + + def extra_setup(self): + self.setup_client() + self.examples_documenter = CodeExamplesDocumenter( + client=self.client, + root_docs_path=self.root_services_path, + ) + + def test_document_codeexamples(self): + examples_json = ''' + { + "examples": [ + { + "id": "service1_actions1", + "file": "service1_metadata.yaml", + "languages": [], + "title": "Action1", + "category": "Api", + "doc_filenames": { + "service_pages": { + "service1": "https://docs.aws.amazon.com/code-library/latest/ug/service1_example_service1_Action1_section.html" + }, + "sdk_pages": [] + }, + "synopsis_list": [], + "source_key": null + }, + { + "id": "service1_basics", + "file": "service1_metadata.yaml", + "title": "Learn the Basics", + "category": "Basics", + "doc_filenames": { + "service_pages": { + "service1": "https://docs.aws.amazon.com/code-library/latest/ug/service_example_service1_Basics_section.html" + }, + "sdk_pages": [] + }, + "synopsis_list": [], + "source_key": null + }, + { + "id": "service1_Scenario1", + "file": "service1_metadata.yaml", + "title": "Scenario1", + "category": "Scenarios", + "doc_filenames": { + "service_pages": { + "service1": "https://docs.aws.amazon.com/code-library/latest/ug/service_example_service1_Scenario1_section.html" + }, + "sdk_pages": [] + }, + "synopsis_list": [], + "source_key": null + } + ] + } + ''' + examples = json.loads(examples_json) + self.examples_documenter.document_code_examples(self.doc_structure, examples['examples'], 'service1') + self.assert_contains_line('AWS SDK Code Examples') + self.assert_contains_line( + 'Explore code examples in the `AWS SDK Code Examples Code Library `_') + self.assert_contains_line('Api') + self.assert_contains_line('* `Action1 `_') + self.assert_contains_line('Basics') + self.assert_contains_line( + '* `Learn the Basics `_') + self.assert_contains_line('Scenarios') + self.assert_contains_line( + '* `Scenario1 `_') + + def test_no_empty_examples_section(self): + examples = [] + + self.examples_documenter.document_code_examples(self.doc_structure, examples, 'service1') + self.assert_not_contains_line('AWS SDK Code Examples') From 767f8689e977e44a46a035134bd233ef6eee7437 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Thu, 27 Mar 2025 09:48:08 -0500 Subject: [PATCH 06/10] Update tests and json load. --- botocore/docs/codeexamples.py | 17 ++++++++----- botocore/docs/service.py | 3 +-- botocore/loaders.py | 28 -------------------- tests/unit/docs/test_sdkcodeexamples.py | 34 +++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 36 deletions(-) diff --git a/botocore/docs/codeexamples.py b/botocore/docs/codeexamples.py index ea4bdf7666..90eaa6a4d7 100644 --- a/botocore/docs/codeexamples.py +++ b/botocore/docs/codeexamples.py @@ -11,6 +11,8 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import requests +from botocore.compat import json CODE_EXAMPLE_LINK = 'https://docs.aws.amazon.com/code-library/latest/ug/python_3_' # Change this to the main repo and branch when PR is merged. @@ -24,18 +26,21 @@ def __init__(self, client, root_docs_path): self._service_name = self._client.meta.service_model.service_name self._root_docs_path = root_docs_path - def load_code_examples_catalog(self, loader, service_id): + def load_code_examples_catalog(self, service_id): """Loads the code library example catalog listing for the service. - :param loader: The loader for the examples catalog. :param service_id: The code examples service id. - :return: The list of examples. """ git_service_url = f"{CODE_EXAMPLE_CATALOG_BASE}/python/example_code/{service_id}/examples_catalog.json" - examples = loader.file_loader.load_examples_url(git_service_url) - if len(examples) > 0: - return examples['examples'] + + response = requests.get(git_service_url) + if response.status_code == 200: + example_json = response.text + if example_json: + examples = json.loads(example_json) + if len(examples) > 0: + return examples['examples'] return [] diff --git a/botocore/docs/service.py b/botocore/docs/service.py index 856659b5b9..6764c81226 100644 --- a/botocore/docs/service.py +++ b/botocore/docs/service.py @@ -124,8 +124,7 @@ def sdk_examples_api(self, section): examples_documenter = CodeExamplesDocumenter( self._client, self._root_docs_path ) - loader = self._session.get_component('data_loader') - library_examples_list = examples_documenter.load_code_examples_catalog(loader, code_example_service_name) + library_examples_list = examples_documenter.load_code_examples_catalog(code_example_service_name) # Only document the examples if there are any in the list. if library_examples_list: examples_documenter.document_code_examples(section, library_examples_list, code_example_service_name) diff --git a/botocore/loaders.py b/botocore/loaders.py index 36167f5391..f5072a3e5f 100644 --- a/botocore/loaders.py +++ b/botocore/loaders.py @@ -104,7 +104,6 @@ import logging import os -import requests from botocore import BOTOCORE_ROOT from botocore.compat import HAS_GZIP, OrderedDict, json @@ -198,33 +197,6 @@ def load_file(self, file_path): return data return None - def load_examples_file(self, file_path): - """Load the examples data if available - - :return: The loaded data if it exists, otherwise None. - - """ - - examples_json = self.load_file(file_path) - if examples_json: - return examples_json - return [] - - def load_examples_url(self, file_url): - """Load the examples data from a url if available - - :return: The loaded data if it exists, otherwise None. - - """ - response = requests.get(file_url) - if response.status_code == 200: - example_json = response.text - if example_json: - examples = json.loads(example_json) - return examples - - return [] - def create_loader(search_path_string=None): """Create a Loader class. diff --git a/tests/unit/docs/test_sdkcodeexamples.py b/tests/unit/docs/test_sdkcodeexamples.py index 4ed83a1cd7..45ad2e7bed 100644 --- a/tests/unit/docs/test_sdkcodeexamples.py +++ b/tests/unit/docs/test_sdkcodeexamples.py @@ -13,6 +13,7 @@ from botocore.docs.codeexamples import CodeExamplesDocumenter from botocore.compat import json from tests.unit.docs import BaseDocsTest +from tests import mock class TestCodeExamplesDocumenter(BaseDocsTest): @@ -28,6 +29,39 @@ def extra_setup(self): root_docs_path=self.root_services_path, ) + def mock_response( + self, + status=200, + content="CONTENT", + json_data=None): + + mock_resp = mock.Mock() + # Aet status code and content. + mock_resp.status_code = status + mock_resp.content = content + # Add json data if provided. + if json_data: + mock_resp.text = json_data + return mock_resp + + def test_load_catalog(self): + mock_json = ''' + { + "examples": [ + { + "id": "service1_actions1" + } + ] + } + ''' + examples_result = json.loads(mock_json) + mock_resp = self.mock_response(status=200, json_data=mock_json) + self.request_patch = mock.patch('requests.get', return_value=mock_resp) + self.request_patch.start() + examples = self.examples_documenter.load_code_examples_catalog('service1') + self.request_patch.stop() + self.assertEqual(examples, examples_result['examples']) + def test_document_codeexamples(self): examples_json = ''' { From 75b93669c3856ada51521311fbdbb82142eb07a6 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Mon, 31 Mar 2025 12:05:21 -0500 Subject: [PATCH 07/10] Update to category listing. --- botocore/docs/codeexamples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/botocore/docs/codeexamples.py b/botocore/docs/codeexamples.py index 90eaa6a4d7..ae7ab73c25 100644 --- a/botocore/docs/codeexamples.py +++ b/botocore/docs/codeexamples.py @@ -61,7 +61,7 @@ def document_code_examples(self, section, examples, service_id): example_categories.setdefault(examples[i]['category'], []).append(examples[i]) # Write a link item for each example in the category. - for category in example_categories: + for category in sorted(example_categories): section.style.new_line() section.style.h3(category) for i in range(len(example_categories[category])): From 5a993a9825673651daf28c0c4e589df5522daf80 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:46:27 -0500 Subject: [PATCH 08/10] Update to cookie domain as required by marketting. --- docs/source/_static/js/custom.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/_static/js/custom.js b/docs/source/_static/js/custom.js index af879c8697..c432e16dab 100644 --- a/docs/source/_static/js/custom.js +++ b/docs/source/_static/js/custom.js @@ -72,7 +72,7 @@ function isValidFragment(splitFragment) { } } })(); -// Given a service name, we apply the html classes which indicate a current page to the corresponsing list item. +// Given a service name, we apply the html classes which indicate a current page to the corresponding list item. // Before:
  • ACM
  • // After:
  • ACM
  • function makeServiceLinkCurrent(serviceName) { @@ -199,8 +199,8 @@ function setupKeyboardFriendlyNavigation() { function loadShortbread() { if (typeof AWSCShortbread !== "undefined") { const shortbread = AWSCShortbread({ - // If you're testing in your dev environment, use ".cloudfront.net" for domain, else ".amazonaws.com" - domain: ".amazonaws.com", + // If you're testing in your dev environment, use ".cloudfront.net" for domain, else "botocore.amazonaws.com" + domain: "botocore.amazonaws.com", }); // Check for cookie consent From 9abcea4f685f41d3ec95f55410eafa6f5305c3d7 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:49:59 -0500 Subject: [PATCH 09/10] Revert "Update to cookie domain as required by marketting." This reverts commit 5a993a9825673651daf28c0c4e589df5522daf80. --- docs/source/_static/js/custom.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/_static/js/custom.js b/docs/source/_static/js/custom.js index c432e16dab..af879c8697 100644 --- a/docs/source/_static/js/custom.js +++ b/docs/source/_static/js/custom.js @@ -72,7 +72,7 @@ function isValidFragment(splitFragment) { } } })(); -// Given a service name, we apply the html classes which indicate a current page to the corresponding list item. +// Given a service name, we apply the html classes which indicate a current page to the corresponsing list item. // Before:
  • ACM
  • // After:
  • ACM
  • function makeServiceLinkCurrent(serviceName) { @@ -199,8 +199,8 @@ function setupKeyboardFriendlyNavigation() { function loadShortbread() { if (typeof AWSCShortbread !== "undefined") { const shortbread = AWSCShortbread({ - // If you're testing in your dev environment, use ".cloudfront.net" for domain, else "botocore.amazonaws.com" - domain: "botocore.amazonaws.com", + // If you're testing in your dev environment, use ".cloudfront.net" for domain, else ".amazonaws.com" + domain: ".amazonaws.com", }); // Check for cookie consent From 9356289c8282bb48af98c07e73900791a01ec597 Mon Sep 17 00:00:00 2001 From: Rachel Hagerman <110480692+rlhagerm@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:45:14 -0500 Subject: [PATCH 10/10] Update catalog version. --- botocore/docs/codeexamples.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/botocore/docs/codeexamples.py b/botocore/docs/codeexamples.py index ae7ab73c25..2ceb40f5ce 100644 --- a/botocore/docs/codeexamples.py +++ b/botocore/docs/codeexamples.py @@ -15,8 +15,9 @@ from botocore.compat import json CODE_EXAMPLE_LINK = 'https://docs.aws.amazon.com/code-library/latest/ug/python_3_' +CODE_EXAMPLE_CATALOG_VERSION = '2025.0.0-alpha' # Change this to the main repo and branch when PR is merged. -CODE_EXAMPLE_CATALOG_BASE = 'https://raw.githubusercontent.com/rlhagerm/aws-doc-sdk-examples/fe9f3a68c94ba888f96e23e799aa8b43cb3f457d' +CODE_EXAMPLE_CATALOG_BASE = 'https://raw.githubusercontent.com/rlhagerm/aws-doc-sdk-examples/refs/tags/' class CodeExamplesDocumenter: @@ -32,7 +33,7 @@ def load_code_examples_catalog(self, service_id): :param service_id: The code examples service id. :return: The list of examples. """ - git_service_url = f"{CODE_EXAMPLE_CATALOG_BASE}/python/example_code/{service_id}/examples_catalog.json" + git_service_url = f"{CODE_EXAMPLE_CATALOG_BASE}/{CODE_EXAMPLE_CATALOG_VERSION}/python/example_code/{service_id}/examples_catalog.json" response = requests.get(git_service_url) if response.status_code == 200: