Skip to content

Commit 04ef99d

Browse files
authored
Merge pull request #514 from linode/dev
v5.42.1
2 parents f2fe95f + 9246840 commit 04ef99d

File tree

14 files changed

+273
-53
lines changed

14 files changed

+273
-53
lines changed

README.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,14 @@ in addition to its normal output. If these warnings can interfere with your
220220
scripts or you otherwise want them disabled, simply add the ``--suppress-warnings``
221221
flag to prevent them from being emitted.
222222

223+
Suppressing Warnings
224+
""""""""""""""""""""
225+
226+
Sometimes the API responds with a error that can be ignored. For example a timeout
227+
or nginx response that can't be parsed correctly, by default the CLI will retry
228+
calls on these errors we've identified. If you'd like to disable this behavior for
229+
any reason use the ``--no-retry`` flag.
230+
223231
Shell Completion
224232
""""""""""""""""
225233

linodecli/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ def main(): # pylint: disable=too-many-branches,too-many-statements
8383
cli.output_handler.columns = parsed.format
8484

8585
cli.defaults = not parsed.no_defaults
86+
cli.retry_count = 0
87+
cli.no_retry = parsed.no_retry
8688
cli.suppress_warnings = parsed.suppress_warnings
8789

8890
if parsed.all_columns or parsed.all:

linodecli/api_request.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import itertools
66
import json
77
import sys
8+
import time
89
from sys import version_info
910
from typing import Iterable, List, Optional
1011

@@ -92,6 +93,11 @@ def do_request(
9293
if ctx.debug_request:
9394
_print_response_debug_info(result)
9495

96+
while _check_retry(result) and not ctx.no_retry and ctx.retry_count < 3:
97+
time.sleep(_get_retry_after(result.headers))
98+
ctx.retry_count += 1
99+
result = method(url, headers=headers, data=body, verify=API_CA_PATH)
100+
95101
_attempt_warn_old_version(ctx, result)
96102

97103
if not 199 < result.status_code < 399 and not skip_error_handling:
@@ -361,3 +367,24 @@ def _handle_error(ctx, response):
361367
columns=["field", "reason"],
362368
)
363369
sys.exit(1)
370+
371+
372+
def _check_retry(response):
373+
"""
374+
Check for valid retry scenario, returns true if retry is valid
375+
"""
376+
if response.status_code in (408, 429):
377+
# request timed out or rate limit exceeded
378+
return True
379+
380+
return (
381+
response.headers
382+
and response.status_code == 400
383+
and response.headers.get("Server") == "nginx"
384+
and response.headers.get("Content-Type") == "text/html"
385+
)
386+
387+
388+
def _get_retry_after(headers):
389+
retry_str = headers.get("Retry-After", "")
390+
return int(retry_str) if retry_str else 0

linodecli/arg_helpers.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from linodecli import plugins
1717

1818
from .completion import bake_completions
19-
from .helpers import register_args_shared
19+
from .helpers import pagination_args_shared, register_args_shared
2020

2121

2222
def register_args(parser):
@@ -76,21 +76,6 @@ def register_args(parser):
7676
action="store_true",
7777
help="If set, does not display headers in output.",
7878
)
79-
parser.add_argument(
80-
"--page",
81-
metavar="PAGE",
82-
default=1,
83-
type=int,
84-
help="For listing actions, specifies the page to request",
85-
)
86-
parser.add_argument(
87-
"--page-size",
88-
metavar="PAGESIZE",
89-
default=100,
90-
type=int,
91-
help="For listing actions, specifies the number of items per page, "
92-
"accepts any value between 25 and 500",
93-
)
9479
parser.add_argument(
9580
"--all",
9681
action="store_true",
@@ -126,6 +111,11 @@ def register_args(parser):
126111
default=False,
127112
help="Prevent the truncation of long values in command outputs.",
128113
)
114+
parser.add_argument(
115+
"--no-retry",
116+
action="store_true",
117+
help="Skip retrying on common errors like timeouts.",
118+
)
129119
parser.add_argument(
130120
"--column-width",
131121
type=int,
@@ -143,6 +133,7 @@ def register_args(parser):
143133
"--debug", action="store_true", help="Enable verbose HTTP debug output."
144134
)
145135

136+
pagination_args_shared(parser)
146137
register_args_shared(parser)
147138

148139
return parser

linodecli/baked/request.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def _parse_request_model(schema, prefix=None, list_of_objects=False):
9999

100100
if schema.properties is not None:
101101
for k, v in schema.properties.items():
102-
if v.type == "object":
102+
if v.type == "object" and not v.readOnly and v.properties:
103103
# nested objects receive a prefix and are otherwise parsed normally
104104
pref = prefix + "." + k if prefix else k
105105
args += _parse_request_model(v, prefix=pref)

linodecli/baked/response.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,14 @@ def __init__(self, response):
187187
)
188188
else:
189189
self.attrs = _parse_response_model(response.schema)
190-
self.rows = response.schema.extensions.get("linode-cli-rows")
190+
self.rows = response.extensions.get("linode-cli-rows")
191191
self.nested_list = response.extensions.get("linode-cli-nested-list")
192192

193193
def fix_json(self, json):
194194
"""
195195
Formats JSON from the API into a list of rows
196196
"""
197+
197198
if self.rows:
198199
return self._fix_json_rows(json)
199200
if self.nested_list:

linodecli/helpers.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,33 @@ def filter_markdown_links(text):
6363
return result
6464

6565

66+
def pagination_args_shared(parser: ArgumentParser):
67+
"""
68+
Add pagination related arguments to the given
69+
ArgumentParser that may be shared across the CLI and plugins.
70+
"""
71+
parser.add_argument(
72+
"--page",
73+
metavar="PAGE",
74+
default=1,
75+
type=int,
76+
help="For listing actions, specifies the page to request",
77+
)
78+
parser.add_argument(
79+
"--page-size",
80+
metavar="PAGESIZE",
81+
default=100,
82+
type=int,
83+
help="For listing actions, specifies the number of items per page, "
84+
"accepts any value between 25 and 500",
85+
)
86+
parser.add_argument(
87+
"--all-rows",
88+
action="store_true",
89+
help="Output all possible rows in the results with pagination",
90+
)
91+
92+
6693
def register_args_shared(parser: ArgumentParser):
6794
"""
6895
Adds certain arguments to the given ArgumentParser that may be shared across
@@ -87,12 +114,6 @@ def register_args_shared(parser: ArgumentParser):
87114
"This is useful for scripting the CLI's behavior.",
88115
)
89116

90-
parser.add_argument(
91-
"--all-rows",
92-
action="store_true",
93-
help="Output all possible rows in the results with pagination",
94-
)
95-
96117
return parser
97118

98119

linodecli/plugins/obj/__init__.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,15 @@
1414
from contextlib import suppress
1515
from datetime import datetime
1616
from math import ceil
17-
from pathlib import Path
18-
from typing import List
17+
from typing import Iterable, List
1918

2019
from rich import print as rprint
2120
from rich.table import Table
2221

2322
from linodecli.cli import CLI
2423
from linodecli.configuration import _do_get_request
2524
from linodecli.configuration.helpers import _default_thing_input
26-
from linodecli.helpers import expand_globs
25+
from linodecli.helpers import expand_globs, pagination_args_shared
2726
from linodecli.plugins import PluginContext, inherit_plugin_args
2827
from linodecli.plugins.obj.buckets import create_bucket, delete_bucket
2928
from linodecli.plugins.obj.config import (
@@ -70,14 +69,36 @@
7069
except ImportError:
7170
HAS_BOTO = False
7271

72+
TRUNCATED_MSG = (
73+
"Notice: Not all results were shown. If your would "
74+
"like to get more results, you can add the '--all-row' "
75+
"flag to the command or use the built-in pagination flags."
76+
)
77+
78+
INVALID_PAGE_MSG = "No result to show in this page."
79+
80+
81+
def flip_to_page(iterable: Iterable, page: int = 1):
82+
"""Given a iterable object and return a specific iteration (page)"""
83+
iterable = iter(iterable)
84+
for _ in range(page - 1):
85+
try:
86+
next(iterable)
87+
except StopIteration:
88+
print(INVALID_PAGE_MSG)
89+
sys.exit(2)
90+
91+
return next(iterable)
92+
7393

7494
def list_objects_or_buckets(
7595
get_client, args, **kwargs
76-
): # pylint: disable=too-many-locals,unused-argument
96+
): # pylint: disable=too-many-locals,unused-argument,too-many-branches
7797
"""
7898
Lists buckets or objects
7999
"""
80100
parser = inherit_plugin_args(ArgumentParser(PLUGIN_BASE + " ls"))
101+
pagination_args_shared(parser)
81102

82103
parser.add_argument(
83104
"bucket",
@@ -106,16 +127,30 @@ def list_objects_or_buckets(
106127
prefix = ""
107128

108129
data = []
130+
objects = []
131+
sub_directories = []
132+
pages = client.get_paginator("list_objects_v2").paginate(
133+
Prefix=prefix,
134+
Bucket=bucket_name,
135+
Delimiter="/",
136+
PaginationConfig={"PageSize": parsed.page_size},
137+
)
109138
try:
110-
response = client.list_objects_v2(
111-
Prefix=prefix, Bucket=bucket_name, Delimiter="/"
112-
)
139+
if parsed.all_rows:
140+
results = pages
141+
else:
142+
page = flip_to_page(pages, parsed.page)
143+
if page.get("IsTruncated", False):
144+
print(TRUNCATED_MSG)
145+
146+
results = [page]
113147
except client.exceptions.NoSuchBucket:
114148
print("No bucket named " + bucket_name)
115149
sys.exit(2)
116150

117-
objects = response.get("Contents", [])
118-
sub_directories = response.get("CommonPrefixes", [])
151+
for item in results:
152+
objects.extend(item.get("Contents", []))
153+
sub_directories.extend(item.get("CommonPrefixes", []))
119154

120155
for d in sub_directories:
121156
data.append((" " * 16, "DIR", d.get("Prefix")))
@@ -329,17 +364,31 @@ def list_all_objects(
329364
"""
330365
# this is for printing help when --help is in the args
331366
parser = inherit_plugin_args(ArgumentParser(PLUGIN_BASE + " la"))
367+
pagination_args_shared(parser)
332368

333-
parser.parse_args(args)
369+
parsed = parser.parse_args(args)
334370

335371
client = get_client()
336372

337-
# all buckets
338373
buckets = [b["Name"] for b in client.list_buckets().get("Buckets", [])]
339374

340375
for b in buckets:
341376
print()
342-
objects = client.list_objects_v2(Bucket=b).get("Contents", [])
377+
objects = []
378+
pages = client.get_paginator("list_objects_v2").paginate(
379+
Bucket=b, PaginationConfig={"PageSize": parsed.page_size}
380+
)
381+
if parsed.all_rows:
382+
results = pages
383+
else:
384+
page = flip_to_page(pages, parsed.page)
385+
if page.get("IsTruncated", False):
386+
print(TRUNCATED_MSG)
387+
388+
results = [page]
389+
390+
for page in results:
391+
objects.extend(page.get("Contents", []))
343392

344393
for obj in objects:
345394
size = obj.get("Size", 0)

tests/integration/helpers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,7 @@ def exec_failing_test_command(args: List[str]):
159159
)
160160
assert process.returncode == 1
161161
return process
162+
163+
164+
def count_lines(text: str):
165+
return len(list(filter(len, text.split("\n"))))

0 commit comments

Comments
 (0)