Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/customizations/paginate.py
2623 views
1
# Copyright 2013 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.
13
"""This module has customizations to unify paging parameters.
14
15
For any operation that can be paginated, we will:
16
17
* Hide the service specific pagination params. This can vary across
18
services and we're going to replace them with a consistent set of
19
arguments. The arguments will still work, but they are not
20
documented. This allows us to add a pagination config after
21
the fact and still remain backwards compatible with users that
22
were manually doing pagination.
23
* Add a ``--starting-token`` and a ``--max-items`` argument.
24
25
"""
26
import logging
27
import sys
28
from functools import partial
29
30
from awscli.customizations.utils import uni_print
31
from botocore import xform_name
32
from botocore.exceptions import DataNotFoundError, PaginationError
33
from botocore import model
34
35
from awscli.arguments import BaseCLIArgument
36
from awscli.utils import resolve_v2_debug_mode
37
38
logger = logging.getLogger(__name__)
39
40
41
STARTING_TOKEN_HELP = """
42
<p>A token to specify where to start paginating. This is the
43
<code>NextToken</code> from a previously truncated response.</p>
44
<p>For usage examples, see <a
45
href="https://docs.aws.amazon.com/cli/latest/userguide/pagination.html"
46
>Pagination</a> in the <i>AWS Command Line Interface User
47
Guide</i>.</p>
48
"""
49
50
MAX_ITEMS_HELP = """
51
<p>The total number of items to return in the command's output.
52
If the total number of items available is more than the value
53
specified, a <code>NextToken</code> is provided in the command's
54
output. To resume pagination, provide the
55
<code>NextToken</code> value in the <code>starting-token</code>
56
argument of a subsequent command. <b>Do not</b> use the
57
<code>NextToken</code> response element directly outside of the
58
AWS CLI.</p>
59
<p>For usage examples, see <a
60
href="https://docs.aws.amazon.com/cli/latest/userguide/pagination.html"
61
>Pagination</a> in the <i>AWS Command Line Interface User
62
Guide</i>.</p>
63
"""
64
65
PAGE_SIZE_HELP = """
66
<p>The size of each page to get in the AWS service call. This
67
does not affect the number of items returned in the command's
68
output. Setting a smaller page size results in more calls to
69
the AWS service, retrieving fewer items in each call. This can
70
help prevent the AWS service calls from timing out.</p>
71
<p>For usage examples, see <a
72
href="https://docs.aws.amazon.com/cli/latest/userguide/pagination.html"
73
>Pagination</a> in the <i>AWS Command Line Interface User
74
Guide</i>.</p>
75
"""
76
77
78
def register_pagination(event_handlers):
79
event_handlers.register('building-argument-table', unify_paging_params)
80
event_handlers.register_last('doc-description', add_paging_description)
81
82
83
def get_paginator_config(session, service_name, operation_name):
84
try:
85
paginator_model = session.get_paginator_model(service_name)
86
except DataNotFoundError:
87
return None
88
try:
89
operation_paginator_config = paginator_model.get_paginator(
90
operation_name)
91
except ValueError:
92
return None
93
return operation_paginator_config
94
95
96
def add_paging_description(help_command, **kwargs):
97
# This customization is only applied to the description of
98
# Operations, so we must filter out all other events.
99
if not isinstance(help_command.obj, model.OperationModel):
100
return
101
service_name = help_command.obj.service_model.service_name
102
paginator_config = get_paginator_config(
103
help_command.session, service_name, help_command.obj.name)
104
if not paginator_config:
105
return
106
help_command.doc.style.new_paragraph()
107
help_command.doc.writeln(
108
('``%s`` is a paginated operation. Multiple API calls may be issued '
109
'in order to retrieve the entire data set of results. You can '
110
'disable pagination by providing the ``--no-paginate`` argument.')
111
% help_command.name)
112
# Only include result key information if it is present.
113
if paginator_config.get('result_key'):
114
queries = paginator_config['result_key']
115
if type(queries) is not list:
116
queries = [queries]
117
queries = ", ".join([('``%s``' % s) for s in queries])
118
help_command.doc.writeln(
119
('When using ``--output text`` and the ``--query`` argument on a '
120
'paginated response, the ``--query`` argument must extract data '
121
'from the results of the following query expressions: %s')
122
% queries)
123
124
125
def unify_paging_params(argument_table, operation_model, event_name,
126
session, **kwargs):
127
paginator_config = get_paginator_config(
128
session, operation_model.service_model.service_name,
129
operation_model.name)
130
if paginator_config is None:
131
# We only apply these customizations to paginated responses.
132
return
133
logger.debug("Modifying paging parameters for operation: %s",
134
operation_model.name)
135
_remove_existing_paging_arguments(argument_table, paginator_config)
136
parsed_args_event = event_name.replace('building-argument-table.',
137
'operation-args-parsed.')
138
call_parameters_event = event_name.replace(
139
'building-argument-table', 'calling-command'
140
)
141
shadowed_args = {}
142
add_paging_argument(argument_table, 'starting-token',
143
PageArgument('starting-token', STARTING_TOKEN_HELP,
144
parse_type='string',
145
serialized_name='StartingToken'),
146
shadowed_args)
147
input_members = operation_model.input_shape.members
148
type_name = 'integer'
149
if 'limit_key' in paginator_config:
150
limit_key_shape = input_members[paginator_config['limit_key']]
151
type_name = limit_key_shape.type_name
152
if type_name not in PageArgument.type_map:
153
raise TypeError(
154
('Unsupported pagination type {0} for operation {1}'
155
' and parameter {2}').format(
156
type_name, operation_model.name,
157
paginator_config['limit_key']))
158
add_paging_argument(argument_table, 'page-size',
159
PageArgument('page-size', PAGE_SIZE_HELP,
160
parse_type=type_name,
161
serialized_name='PageSize'),
162
shadowed_args)
163
164
add_paging_argument(argument_table, 'max-items',
165
PageArgument('max-items', MAX_ITEMS_HELP,
166
parse_type=type_name,
167
serialized_name='MaxItems'),
168
shadowed_args)
169
session.register(
170
parsed_args_event,
171
partial(check_should_enable_pagination,
172
list(_get_all_cli_input_tokens(paginator_config)),
173
shadowed_args, argument_table))
174
session.register(
175
call_parameters_event,
176
partial(
177
check_should_enable_pagination_call_parameters,
178
session,
179
list(_get_all_input_tokens(paginator_config)),
180
),
181
)
182
183
184
def add_paging_argument(argument_table, arg_name, argument, shadowed_args):
185
if arg_name in argument_table:
186
# If there's already an entry in the arg table for this argument,
187
# this means we're shadowing an argument for this operation. We
188
# need to store this later in case pagination is turned off because
189
# we put these arguments back.
190
# See the comment in check_should_enable_pagination() for more info.
191
shadowed_args[arg_name] = argument_table[arg_name]
192
argument_table[arg_name] = argument
193
194
195
def check_should_enable_pagination(input_tokens, shadowed_args, argument_table,
196
parsed_args, parsed_globals, **kwargs):
197
normalized_paging_args = ['start_token', 'max_items']
198
for token in input_tokens:
199
py_name = token.replace('-', '_')
200
if getattr(parsed_args, py_name) is not None and \
201
py_name not in normalized_paging_args:
202
# The user has specified a manual (undocumented) pagination arg.
203
# We need to automatically turn pagination off.
204
logger.debug("User has specified a manual pagination arg. "
205
"Automatically setting --no-paginate.")
206
parsed_globals.paginate = False
207
208
if not parsed_globals.paginate:
209
ensure_paging_params_not_set(parsed_args, shadowed_args)
210
# Because pagination is now disabled, there's a chance that
211
# we were shadowing arguments. For example, we inject a
212
# --max-items argument in unify_paging_params(). If the
213
# the operation also provides its own MaxItems (which we
214
# expose as --max-items) then our custom pagination arg
215
# was shadowing the customers arg. When we turn pagination
216
# off we need to put back the original argument which is
217
# what we're doing here.
218
for key, value in shadowed_args.items():
219
argument_table[key] = value
220
221
222
def ensure_paging_params_not_set(parsed_args, shadowed_args):
223
paging_params = ['starting_token', 'page_size', 'max_items']
224
shadowed_params = [p.replace('-', '_') for p in shadowed_args.keys()]
225
params_used = [p for p in paging_params if
226
p not in shadowed_params and getattr(parsed_args, p, None)]
227
228
if len(params_used) > 0:
229
converted_params = ', '.join(
230
["--" + p.replace('_', '-') for p in params_used])
231
raise PaginationError(
232
message="Cannot specify --no-paginate along with pagination "
233
"arguments: %s" % converted_params)
234
235
236
def _remove_existing_paging_arguments(argument_table, pagination_config):
237
for cli_name in _get_all_cli_input_tokens(pagination_config):
238
argument_table[cli_name]._UNDOCUMENTED = True
239
240
241
def _get_all_cli_input_tokens(pagination_config):
242
# Get all input tokens including the limit_key
243
# if it exists.
244
tokens = _get_input_tokens(pagination_config)
245
for token_name in tokens:
246
cli_name = xform_name(token_name, '-')
247
yield cli_name
248
if 'limit_key' in pagination_config:
249
key_name = pagination_config['limit_key']
250
cli_name = xform_name(key_name, '-')
251
yield cli_name
252
253
254
# Get all tokens but return them in API namespace rather than CLI namespace
255
def _get_all_input_tokens(pagination_config):
256
# Get all input tokens including the limit_key
257
# if it exists.
258
tokens = _get_input_tokens(pagination_config)
259
for token_name in tokens:
260
yield token_name
261
if 'limit_key' in pagination_config:
262
key_name = pagination_config['limit_key']
263
yield key_name
264
265
266
def _get_input_tokens(pagination_config):
267
tokens = pagination_config['input_token']
268
if not isinstance(tokens, list):
269
return [tokens]
270
return tokens
271
272
273
def _get_cli_name(param_objects, token_name):
274
for param in param_objects:
275
if param.name == token_name:
276
return param.cli_name.lstrip('-')
277
278
279
def check_should_enable_pagination_call_parameters(
280
session,
281
input_tokens,
282
call_parameters,
283
parsed_args,
284
parsed_globals,
285
**kwargs
286
):
287
"""
288
Check for pagination args in the actual calling arguments passed to
289
the function.
290
291
If the user is using the --cli-input-json parameter to provide JSON
292
parameters they are all in the API naming space rather than the CLI
293
naming space and would be missed by the processing above. This function
294
gets called on the calling-command event.
295
"""
296
if resolve_v2_debug_mode(parsed_globals):
297
cli_input_json_data = session.emit_first_non_none_response(
298
f"get-cli-input-json-data",
299
)
300
if cli_input_json_data is None:
301
cli_input_json_data = {}
302
pagination_params_in_input_tokens = [
303
param for param in cli_input_json_data if param in input_tokens
304
]
305
if pagination_params_in_input_tokens:
306
uni_print(
307
'\nAWS CLI v2 UPGRADE WARNING: In AWS CLI v2, if you specify '
308
'pagination parameters by using a file with the '
309
'`--cli-input-json` parameter, automatic pagination will be '
310
'turned off. This is different from v1 behavior, where '
311
'pagination parameters specified via the `--cli-input-json` '
312
'parameter are ignored. To retain AWS CLI v1 behavior in '
313
'AWS CLI v2, remove all pagination parameters from the input '
314
'JSON. See https://docs.aws.amazon.com/cli/latest/userguide/'
315
'cliv2-migration-changes.html'
316
'#cliv2-migration-skeleton-paging.\n',
317
out_file=sys.stderr
318
)
319
320
321
class PageArgument(BaseCLIArgument):
322
type_map = {
323
'string': str,
324
'integer': int,
325
'long': int,
326
}
327
328
def __init__(self, name, documentation, parse_type, serialized_name):
329
self.argument_model = model.Shape('PageArgument', {'type': 'string'})
330
self._name = name
331
self._serialized_name = serialized_name
332
self._documentation = documentation
333
self._parse_type = parse_type
334
self._required = False
335
336
def _emit_non_positive_max_items_warning(self):
337
uni_print(
338
"warning: Non-positive values for --max-items may result in undefined behavior.\n",
339
sys.stderr)
340
341
@property
342
def cli_name(self):
343
return '--' + self._name
344
345
@property
346
def cli_type_name(self):
347
return self._parse_type
348
349
@property
350
def required(self):
351
return self._required
352
353
@required.setter
354
def required(self, value):
355
self._required = value
356
357
@property
358
def documentation(self):
359
return self._documentation
360
361
def add_to_parser(self, parser):
362
parser.add_argument(self.cli_name, dest=self.py_name,
363
type=self.type_map[self._parse_type])
364
365
def add_to_params(self, parameters, value):
366
if value is not None:
367
if self._serialized_name == 'MaxItems' and int(value) <= 0:
368
self._emit_non_positive_max_items_warning()
369
pagination_config = parameters.get('PaginationConfig', {})
370
pagination_config[self._serialized_name] = value
371
parameters['PaginationConfig'] = pagination_config
372
373