diff options
author | Henrique Joaquim <henriquecjoaquim@gmail.com> | 2024-06-26 14:08:56 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-26 13:08:56 +0000 |
commit | 5e69ce3d259d3af6660c99ae22963e107e3bc77e (patch) | |
tree | 3b05e84803861575822e5500eb26eaf7e924279a | |
parent | 37903a1caea19fb4d4f1833c4079d87c586fe064 (diff) |
[Feature] Improvements to handling charts on the CLI `results` (#6544)
* better handling of the chart arg: falls back to charting.to_chart() and only then, if not possible to display falls back to the print_rich_table()
* handling chart arguments aka unknown arguments
* propagating changes on return type of parse_simple_args to choices.py for coherency
-rw-r--r-- | cli/openbb_cli/controllers/base_controller.py | 93 | ||||
-rw-r--r-- | cli/openbb_cli/controllers/choices.py | 7 | ||||
-rw-r--r-- | cli/openbb_cli/controllers/cli_controller.py | 3 | ||||
-rw-r--r-- | cli/openbb_cli/controllers/settings_controller.py | 4 | ||||
-rw-r--r-- | cli/openbb_cli/controllers/utils.py | 74 |
5 files changed, 134 insertions, 47 deletions
diff --git a/cli/openbb_cli/controllers/base_controller.py b/cli/openbb_cli/controllers/base_controller.py index 91a353f8b60..da54491c11e 100644 --- a/cli/openbb_cli/controllers/base_controller.py +++ b/cli/openbb_cli/controllers/base_controller.py @@ -8,7 +8,7 @@ import shlex from abc import ABCMeta, abstractmethod from datetime import datetime from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, List, Literal, Optional, Tuple, Union import pandas as pd from openbb_cli.config.completer import NestedCompleter @@ -19,7 +19,9 @@ from openbb_cli.controllers.utils import ( check_file_type_saved, check_positive, get_flair_and_username, + handle_obbject_display, parse_and_split_input, + parse_unknown_args_to_dict, print_guest_block_msg, print_rich_table, remove_file, @@ -373,7 +375,7 @@ class BaseController(metaclass=ABCMeta): if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-n") - ns_parser = self.parse_simple_args(parser, other_args) + ns_parser, _ = self.parse_simple_args(parser, other_args) if ns_parser: if not ns_parser.name: @@ -472,7 +474,7 @@ class BaseController(metaclass=ABCMeta): description="Stop recording session into .openbb routine file", ) # This is only for auto-completion purposes - _ = self.parse_simple_args(parser, other_args) + _, _ = self.parse_simple_args(parser, other_args) if "-h" not in other_args and "--help" not in other_args: global RECORD_SESSION # noqa: PLW0603 @@ -603,7 +605,7 @@ class BaseController(metaclass=ABCMeta): prog="whoami", description="Show current user", ) - ns_parser = self.parse_simple_args(parser, other_args) + ns_parser, _ = self.parse_simple_args(parser, other_args) if ns_parser: current_user = session.user @@ -635,11 +637,27 @@ class BaseController(metaclass=ABCMeta): "--chart", action="store_true", dest="chart", help="Display chart." ) parser.add_argument( - "--export", dest="export", help="Export data.", nargs="+", default=None + "--export", + default="", + type=check_file_type_saved(["csv", "json", "xlsx", "png", "jpg"]), + dest="export", + help="Export raw data into csv, json, xlsx and figure into png or jpg.", + nargs="+", + ) + parser.add_argument( + "--sheet-name", + dest="sheet_name", + default=None, + nargs="+", + help="Name of excel sheet to save data to. Only valid for .xlsx files.", + ) + + ns_parser, unknown_args = self.parse_simple_args( + parser, other_args, unknown_args=True ) - ns_parser = self.parse_simple_args(parser, other_args) if ns_parser: + kwargs = parse_unknown_args_to_dict(unknown_args) if not ns_parser.index and not ns_parser.key: results = session.obbject_registry.all if results: @@ -657,21 +675,13 @@ class BaseController(metaclass=ABCMeta): index = int(ns_parser.index) obbject = session.obbject_registry.get(index) if obbject: - if ns_parser.chart and obbject.chart: - obbject.show() - else: - title = obbject.extra.get("command", "") - df = obbject.to_dataframe() - print_rich_table( - df=df, - show_index=True, - title=title, - export=ns_parser.export, - ) - if ns_parser.chart and not obbject.chart: - session.console.print( - "[info]No chart available.[/info]" - ) + handle_obbject_display( + obbject=obbject, + chart=ns_parser.chart, + export=ns_parser.export, + sheet_name=ns_parser.sheet_name, + **kwargs, + ) else: session.console.print( f"[info]No result found at index {index}.[/info]" @@ -683,26 +693,24 @@ class BaseController(metaclass=ABCMeta): elif ns_parser.key: obbject = session.obbject_registry.get(ns_parser.key) if obbject: - if ns_parser.chart and obbject.chart: - obbject.show() - else: - title = obbject.extra.get("command", "") - df = obbject.to_dataframe() - print_rich_table( - df=df, - show_index=True, - title=title, - export=ns_parser.export, - ) - if ns_parser.chart and not obbject.chart: - session.console.print("[info]No chart available.[/info]") + handle_obbject_display( + obbject=obbject, + chart=ns_parser.chart, + export=ns_parser.export, + sheet_name=ns_parser.sheet_name, + **kwargs, + ) else: session.console.print( f"[info]No result found with key '{ns_parser.key}'.[/info]" ) @staticmethod - def parse_simple_args(parser: argparse.ArgumentParser, other_args: List[str]): + def parse_simple_args( + parser: argparse.ArgumentParser, + other_args: List[str], + unknown_args: bool = False, + ) -> Tuple[Optional[argparse.Namespace], Optional[List[str]]]: """Parse list of arguments into the supplied parser. Parameters @@ -711,11 +719,15 @@ class BaseController(metaclass=ABCMeta): Parser with predefined arguments other_args: List[str] List of arguments to parse + unknown_args: bool + Flag to indicate if unknown arguments should be returned Returns ------- - ns_parser: + ns_parser: argparse.Namespace Namespace with parsed arguments + l_unknown_args: List[str] + List of unknown arguments """ parser.add_argument( "-h", "--help", action="store_true", help="show this help message" @@ -729,19 +741,18 @@ class BaseController(metaclass=ABCMeta): except SystemExit: # In case the command has required argument that isn't specified session.console.print("\n") - return None + return None, None if ns_parser.help: txt_help = parser.format_help() session.console.print(f"[help]{txt_help}[/help]") - return None + return None, None - if l_unknown_args: + if l_unknown_args and not unknown_args: session.console.print( f"The following args couldn't be interpreted: {l_unknown_args}\n" ) - - return ns_parser + return ns_parser, l_unknown_args @classmethod def parse_known_args_and_warn( diff --git a/cli/openbb_cli/controllers/choices.py b/cli/openbb_cli/controllers/choices.py index 3afc5234c11..f4e87552095 100644 --- a/cli/openbb_cli/controllers/choices.py +++ b/cli/openbb_cli/controllers/choices.py @@ -4,7 +4,7 @@ from argparse import SUPPRESS, ArgumentParser from contextlib import contextmanager from inspect import isfunction, unwrap from types import MethodType -from typing import Callable, List, Literal +from typing import Callable, List, Literal, Tuple from unittest.mock import patch from openbb_cli.controllers.utils import ( @@ -110,7 +110,7 @@ def __mock_parse_known_args_and_warn( ) -def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> None: +def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> Tuple: """Add arguments. Add the arguments that would have normally added by: @@ -127,6 +127,7 @@ def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> N "-h", "--help", action="store_true", help="show this help message" ) _ = other_args + return None, None def __get_command_func(controller, command: str): @@ -216,7 +217,7 @@ def __patch_controller_functions(controller): target=controller, attribute="parse_simple_args", side_effect=__mock_parse_simple_args, - return_value=None, + return_value=(None, None), ), patch.object( target=controller, diff --git a/cli/openbb_cli/controllers/cli_controller.py b/cli/openbb_cli/controllers/cli_controller.py index 004239bf436..9bacd479505 100644 --- a/cli/openbb_cli/controllers/cli_controller.py +++ b/cli/openbb_cli/controllers/cli_controller.py @@ -205,10 +205,11 @@ class CLIController(BaseController): choices["results"] = { "--help": None, "-h": "--help", - "--export": None, + "--export": {c: None for c in ["csv", "json", "xlsx", "png", "jpg"]}, "--index": None, "--key": None, "--chart": None, + "--sheet_name": None, } self.update_completer(choices) diff --git a/cli/openbb_cli/controllers/settings_controller.py b/cli/openbb_cli/controllers/settings_controller.py index da8add50b52..ab1099362f0 100644 --- a/cli/openbb_cli/controllers/settings_controller.py +++ b/cli/openbb_cli/controllers/settings_controller.py @@ -80,7 +80,7 @@ class SettingsController(BaseController): description=field["description"], add_help=False, ) - ns_parser = self.parse_simple_args(parser, other_args) + ns_parser, _ = self.parse_simple_args(parser, other_args) if ns_parser: session.settings.set_item( field_name, not getattr(session.settings, field_name) @@ -113,7 +113,7 @@ class SettingsController(BaseController): type=type_, # type: ignore[arg-type] choices=choices, ) - ns_parser = self.parse_simple_args(parser, other_args) + ns_parser, _ = self.parse_simple_args(parser, other_args) if ns_parser: if ns_parser.value: # Console style is applied immediately diff --git a/cli/openbb_cli/controllers/utils.py b/cli/openbb_cli/controllers/utils.py index c921ad5ab3a..5ab5a817a5e 100644 --- a/cli/openbb_cli/controllers/utils.py +++ b/cli/openbb_cli/controllers/utils.py @@ -21,6 +21,7 @@ from openbb_charting.core.backend import create_backend, get_backend from openbb_cli.config.constants import AVAILABLE_FLAIRS, ENV_FILE_SETTINGS from openbb_cli.session import Session from openbb_core.app.model.charts.charting_settings import ChartingSettings +from openbb_core.app.model.obbject import OBBject from pytz import all_timezones, timezone from rich.table import Table @@ -974,3 +975,76 @@ def request( timeout=timeout, **kwargs, ) + + +def parse_unknown_args_to_dict(unknown_args: Optional[List[str]]) -> Dict[str, str]: + """Parse unknown arguments to a dictionary.""" + unknown_args_dict = {} + if unknown_args: + for idx, arg in enumerate(unknown_args): + if arg.startswith("--"): + if idx + 1 < len(unknown_args): + try: + unknown_args_dict[arg.replace("--", "")] = ( + eval( # noqa: S307, E501 pylint: disable=eval-used + unknown_args[idx + 1] + ) + ) + except Exception: + unknown_args_dict[arg] = unknown_args[idx + 1] + else: + session.console.print( + f"Missing value for argument {arg}. Skipping this argument." + ) + return unknown_args_dict + + +def handle_obbject_display( + obbject: OBBject, + chart: bool = False, + export: str = "", + sheet_name: str = "", + **kwargs, +): + """Handle the display of an OBBject.""" + df: pd.DataFrame = pd.DataFrame() + fig: Optional[OpenBBFigure] = None + if chart: + try: + if obbject.chart: + obbject.show(**kwargs) + else: + obbject.charting.to_chart(**kwargs) + if export: + fig = obbject.chart.fig + df = obbject.to_dataframe() + except Exception as e: + session.console.print(f"Failed to display chart: {e}") + else: + df = obbject.to_dataframe() + print_rich_table( + df=df, + show_index=True, + title=obbject.extra.get("command", ""), + export=bool(export), + ) + if export and not df.empty: + if sheet_name and isinstance(sheet_name, list): + sheet_name = sheet_name[0] + + func_name = ( + obbject.extra.get("command", "") + .replace("/", "_") + .replace(" ", "_") + .replace("--", "_") + ) + export_data( + export_type=",".join(export), + dir_path=os.path.dirname(os.path.abspath(__file__)), + func_name=func_name, + df=df, + sheet_name=sheet_name, + figure=fig, + ) + elif export and df.empty: + session.console.print("[yellow]No data to export.[/yellow]") |