Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v2] Docs rewrite rules #9325

Merged
merged 27 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
13472fd
Create starter crosslinker Sphinx extension.
aemous Feb 25, 2025
fa76eeb
Progress on crosslinking extension.
aemous Feb 26, 2025
b500e9d
Resolve invalid escape sequence.
aemous Feb 26, 2025
cf416aa
Delete doc/compfiles.py
aemous Feb 26, 2025
a071703
Remove todo comment
aemous Feb 27, 2025
d124ca8
Update base path to be relative
aemous Mar 7, 2025
317c263
Update to latest models
aws-sdk-python-automation Feb 25, 2025
bfcb377
Update endpoints model
aws-sdk-python-automation Feb 25, 2025
d50bf68
Bump version to 2.24.12
aws-sdk-python-automation Feb 25, 2025
a267d7c
[v2] CloudFormation deploy docs clarification (#9321)
aemous Feb 26, 2025
394a97f
Update SSO configuration and related tests
AndrewAsseily Jan 13, 2025
591e383
Add changelog entry for SSO configuration updates
AndrewAsseily Jan 13, 2025
88c4214
Fix missing fallback to json output when profile_name is None
AndrewAsseily Jan 15, 2025
5f2a691
Set default output to 'json' in both final config and user prompts
AndrewAsseily Jan 15, 2025
fa101ea
cleanup comment
AndrewAsseily Jan 15, 2025
b4cf0c3
specify json is the default output, but None will be saved if nothing…
AndrewAsseily Jan 24, 2025
c0da4ad
Remove untracked files that were accidentally committed
AndrewAsseily Jan 24, 2025
5c7a17f
Remove test for empty profile name handling
AndrewAsseily Feb 25, 2025
0460fc1
Merge customizations for Chime
aws-sdk-python-automation Feb 26, 2025
29ce563
Update to latest models
aws-sdk-python-automation Feb 26, 2025
e31604b
Update endpoints model
aws-sdk-python-automation Feb 26, 2025
2200fac
Bump version to 2.24.13
aws-sdk-python-automation Feb 26, 2025
4f41137
Fix sdist lockfile test (#9326)
hssyoo Feb 26, 2025
dc17c15
Merge branch 'v2' into docs-rewrite-rules
aemous Mar 7, 2025
245760b
Merge branch 'v2' into docs-rewrite-rules
aemous Mar 7, 2025
28d3a2c
Update doc/source/crosslinker.py
aemous Mar 10, 2025
839af66
Formatting
aemous Mar 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['notfound.extension',]
extensions = ['notfound.extension', 'crosslinker']
notfound_context = {
'title': 'Page not found',
'body': '<h1>Page not found</h1>\n\n'
Expand Down
205 changes: 205 additions & 0 deletions doc/source/crosslinker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import os

from sphinx.application import Sphinx

import awscli.botocore.session
import awscli.clidriver

"""Generate apache rewrite rules for cross linking.

Example:

^/goto/cli2/acm-2015-12-08/AddTagsToCertificate$

will redirect to the relative path

/cli/latest/reference/acm/add-tags-to-certificate.html

Usage
=====

Make sure you're in a venv with the correct versions of the AWS CLI v2 installed.
It will be imported directly to generate the crosslinks.

This is a Sphinx extension that gets run after all document updates have been
executed and before the cleanup phase.
"""

class AWSCLICrossLinkGenerator:
# The name of the tool, this is what's
# used in the goto links: /goto/{toolname}/{operation}
TOOL_NAME = 'cli2'
BASE_URL = '/cli/latest/reference'
# The base url for your SDK service reference. This page
# is used as a fallback for goto links for unknown service
# uids and for the "catch-all" regex for the tool.
FALLBACK_BASE = (
'/cli/latest/reference/index.html'
)
# The url used for a specific service. This value
# must have one string placeholder value where the
# service_name can be placed.
SERVICE_BASE = (
'/cli/latest/reference/%s/index.html'
)

def __init__(self):
self._driver = awscli.clidriver.create_clidriver()
# Cache of service -> operation names
# The operation names are not xformed(), they're
# exactly as they're spelled in the API reference.
self._service_operations = {}
# Mapping of service name to boto class name.
self._service_class_names = {}
# Mapping of uid -> service_name
self._uid_mapping = {}
self._generate_mappings()

def _generate_mappings(self):
command_table = self._driver.create_help_command().command_table
for name, command in command_table.items():
if hasattr(command, '_UNDOCUMENTED'):
continue
if not hasattr(command, 'service_model'):
continue
uid = command.service_model.metadata.get('uid')
if uid is None:
continue
self._uid_mapping[uid] = name
ops_table = command.create_help_command().command_table
mapping = {}
for op_name, op_command in ops_table.items():
op_help = op_command.create_help_command()
mapping[op_help.obj.name] = op_name
self._service_operations[name] = mapping

def _generate_cross_link(self, service_name, operation_name):
if operation_name not in self._service_operations[service_name]:
return self.SERVICE_BASE % service_name
return '%s/%s/%s.html' % (
self.BASE_URL, service_name,
self._service_operations[service_name][operation_name]
)

def _is_catchall_regex(self, parts):
# This is the catch-all regex used as a safety net
# for any requests to our tool that we don't understand.
# For example: /goto/aws-cli/(.*)
return len(parts) == 4 and parts[-1] == '(.*)'

def _is_service_catchall_regex(self, parts):
# This is the catch-all regex used for requests to a
# known service for an unknown operation/shape.
# For example: /goto/cli2/xray-2016-04-12/(.*)
return len(parts) == 5 and parts[-1] == '(.*)'

def generate_cross_link(self, link):
parts = link.split('/')
if len(parts) < 4:
return None
tool_name = parts[2]
if tool_name != self.TOOL_NAME:
return None
if self._is_catchall_regex(parts):
return self.FALLBACK_BASE
uid = parts[3]
if uid not in self._uid_mapping:
return self.FALLBACK_BASE
service_name = self._uid_mapping[uid]
if self._is_service_catchall_regex(parts):
return self.SERVICE_BASE % service_name
# At this point we know this is a valid cross-link
# for an operation we probably know about, so we can
# defer to the template method.
return self._generate_cross_link(
service_name=service_name,
operation_name=parts[-1],
)


def create_goto_links_iter(session):
for service_name in session.get_available_services():
m = session.get_service_model(service_name)
uid = m.metadata.get('uid')
if uid is None:
continue
for operation_name in m.operation_names:
yield '/goto/{toolname}/%s/%s' % (uid, operation_name)
# We also want to yield a catch-all link for the service.
yield '/goto/{toolname}/%s/(.*)' % uid
# And a catch-all for the entire tool.
yield '/goto/{toolname}/(.*)'


def create_rewrite_rule(incoming_link, redirect):
# Given an incoming_link (/goto/aws-cli/...) and the
# URL it should redirect to, generate the actual
# rewrite rule.
return 'RewriteRule ^%s$ %s [L,R,NE]\n' % (incoming_link, redirect)


def generate_all_cross_links(session, out_file):
# links_iter: Generator of crosslinks to generate
linker = AWSCLICrossLinkGenerator()
# This gives us a list of tuples of
# (goto_link, redirect_link)
crosslinks = generate_tool_cross_links(session, linker)
# From there we need to convert that to the actual Rewrite rules.
lines = generate_conf_for_crosslinks(linker, crosslinks)
out_file.writelines(lines)


def generate_conf_for_crosslinks(linker, crosslinks):
# These first two lines are saying that if the URL
# does not match the regex '/goto/(toolname)', then
# we should skip all the RewriteRules associated with
# that toolname.
# The way RewriteCond works is that if the condition
# evalutes to true, the immediately next RewriteRule
# is triggered.
lines = [
f'RewriteCond %%{{REQUEST_URI}} !^\\/goto\\/{linker.TOOL_NAME}\\/.*$\n',
# The S=12345 means skip the next 12345 lines. This
# rule is only triggered if the RewriteCond above
# evaluates to true. Think of this as a fancy GOTO.
f'RewriteRule ".*" "-" [S={len(crosslinks)}]\n'
]
for goto_link, redirect in crosslinks:
lines.append(create_rewrite_rule(goto_link, redirect))
return lines


def generate_tool_cross_links(session, linker):
crosslinks = []
for link_template in create_goto_links_iter(session):
link_str = link_template.format(toolname=linker.TOOL_NAME)
result = linker.generate_cross_link(link_str)
if result is not None:
crosslinks.append((link_str, result))
return crosslinks


def generate_crosslinks(app: Sphinx):
session = awscli.botocore.session.get_session()
out_path = os.path.join(os.path.abspath(app.outdir), 'package.redirects.conf')
with open(out_path, 'w') as out_file:
generate_all_cross_links(session, out_file)

# Sphinx expects us to return an iterable of pages we want to create using
# their template/context system. We return an empty list since this extension
# does not create HTML pages via this system.
return []


def setup(app: Sphinx):
# hook into the html-collect-pages event to guarantee all
# document writing/modification has been completed before
# generating crosslinks.
app.connect('html-collect-pages', generate_crosslinks)

return {
'version': '1.0',
'env_version': 1,
'parallel_read_safe': True,
'parallel_write_safe': True,
}
Loading