File: crosslinker.py

package info (click to toggle)
awscli 2.31.35-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 156,692 kB
  • sloc: python: 213,816; xml: 14,082; makefile: 189; sh: 178; javascript: 8
file content (205 lines) | stat: -rw-r--r-- 7,535 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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,
    }