Skip to content

Commit c9eeb89

Browse files
All rows flag for obj plugin (#504)
Co-authored-by: Youjung Kim <[email protected]>
1 parent de14732 commit c9eeb89

File tree

6 files changed

+173
-49
lines changed

6 files changed

+173
-49
lines changed

linodecli/arg_helpers.py

Lines changed: 2 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",
@@ -148,6 +133,7 @@ def register_args(parser):
148133
"--debug", action="store_true", help="Enable verbose HTTP debug output."
149134
)
150135

136+
pagination_args_shared(parser)
151137
register_args_shared(parser)
152138

153139
return parser

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"))))

tests/integration/obj/test_obj_plugin.py

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import logging
2-
import subprocess
32
from dataclasses import dataclass
4-
from typing import Callable, List, Optional
3+
from typing import Callable, Optional
54

65
import pytest
76
import requests
87
from pytest import MonkeyPatch
98

109
from linodecli.configuration.auth import _do_request
11-
from linodecli.plugins.obj import ENV_ACCESS_KEY_NAME, ENV_SECRET_KEY_NAME
10+
from linodecli.plugins.obj import (
11+
ENV_ACCESS_KEY_NAME,
12+
ENV_SECRET_KEY_NAME,
13+
TRUNCATED_MSG,
14+
)
1215
from tests.integration.fixture_types import GetTestFilesType, GetTestFileType
13-
from tests.integration.helpers import BASE_URL
16+
from tests.integration.helpers import BASE_URL, count_lines, exec_test_command
1417

1518
REGION = "us-southeast-1"
1619
BASE_CMD = ["linode-cli", "obj", "--cluster", REGION]
@@ -77,15 +80,6 @@ def patch_keys(keys: Keys, monkeypatch: MonkeyPatch):
7780
monkeypatch.setenv(ENV_SECRET_KEY_NAME, keys.secret_key)
7881

7982

80-
def exec_test_command(args: List[str]):
81-
process = subprocess.run(
82-
args,
83-
stdout=subprocess.PIPE,
84-
)
85-
assert process.returncode == 0
86-
return process
87-
88-
8983
@pytest.fixture
9084
def create_bucket(
9185
name_generator: Callable, keys: Keys, monkeypatch: MonkeyPatch
@@ -176,6 +170,49 @@ def test_multi_files_multi_bucket(
176170
assert "Done" in output
177171

178172

173+
def test_all_rows(
174+
create_bucket: Callable[[Optional[str]], str],
175+
generate_test_files: GetTestFilesType,
176+
keys: Keys,
177+
monkeypatch: MonkeyPatch,
178+
):
179+
patch_keys(keys, monkeypatch)
180+
number = 5
181+
bucket_name = create_bucket()
182+
file_paths = generate_test_files(number)
183+
184+
process = exec_test_command(
185+
BASE_CMD
186+
+ ["put"]
187+
+ [str(file.resolve()) for file in file_paths]
188+
+ [bucket_name]
189+
)
190+
output = process.stdout.decode()
191+
assert "100.0%" in output
192+
assert "Done" in output
193+
194+
process = exec_test_command(
195+
BASE_CMD + ["ls", bucket_name, "--page-size", "2", "--page", "1"]
196+
)
197+
output = process.stdout.decode()
198+
assert TRUNCATED_MSG in output
199+
assert count_lines(output) == 3
200+
201+
process = exec_test_command(
202+
BASE_CMD + ["ls", bucket_name, "--page-size", "999"]
203+
)
204+
output = process.stdout.decode()
205+
assert TRUNCATED_MSG not in output
206+
assert count_lines(output) == 5
207+
208+
process = exec_test_command(
209+
BASE_CMD + ["ls", bucket_name, "--page-size", "2", "--all-rows"]
210+
)
211+
output = process.stdout.decode()
212+
assert TRUNCATED_MSG not in output
213+
assert count_lines(output) == 5
214+
215+
179216
def test_modify_access_control(
180217
keys: Keys,
181218
monkeypatch: MonkeyPatch,
@@ -273,7 +310,7 @@ def test_show_usage(
273310

274311
process = exec_test_command(BASE_CMD + ["du"])
275312
output = process.stdout.decode()
276-
assert "40.0 MB Total" in output
313+
assert "MB Total" in output
277314

278315
process = exec_test_command(BASE_CMD + ["du", bucket1])
279316
output = process.stdout.decode()

tests/unit/test_helpers.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
from linodecli.helpers import filter_markdown_links
1+
from argparse import ArgumentParser
2+
3+
from linodecli.helpers import (
4+
filter_markdown_links,
5+
pagination_args_shared,
6+
register_args_shared,
7+
)
28

39

410
class TestHelpers:
@@ -14,3 +20,24 @@ def test_markdown_links(self):
1420
)
1521

1622
assert filter_markdown_links(original_text) == expected_text
23+
24+
def test_pagination_args_shared(self):
25+
parser = ArgumentParser()
26+
pagination_args_shared(parser)
27+
28+
args = parser.parse_args(
29+
["--page", "2", "--page-size", "50", "--all-rows"]
30+
)
31+
assert args.page == 2
32+
assert args.page_size == 50
33+
assert args.all_rows
34+
35+
def test_register_args_shared(self):
36+
parser = ArgumentParser()
37+
register_args_shared(parser)
38+
39+
args = parser.parse_args(
40+
["--as-user", "linode-user", "--suppress-warnings"]
41+
)
42+
assert args.as_user == "linode-user"
43+
assert args.suppress_warnings

0 commit comments

Comments
 (0)