def test_indent_lines(): console = Console(width=100, color_system=None) console.begin_capture() console.print(Pretty([100, 200], indent_guides=True), width=8) expected = """\ [ │ 100, │ 200 ] """ result = console.end_capture() print(repr(result)) print(result) assert result == expected
def test_render(): tree = Tree("foo") tree.add("bar", style="italic") baz_tree = tree.add("baz", guide_style="bold red", style="on blue") baz_tree.add("1") baz_tree.add("2") tree.add("egg") console = Console(width=20, force_terminal=True, color_system="standard") console.begin_capture() console.print(tree) result = console.end_capture() print(repr(result)) expected = "foo \n├── \x1b[3mbar\x1b[0m\x1b[3m \x1b[0m\n\x1b[44m├── \x1b[0m\x1b[44mbaz\x1b[0m\x1b[44m \x1b[0m\n\x1b[44m│ \x1b[0m\x1b[1;31;44m┣━━ \x1b[0m\x1b[44m1\x1b[0m\x1b[44m \x1b[0m\n\x1b[44m│ \x1b[0m\x1b[1;31;44m┗━━ \x1b[0m\x1b[44m2\x1b[0m\x1b[44m \x1b[0m\n└── egg \n" assert result == expected
def test_render_tree_hide_root_non_win32(): tree = Tree("foo", hide_root=True) tree.add("bar", style="italic") baz_tree = tree.add("baz", guide_style="bold red", style="on blue") baz_tree.add("1") baz_tree.add("2") tree.add("egg") console = Console(width=20, force_terminal=True, color_system="standard", _environ={}) console.begin_capture() console.print(tree) result = console.end_capture() print(repr(result)) expected = "\x1b[3mbar\x1b[0m\x1b[3m \x1b[0m\n\x1b[44mbaz\x1b[0m\x1b[44m \x1b[0m\n\x1b[31;44m┣━━ \x1b[0m\x1b[44m1\x1b[0m\x1b[44m \x1b[0m\n\x1b[31;44m┗━━ \x1b[0m\x1b[44m2\x1b[0m\x1b[44m \x1b[0m\negg \n" assert result == expected
def test_ipy_display_hook__multiple_special_reprs(): """ The case where there are multiple IPython special _repr_*_ methods on the object, and one of them returns None but another one does not. """ console = Console(file=io.StringIO(), force_jupyter=True) class Thing: def _repr_latex_(self): return None def _repr_html_(self): return "hello" console.begin_capture() _ipy_display_hook(Thing(), console=console) assert console.end_capture() == ""
def test_spinner_render(): time = 0.0 def get_time(): nonlocal time return time console = Console(width=80, color_system=None, force_terminal=True, get_time=get_time) console.begin_capture() spinner = Spinner("dots", "Foo") console.print(spinner) time += 80 / 1000 console.print(spinner) result = console.end_capture() print(repr(result)) expected = "⠋ Foo\n⠙ Foo\n" assert result == expected
def test_ipy_display_hook__special_repr_raises_exception(): """ When an IPython special repr method raises an exception, we treat it as if it doesn't exist and look for the next. """ console = Console(file=io.StringIO(), force_jupyter=True) class Thing: def _repr_markdown_(self): raise Exception() def _repr_latex_(self): return None def _repr_html_(self): return "hello" console.begin_capture() _ipy_display_hook(Thing(), console=console) assert console.end_capture() == ""
def test_traceback_console_theme_applies(): """ Ensure that themes supplied via Console init work on Tracebacks. Regression test for https://github.com/Textualize/rich/issues/1786 """ r, g, b = 123, 234, 123 console = Console( force_terminal=True, _environ={"COLORTERM": "truecolor"}, theme=Theme({"traceback.title": f"rgb({r},{g},{b})"}), ) console.begin_capture() try: 1 / 0 except Exception: console.print_exception() result = console.end_capture() assert f"\\x1b[38;2;{r};{g};{b}mTraceback \\x1b[0m" in repr(result)
def test_pretty_namedtuple(): console = Console(color_system=None) console.begin_capture() example_namedtuple = StockKeepingUnit( "Sparkling British Spring Water", "Carbonated spring water", 0.9, "water", ["its amazing!", "its terrible!"], ) result = pretty_repr(example_namedtuple) print(result) assert (result == """StockKeepingUnit( name='Sparkling British Spring Water', description='Carbonated spring water', price=0.9, category='water', reviews=['its amazing!', 'its terrible!'] )""")
def test_progress_max_refresh() -> None: """Test max_refresh argment.""" time = 0.0 def get_time() -> float: nonlocal time try: return time finally: time = time + 1.0 console = Console( color_system=None, width=80, legacy_windows=False, force_terminal=True, _environ={}, ) column = TextColumn("{task.description}") column.max_refresh = 3 progress = Progress( column, get_time=get_time, auto_refresh=False, console=console, ) console.begin_capture() with progress: task_id = progress.add_task("start") for tick in range(6): progress.update(task_id, description=f"tick {tick}") progress.refresh() result = console.end_capture() print(repr(result)) assert ( result == "\x1b[?25l\r\x1b[2Kstart\r\x1b[2Kstart\r\x1b[2Ktick 1\r\x1b[2Ktick 1\r\x1b[2Ktick 3\r\x1b[2Ktick 3\r\x1b[2Ktick 5\r\x1b[2Ktick 5\n\x1b[?25h" )
def parse_caas_response(data: Union[dict, List[dict]]) -> List[str]: """Parses Response Object from caas API updates output attribute """ console = Console(emoji=False, record=True) data = utils.unlistify(data) out = [] lines = f"[reset]{'-' * 22}" if data.get("_global_result", {}).get("status", '') == 0: global_res = "[bright_green]Success[/bright_green]" else: global_res = "[red]Failure[/red]" out += [lines, f"Global Result: {global_res}", lines] if data.get("cli_cmds_result"): out += ["\n -- [cyan bold]Command Results[/cyan bold] --"] for cmd_resp in data["cli_cmds_result"]: for _c, _r in cmd_resp.items(): _r_code = _r.get("status") if _r_code == 0: _r_pretty = "[bright_green]OK[/bright_green]" elif _r_code == 2: _r_pretty = "[dark_orange3]WARNING[/dark_orange3]" else: _r_pretty = f"[red]ERROR[/red]" if _r_code == 1 else f"[red]ERROR ({_r_code})[/red]" out += [f" [{_r_pretty}] {_c}"] cmd_status = _r.get('status_str') if cmd_status: _r_txt = f"[italic]{escape(cmd_status)}[/italic]" out += [lines, _r_txt, lines] console.begin_capture() console.print("\n".join(out)) out = console.end_capture() return out
def test_spinner_update(): time = 0.0 def get_time(): nonlocal time return time console = Console(width=20, force_terminal=True, get_time=get_time) console.begin_capture() spinner = Spinner("dots") console.print(spinner) spinner.update(text="Bar", style="green", speed=2) time += 80 / 1000 console.print(spinner) spinner.update(text=Rule("Bar")) time += 80 / 1000 console.print(spinner) result = console.end_capture() print(repr(result)) expected = f"⠋\n\x1b[32m⠙\x1b[0m Bar\n\x1b[32m⠸\x1b[0m \x1b[92m────── \x1b[0mBar\x1b[92m ───────\x1b[0m\n" assert result == expected
def test_render_simple(): console = Console(width=80) console.begin_capture() console.print(Text("foo")) result = console.end_capture() assert result == "foo\n"
def test_pprint_max_string(): console = Console(color_system=None) console.begin_capture() pprint(["Hello" * 20], console=console, max_string=8) assert console.end_capture() == """['HelloHel'+92]\n"""
def test_pprint_max_items(): console = Console(color_system=None) console.begin_capture() pprint({"foo": 1, "bar": 2, "egg": 3}, console=console, max_length=2) assert console.end_capture() == """{'foo': 1, 'bar': 2, ... +1}\n"""
def test_pprint_max_values(): console = Console(color_system=None) console.begin_capture() pprint([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], console=console, max_length=2) assert console.end_capture() == "[1, 2, ... +8]\n"
def test_pprint(): console = Console(color_system=None) console.begin_capture() pprint(1, console=console) assert console.end_capture() == "1\n"
def test_justify(): console = Console(width=20, log_path=False, log_time=False, color_system=None) console.begin_capture() console.log("foo", justify="right") result = console.end_capture() assert result == " foo\n"
def output( self, outdata: Union[List[str], Dict[str, Any]], tablefmt: str = "rich", title: str = None, caption: str = None, account: str = None, config=None, set_width_cols: dict = None, full_cols: Union[List[str], str] = [], fold_cols: Union[List[str], str] = [], ok_status: Union[int, List[int], Tuple[int, str], List[Tuple[int, str]]] = None, ) -> str: # log.debugv(f"data passed to output():\n{pprint(outdata, indent=4)}") def _do_subtables(data: list, tablefmt: str = "rich"): out = [] for inner_dict in data: # the object: switch/vlan etc dict for key, val in inner_dict.items(): if not isinstance(val, (list, dict, tuple)): if val is None: inner_dict[key] = '' elif isinstance(val, str) and val.lower() in ['up', 'down']: color = 'red' if val.lower() == 'down' else 'green' if tablefmt == 'rich': inner_dict[key] = f'[b {color}]{val.title()}[/b {color}]' else: inner_dict[key] = typer.style(val.title(), fg=color) else: if tablefmt == 'rich': inner_dict[key] = Text(str(val), style=None) else: inner_dict[key] = str(val) else: val = self.listify(val) if val and tablefmt == "rich" and hasattr(val[0], 'keys'): inner_table = Table(*(k for k in val[0].keys()), show_header=True, # padding=(0, 0), pad_edge=False, collapse_padding=True, show_edge=False, header_style="bold cyan", box=SIMPLE ) _ = [inner_table.add_row(*[self.do_pretty(kk, str(vv)) for kk, vv in v.items()]) for v in val] console.begin_capture() console.print(inner_table) inner_dict[key] = console.end_capture() elif val and tablefmt == "tabulate" and hasattr(val[0], 'keys'): inner_table = tabulate(val, headers="keys", tablefmt=tablefmt) inner_dict[key] = inner_table else: if all(isinstance(v, str) for v in val): inner_dict[key] = ", ".join(val) out.append(inner_dict) return out raw_data = outdata _lexer = table_data = None if config and config.sanitize and raw_data and all(isinstance(x, dict) for x in raw_data): redact = ["mac", "serial", "neighborMac", "neighborSerial", "neighborPortMac", "longitude", "latitude"] outdata = [{k: d[k] if k not in redact else "--redacted--" for k in d} for d in raw_data] # -- // List[str, ...] \\ -- Bypass all formatters, (config file output, etc...) if outdata and all(isinstance(x, str) for x in outdata): tablefmt = "strings" # -- convert List[dict] --> Dict[dev_name: dict] for yaml/json outputs if tablefmt in ['json', 'yaml', 'yml']: outdata = self.listify(outdata) if outdata and 'name' in outdata[0]: outdata: Dict[str, Dict[str, Any]] = { item['name']: {k: v for k, v in item.items() if k != 'name'} for item in outdata } if tablefmt == "json": raw_data = json.dumps(outdata, indent=4) _lexer = lexers.JsonLexer elif tablefmt in ["yml", "yaml"]: raw_data = yaml.dump(outdata, sort_keys=False) _lexer = lexers.YamlLexer elif tablefmt == "csv": raw_data = table_data = "\n".join( [ ",".join( [ k if outdata.index(d) == 0 else str(v) for k, v in d.items() if k not in CUST_KEYS ]) for d in outdata ]) elif tablefmt == "rich": from rich.console import Console from rich.table import Table from rich.box import HORIZONTALS, SIMPLE from rich.text import Text # from rich.progress import Progress from centralcli import constants console = Console(record=True, emoji=False) customer_id, customer_name = "", "" # outdata = self.listify(outdata) # -- // List[dict, ...] \\ -- if outdata and all(isinstance(x, dict) for x in outdata): customer_id = outdata[0].get("customer_id", "") customer_name = outdata[0].get("customer_name", "") outdata = [{k: v for k, v in d.items() if k not in CUST_KEYS} for d in outdata] table = Table( # show_edge=False, show_header=True, title=title, header_style='magenta', show_lines=False, box=HORIZONTALS, row_styles=['none', 'dark_sea_green'] ) fold_cols = [*fold_cols, 'description'] _min_max = {'min': 10, 'max': 30} set_width_cols = set_width_cols or {'name': _min_max, 'model': _min_max} # default full cols #TODO clean this up _full_cols = ['mac', 'serial', 'ip', 'public ip', 'version', 'radio', 'id'] full_cols = [*full_cols, *_full_cols] for k in outdata[0].keys(): if k in fold_cols: table.add_column(k, overflow='fold', max_width=115, justify='left') elif k in set_width_cols: table.add_column( k, min_width=set_width_cols[k]['min'], max_width=set_width_cols[k]['max'], justify='left' ) elif k in full_cols: table.add_column(k, no_wrap=True, justify='left') else: table.add_column(k, justify='left') formatted = _do_subtables(outdata) [table.add_row(*list(in_dict.values())) for in_dict in formatted] if title: table.title = f'[italic cornflower_blue]{constants.what_to_pretty(title)}' if account or caption: table.caption_justify = 'left' table.caption = '' if not account else f'[italic dark_olive_green2] Account: {account}' if caption: table.caption = f"[italic dark_olive_green2]{table.caption} {caption}" data_header = f"--\n{'Customer ID:':15}{customer_id}\n{'Customer Name:':15} {customer_name}\n--\n" # TODO look into this. console.capture stopped working reliably this works console.begin_capture() console.print(table) table_data = console.end_capture() raw_data = typer.unstyle(table_data) if customer_id: raw_data = f"{data_header}{raw_data}" table_data = f"{data_header}{table_data}" elif tablefmt == "tabulate": customer_id = customer_name = "" outdata = self.listify(outdata) # -- // List[dict, ...] \\ -- if outdata and all(isinstance(x, dict) for x in outdata): customer_id = outdata[0].get("customer_id", "") customer_name = outdata[0].get("customer_name", "") outdata = [{k: v for k, v in d.items() if k not in CUST_KEYS} for d in outdata] raw_data = outdata outdata = _do_subtables(outdata, tablefmt=tablefmt) # outdata = [dict((k, v) for k, v in zip(outdata[0].keys(), val)) for val in outdata] table_data = tabulate(outdata, headers="keys", tablefmt=tablefmt) td = table_data.splitlines(keepends=True) if td: table_data = f"{typer.style(td[0], fg='cyan')}{''.join(td[1:])}" data_header = f"--\n{'Customer ID:':15}{customer_id}\n" \ f"{'Customer Name:':15} {customer_name}\n--\n" table_data = f"{data_header}{table_data}" if customer_id else f"{table_data}" raw_data = f"{data_header}{raw_data}" if customer_id else f"{raw_data}" else: # strings output No formatting # -- // List[str, ...] \\ -- if len(outdata) == 1: if "\n" not in outdata[0]: # we can format green as only success output is sent through formatter. table_data = typer.style(f" {outdata[0]}", fg="green") raw_data = outdata[0] else: # template / config file output # get rid of double nl @ EoF (configs) raw_data = table_data = "{}\n".format('\n'.join(outdata).rstrip('\n')) else: raw_data = table_data = '\n'.join(outdata) # Not sure what hit's this, but it was created so something must log.debug("List[str] else hit") if _lexer and raw_data: table_data = highlight(bytes(raw_data, 'UTF-8'), _lexer(), formatters.Terminal256Formatter(style='solarized-dark') ) return self.Output(rawdata=raw_data, prettydata=table_data, config=config)
def test_render_single_node(): tree = Tree("foo") console = Console(color_system=None, width=20) console.begin_capture() console.print(tree) assert console.end_capture() == "foo \n"
def test_ipy_display_hook__console_renderables_on_newline(): console = Console(file=io.StringIO(), force_jupyter=True) console.begin_capture() _ipy_display_hook(Text("hello"), console=console) assert console.end_capture() == "\nhello\n"
def test_expand_false(): console = Console(width=100, color_system=None) console.begin_capture() console.print(Padding("foo", 1, expand=False)) assert console.end_capture() == " \n foo \n \n"
import json import io from time import time from rich.console import Console from rich.pretty import Pretty console = Console(file=io.StringIO(), color_system="truecolor", width=100) with open("cats.json") as fh: cats = json.load(fh) console.begin_capture() start = time() pretty = Pretty(cats) console.print(pretty, overflow="ignore", crop=False) result = console.end_capture() taken = (time() - start) * 1000 print(result) print(console.file.getvalue()) print(f"{taken:.1f}")
def config_( group_dev: str = typer.Argument( ..., metavar="GROUP|DEVICE", help="Group or device to update.", autocompletion=cli.cache.group_dev_ap_gw_completion ), # TODO simplify structure can just remove device arg # device: str = typer.Argument( # None, # autocompletion=cli.cache.dev_ap_gw_completion # # TODO dev type gw or ap only # # autocompletion=lambda incomplete: [ # # c for c in cli.cache.dev_completion(incomplete, dev_type="gw") if c.lower().startswith(incomplete.lower()) # # ] # ), # TODO collect multi-line input as option to paste in config cli_file: Path = typer.Argument(..., help="File containing desired config/template in CLI format.", exists=True, autocompletion=lambda incomplete: tuple()), var_file: Path = typer.Argument(None, help="File containing variables for j2 config template.", exists=True, autocompletion=lambda incomplete: tuple()), # TODO --vars PATH help="File containing variables to convert jinja2 template." yes: bool = typer.Option(False, "-Y", help="Bypass confirmation prompts - Assume Yes"), yes_: bool = typer.Option(False, "-y", hidden=True), do_gw: bool = typer.Option(None, "--gw", help="Show group level config for gateways."), do_ap: bool = typer.Option(None, "--ap", help="Show group level config for APs."), debug: bool = typer.Option(False, "--debug", envvar="ARUBACLI_DEBUG", help="Enable Additional Debug Logging",), default: bool = typer.Option(False, "-d", is_flag=True, help="Use default central account", show_default=False,), account: str = typer.Option("central_info", envvar="ARUBACLI_ACCOUNT", help="The Aruba Central Account to use (must be defined in the config)",), ) -> None: yes = yes_ if yes_ else yes group_dev: CentralObject = cli.cache.get_identifier(group_dev, qry_funcs=["group", "dev"], device_type=["ap", "gw"]) config_out = utils.generate_template(cli_file, var_file=var_file) cli_cmds = utils.validate_config(config_out) # TODO render.py module with helper function to return styled rule/line console = Console(record=True, emoji=False) console.begin_capture() console.rule("Configuration to be sent") console.print("\n".join([f"[green]{line}[/green]" for line in cli_cmds])) console.rule() console.print(f"\nUpdating {'group' if group_dev.is_group else group_dev.generic_type.upper()} [cyan]{group_dev.name}") _msg = console.end_capture() if group_dev.is_group: device = None if not do_ap and not do_gw: print("Invalid Input, --gw or --ap option must be supplied for group level config.") raise typer.Exit(1) else: # group_dev is a device iden device = group_dev if do_gw or (device and device.generic_type == "gw"): if device and device.generic_type != "gw": print(f"Invalid input: --gw option conflicts with {device.name} which is an {device.generic_type}") raise typer.Exit(1) use_caas = True caasapi = caas.CaasAPI(central=cli.central) # XXX Burried import node_iden = group_dev.name if group_dev.is_group else group_dev.mac elif do_ap or (device and device.generic_type == "ap"): if device and device.generic_type != "ap": print(f"Invalid input: --ap option conflicts with {device.name} which is a {device.generic_type}") raise typer.Exit(1) use_caas = False node_iden = group_dev.name if group_dev.is_group else group_dev.serial typer.echo(_msg) if yes or typer.confirm("Proceed?", abort=True): if use_caas: resp = cli.central.request(caasapi.send_commands, node_iden, cli_cmds) cli.display_results(resp, cleaner=cleaner.parse_caas_response) else: # FIXME this is OK for group level ap config , for AP this method is not valid if group_dev.is_dev: print("Not Implemented yet for AP device level updates") raise typer.Exit(1) resp = cli.central.request(cli.central.replace_ap_config, node_iden, cli_cmds) cli.display_results(resp, tablefmt="action")
def display_results( self, resp: Union[Response, List[Response]] = None, data: Union[List[dict], List[str], dict, None] = None, tablefmt: TableFormat = "rich", title: str = None, caption: str = None, pager: bool = False, outfile: Path = None, sort_by: str = None, reverse: bool = False, stash: bool = True, pad: int = None, exit_on_fail: bool = False, ok_status: Union[int, List[int], Dict[int, str]] = None, set_width_cols: dict = None, full_cols: Union[List[str], str] = [], fold_cols: Union[List[str], str] = [], cleaner: callable = None, **cleaner_kwargs, ) -> None: """Output Formatted API Response to display and optionally to file one of resp or data attribute is required Args: resp (Union[Response, List[Response], None], optional): API Response objects. data (Union[List[dict], List[str], None], optional): API Response output data. tablefmt (str, optional): Format of output. Defaults to "rich" (tabular). Valid Values: "json", "yaml", "csv", "rich", "simple", "tabulate", "raw", "action" Where "raw" is unformatted raw response and "action" is formatted for POST|PATCH etc. where the result is a simple success/error. title: (str, optional): Title of output table. Only applies to "rich" tablefmt. Defaults to None. caption: (str, optional): Caption displayed at bottome of table. Only applies to "rich" tablefmt. Defaults to None. pager (bool, optional): Page Output / or not. Defaults to True. outfile (Path, optional): path/file of output file. Defaults to None. sort_by (Union[str, List[str], None] optional): column or columns to sort output on. reverse (bool, optional): reverse the output. stash (bool, optional): stash (cache) the output of the command. The CLI can re-display with show last. Default: True ok_status (Union[int, List[int], Tuple[int, str], List[Tuple[int, str]]], optional): By default responses with status_code 2xx are considered OK and are rendered as green by Output class. provide int or list of int to override additional status_codes that should also be rendered as success/green. provide a dict with {int: str, ...} where string can be any color supported by Output class or "neutral" "success" "fail" where neutral is no formatting, and success / fail will use the default green / red respectively. set_width_cols (Dict[str: Dict[str, int]]): Passed to output function defines cols with min/max width example: {'details': {'min': 10, 'max': 30}, 'device': {'min': 5, 'max': 15}} full_cols (list): columns to ensure are displayed at full length (no wrap no truncate) cleaner (callable, optional): The Cleaner function to use. """ # TODO remove ok_status, and handle in CentralAPI method (set resp.ok = True) if pad: log.error("Deprecated pad parameter referenced in display_results", show=True) if resp is not None: resp = utils.listify(resp) # update caption with rate limit if resp[-1].rl: rl_str = f"[italic dark_olive_green2]{resp[-1].rl}[/]".lstrip() caption = f"{caption}\n {rl_str}" if caption else f" {rl_str}" for idx, r in enumerate(resp): # Multi request url line m_colors = { "GET": "bright_green", "DELETE": "red", "PATH": "dark_orange3", "PUT": "dark_orange3", "POST": "dark_orange3" } fg = "bright_green" if r else "red" conditions = [ len(resp) > 1, tablefmt in ["action", "raw"], r.ok and not r.output ] if any(conditions): _url = r.url if not hasattr(r.url, "raw_path_qs") else r.url.path m_color = m_colors.get(r.method, "reset") print(f"Request {idx + 1} [[{m_color}]{r.method}[reset]: " f"[cyan]{_url}[/cyan]]\n [fg]Response[reset]:") if self.raw_out: tablefmt = "raw" if not r.output: c = Console(record=True) c.begin_capture() c.print(f" Status Code: [{fg}]{r.status}[/]") c.print( f" :warning: Empty Response. This may be normal.") r.output = c.end_capture() if not r or tablefmt in ["action", "raw"]: if tablefmt == "raw": # dots = f"[{fg}]{'.' * 16}[/{fg}]" status_code = f"[{fg}]status code: {r.status}[/{fg}]" print(r.url) print(status_code) if not r.ok: print(r.error) # print(f"{dots}\n{status_code}\n{dots}") print( "[bold cyan]Unformatted response from Aruba Central API GW[/bold cyan]" ) print(r.raw) if outfile: self.write_file(outfile, r.raw) else: print(f"[{fg}]{r}") if idx + 1 == len(resp): console.print(f"\n{rl_str}") else: self._display_results(r.output, tablefmt=tablefmt, title=title, caption=caption, pager=pager, outfile=outfile, sort_by=sort_by, reverse=reverse, stash=stash, pad=pad, set_width_cols=set_width_cols, full_cols=full_cols, fold_cols=fold_cols, cleaner=cleaner, **cleaner_kwargs) # TODO make elegant caas send-cmds uses this logic if cleaner and cleaner.__name__ == "parse_caas_response": print(caption) if exit_on_fail and not all([r.ok for r in resp]): raise typer.Exit(1) elif data: self._display_results(data, tablefmt=tablefmt, title=title, caption=caption, pager=pager, outfile=outfile, sort_by=sort_by, reverse=reverse, stash=stash, pad=pad, set_width_cols=set_width_cols, full_cols=full_cols, fold_cols=fold_cols, cleaner=cleaner, **cleaner_kwargs)